/* ==========================================================================
   Oxford Optical — website stylesheet
   Design system: Oxford Optical brand tokens (colour, type, hairlines, radii)
   expressed through the compositional grammar of augen.pro — a 5-column vw
   grid, one type family, opacity-driven hierarchy, hairline rules, and very
   large vw-based vertical rhythm.
   Tokens sourced from: Oxford Optical Design System (tokens/*.css).
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. Self-hosted faces

   These replace the Google Fonts CDN link that used to sit in each page's
   <head>, and they replace the comp's Nohemi.

   WHY NOHEMI IS GONE. It is commercial (Pangram Pangram), was never licensed
   for the web, and was therefore never going to ship — §8c has carried that
   warning since it was written. What actually happened in the meantime is
   worse than a missing face: it silently fell back to Space Grotesk, so the
   site's display face was an accident rather than a decision. Both faces are
   also installed on the authoring machine, which is why nobody noticed.

   WHY SELF-HOSTED. The old <head> link carried a PROVISIONAL note asking for
   exactly this before launch: a third-party font request from a UK site is a
   GDPR question that self-hosting removes, and it drops a render-blocking
   round trip to a second origin.

   LICENCE. Instrument Sans and IBM Plex Mono are both SIL Open Font License
   1.1 — free to self-host and to use commercially, with attribution retained.
   See assets/fonts/OFL.txt. Neither file may be renamed in a way that implies
   it is the original: these are the unmodified latin subsets as served by
   Google Fonts, kept whole rather than re-subset so that the −, —, ·, ’ and ©
   the site uses elsewhere all survive (verified against the cmap, not assumed).

   font-display: swap, not block. The hero's type IS the hero; a blocked paint
   is worse than one reflow, and the fallback stacks below are metric-near.
   -------------------------------------------------------------------------- */

@font-face {
  font-family: "Instrument Sans";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("../assets/fonts/InstrumentSans-Medium.woff2") format("woff2");
}

@font-face {
  font-family: "Instrument Sans";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../assets/fonts/InstrumentSans-Bold.woff2") format("woff2");
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/IBMPlexMono-Regular.woff2") format("woff2");
}

/* --------------------------------------------------------------------------
   1. Reset
   -------------------------------------------------------------------------- */

*,
*::before,
*::after { box-sizing: border-box; }

* { margin: 0; padding: 0; }

html {
  /* 1rem = 10px, so the rem values below read as px */
  font-size: 62.5%;
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

img,
svg { display: block; max-width: 100%; }

button {
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

a { color: inherit; text-decoration: none; }

ul { list-style: none; }

/* --------------------------------------------------------------------------
   2. Tokens
   -------------------------------------------------------------------------- */

:root {
  /* — Ink & neutrals — */
  --black:      #000000;
  --ink:        #070117;
  --white:      #FFFFFF;
  --grey-100:   #EEEEEE;
  --grey-300:   #CCCCCC;
  --grey-700:   #616161;

  /* — Signature: Oxford Blue. The only accent on the site. — */
  --navy:       #0A1A4A;
  --navy-deep:  #000F43;

  /* Oxford Blue 1, the tone ramp's first tint — the HIGHLIGHT blue, added
     2026-08-27. Directed: the heading highlights "are still dark from what I
     can see" after being pointed at --navy.

     WHY A SECOND BLUE IS NEEDED, because this looks like duplication and is
     not. --navy #0A1A4A and this are both "Oxford Blue" to the eye only at
     LARGE AREA. As the closing strapline's full-bleed background, #0A1A4A
     reads unmistakably blue. In an <em> inside a heading it is a few thin
     strokes on white, and a dark saturated colour needs area to show its hue —
     so the same hex reads near-black there. That is what was reported, and it
     is a perceptual fact, not a bug: measured, the two uses are the identical
     rgb(10, 26, 74).

     So: --navy stays the colour of FIELDS (the strapline, the hero vignette,
     buttons) and --navy-1 is the colour of INK at heading scale. Do not
     "unify" them; they are the same brand colour compensated for area.

     NOTE FOR WHOEVER OWNS THE BRAND: `OO Stitch design.md` lists this swatch as
     "Tone-ramp tint — GRADIENT USE ONLY" and says Oxford Blue #0A1A4A "is the
     only accent for interactive and brand elements". Using it as heading ink is
     a departure from both. Directed by the AI Lead, who owns the project, so it
     is applied — but the guideline document still says otherwise and should be
     updated rather than left to contradict the build. Measured 10.86:1 on
     white, so contrast is not the concern. */
  --navy-1:     #003399;
  /* Oxford Blue as channels, for consumers that need it behind an alpha ramp —
     currently §44's hero vignette. Same idiom as §23/§44's `--hero-scrim`, and
     preferred over relative-colour syntax (`rgb(from var(--navy) r g b / a)`),
     which is newer than this site's baseline and would fail to nothing.
     KEEP IN SYNC with --navy above; they are the same colour by definition. */
  --navy-rgb:   10, 26, 74;


  /* — Power-map spectrum. One gradient per page, as a hairline rule. — */
  --spectrum: linear-gradient(90deg,
    #0A1A4A 0%, #006699 22%, #009999 40%,
    #33CC99 56%, #66FF99 68%, #FFFF66 84%, #FF3366 100%);

  /* — Surfaces — */
  --paper:      #FFFFFF;
  --paper-alt:  #EEEEEE;
  --surface-ink: #070117;

  /* — Text. Hierarchy is carried by opacity, not by separate greys.
       t3/d3 carry the small mono labels, so both are held at the WCAG AA
       floor for normal text rather than pushed as light as they look. — */
  --t1: var(--black);
  --t2: rgba(7, 1, 23, 0.64);   /* body secondary   — 7.0:1 on white */
  --t3: rgba(7, 1, 23, 0.56);   /* mono labels      — 4.8:1 on white */
  --d1: var(--white);
  --d2: rgba(255, 255, 255, 0.66); /* body secondary — 8.8:1 on ink  */
  --d3: rgba(255, 255, 255, 0.48); /* mono labels    — 4.9:1 on ink  */

  /* — Hairlines — */
  --rule:      rgba(7, 1, 23, 0.15);
  --rule-dark: rgba(255, 255, 255, 0.19);

  /* — Type — */
  /* Was Arial / "Courier New". Both were defaults rather than choices, and
     Courier in particular renders differently on every platform and sits
     markedly lighter than the type it annotates. See §0. */
  --font-sans: "Instrument Sans", Arial, "Helvetica Neue", Helvetica, sans-serif;
  --font-mono: "IBM Plex Mono", "Courier New", Courier, monospace;

  /* Nohemi and Space Grotesk both removed 2026-08-26 — see §0. One family now
     carries display, UI and body; the mono is the only second voice. Keeping
     --font-display and --font-ui as separate tokens even though they resolve
     the same: the hero and the section headlines are still allowed to diverge
     later, and collapsing them to one name would hide that they can.

     --font-impact has no consumer any more. The hero's oversized comma went
     with the Option B lockup (§44), and Impact was never safe anyway: it ships
     on Windows and macOS but not Linux or Android. Anton is the OFL analogue
     if the device is ever wanted back. Left defined, per the note at §26 on
     why dead declarations get a deliberate pass rather than a hand-removal. */
  --font-display: "Instrument Sans", Arial, sans-serif;
  --font-ui:      "Instrument Sans", Arial, sans-serif;
  --font-impact:  Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif;

  /* Taken straight from the comp. Two greys rather than black: the display
     lines are the darker of the pair, the eyebrow and outcome lines lighter. */
  --hero-ink:  #111111;
  --hero-grey: #3F3F3F;

  --fs-display: clamp(3.2rem, 5.2vw, 7.6rem);
  --fs-h2:      clamp(2.9rem, 3.8vw, 5.4rem);
  --fs-h3:      clamp(2.2rem, 2.6vw, 3.4rem);
  --fs-lead:    clamp(1.9rem, 2.3vw, 2.8rem);
  --fs-title:   clamp(1.8rem, 1.9vw, 2.3rem);
  --fs-body:    1.6rem;
  --fs-small:   1.4rem;
  --fs-mono:    1.1rem;

  /* The interval between a section's statement headline and the body it
     introduces — see §5. A token rather than a literal because §24's .split now
     also answers a headline, and the two must not drift apart. */
  --gap-statement: clamp(3.2rem, 3.4vw, 5.6rem);

  --ls-display: -0.03em;
  --ls-h2:      -0.025em;
  --ls-lead:    -0.02em;
  --ls-body:    -0.005em;
  --ls-mono:     0.10em;

  /* — Grid —

     FOUR content columns between two annotation rails, not five columns with
     the first one left empty. See §4 for why that mattered.

     THE RAIL INTERPOLATES, and until 2026-08-27 it stepped. It was `0px` below
     1024 and `1.08fr` above, which put a cliff in the middle of the range real
     readers use — measured, both sides, at index.html:

       1023   73.6px of margin each side,  7.2% of the viewport, block 875.8px
       1024  214.4px of margin each side, 20.9% of the viewport, block 595.1px

     — the content block losing 280.7px across one pixel of window. And above
     1024 the proportion never eased, because a rail in `fr` and an outer in `vw`
     are both fractions of the viewport: 1024, 1097, 1280, 1440 and 1680 all sat
     at the same 20.9% each side. Browser zoom shrinks the CSS-pixel viewport, so
     a 1920 monitor at 150% zoom is 1280 CSS px, at 175% is 1097 and at 187.5% is
     1024 — every one of them inside that band, on an already-narrow page.
     Reported by the site owner: "the margins do look quite wide - pushing the
     content to be very central. It's mainly a problem for lower resolution
     screens / when the user is using larger magnification - so I don't want this
     to adversely affect those using high resolution monitors."

     So the rail is a LENGTH that ramps rather than a fraction that is constant.
     Three parts, and only one of them is a new number:

       foot   0px at 1024px and below. NOTHING at or below 1024 changes — the
              rail was already 0px there — so the 834-1024 compromise recorded
              below this block is untouched and no width loses content. The old
              step is the ramp's origin instead of a cliff.
       ramp   0.31px of rail per px of viewport. DERIVED, not chosen:
              252.98 / (1840 - 1024), where 252.98px is what the old `1.08fr`
              resolves to at --max. It is the one slope that MEETS the old rail
              at --max rather than crossing it, so the ramp arrives exactly at
              the historical value. Knee measured at 1839.8px.
       crest  --rail-cap, which is the old `1.08fr` rewritten as a length: 1.08
              of the 6.16fr the grid shared out, taken of the track total. It
              binds from --max upward, so 1840, 1920, 2560 and 3840 are
              unchanged to the pixel — 428.06px at 1920, as before.

     A length and not a fraction because the step was the thing that had to go,
     and `clamp()` interpolates lengths; `fr` is not one. The warning this comment
     used to carry still holds and is exactly what --rail-cap is for: above --max
     the container stops growing while `vw` does not, so an uncapped `vw` rail
     would keep growing and eat the page.

     WHAT 1.08 WAS FOR, AND WHY IT NO LONGER CONSTRAINS THIS. 1.08 was measured,
     not chosen: it put the content block's left edge on 428px of 1920, the axis
     images/HeroConcept.svg gives the LEFT-ANCHORED hero lockup (428.84 of
     1920 = 22.3%). That hero was replaced on 2026-08-26 by the Option B centred
     lockup (§44). §8b and §8c are scoped to `.hero-left`, and `.hero-left` no
     longer appears on either page — only inside HTML comments explaining that it
     was replaced. Verified by grep across all five pages that load this file.
     So NOTHING on the site reads the comp's 22.3% axis any more, and the
     agreement, while still true from --max upward where the cap binds, is now a
     byproduct rather than a constraint. §44's lockup is centred by
     `margin-inline: auto` inside a block that is itself symmetric, so it is
     indifferent to where the rail puts the block's edges.

     The one thing that DOES still have to agree with the rail is §37's header,
     and it now reads var(--rail) directly instead of re-deriving it.

     The block stays symmetric about the viewport centre BY CONSTRUCTION: both
     rails are the same token, so they are the same length whatever it resolves
     to, at every width. That property is load-bearing — see §4. */
  --rail-cap: calc((min(100vw, var(--max)) - 2 * var(--outer) - 5 * var(--gutter))
                   * 1.08 / 6.16);
  --rail: clamp(0px, calc((100vw - 1024px) * 0.31), var(--rail-cap));
  --gutter: 5.128vw;
  --outer:  6.4vw;
  --header-h: 6.4rem;
  --max: 1840px;

  /* — Radii — */
  --r-sm: 6px;
  --r-md: 10px;
  --r-lg: 16px;
  --r-pill: 999px;

  /* — Motion — */
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --dur-fast: 120ms;
  --dur: 200ms;
  --dur-slow: 320ms;
}

@media only screen and (min-width: 834px) {
  :root {
    --gutter: 2.398vw;
    --outer:  4.796vw;
    --header-h: 7.9rem;
  }
}

/* THE MEDIA QUERY THAT USED TO BE HERE IS GONE — a `min-width: 1024px` query
   whose whole body was `--rail: 1.08fr` on :root, and it was the cliff. (Written
   out rather than quoted: brace-counting is the check this file is audited with,
   and braces inside a comment break it.) §2's clamp covers the whole range in
   one expression now. The reasoning
   this block carried is still live, because it is why the ramp's foot sits at
   exactly 1024px rather than lower, so it is kept verbatim:

     The rails open at 1024, not at the 834 breakpoint where the two-column
     layouts begin. Between the two, giving away 2.16 columns to margin would
     take .split's halves down to 232px each — a portrait photograph and four
     paragraphs of copy in 232px is worse than either of them full width, and
     1.08fr of rail is a luxury a 900px window does not have. Below 1024 the
     rails collapse to nothing and the content block fills the padded width,
     which is itself a correction: the empty column was there at every size, not
     only on desktop.

     Below it the rail is 0px rather than a small fraction: a 0.02fr rail still
     resolved to 4px at 1023 and left the content block 28px off the header's
     own axis, which is a discrepancy with no author behind it. At 0px the two
     agree, and the content block is inset from the page edge by exactly one
     gutter — one number the header can read too.

   That argument is the reason the clamp's FLOOR is 0px and its foot is 1024px:
   the ramp cannot reach below 1024, so 834-1023 keeps the full padded width and
   .split's halves are never taken to 232px. Measured across the step:

                  1023      1024      block at 1024
     before      73.6px   214.4px          595.1px
     after       73.6px    73.7px          876.7px

   The line this block used to end with — "The step at 1024 is visible if you
   drag a window across it. That is the cost of a grid that is right at both ends
   rather than interpolated between them" — was an accurate statement of a cost
   that was then reported by the site owner. It is interpolated between them now,
   and the 0.1px residual at the join is the clamp's floor meeting a ramp whose
   foot is the same number. */

/* --------------------------------------------------------------------------
   3. Base
   -------------------------------------------------------------------------- */

body {
  background: var(--paper);
  color: var(--t1);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  font-weight: 400;
  letter-spacing: var(--ls-body);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

::selection { background: var(--navy); color: var(--white); }

:focus-visible {
  outline: 2px solid var(--navy);
  outline-offset: 3px;
  border-radius: 2px;
}

.section-ink :focus-visible { outline-color: var(--white); }

.skip-link {
  position: absolute;
  top: -100px;
  left: var(--outer);
  z-index: 200;
  padding: 1.2rem 2rem;
  background: var(--navy);
  color: var(--white);
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  border-radius: var(--r-sm);
}

.skip-link:focus { top: 1.2rem; }

/* --------------------------------------------------------------------------
   4. Grid container
   -------------------------------------------------------------------------- */

/* SIX tracks: an annotation rail, four content columns, an annotation rail.

   It was five equal columns with the content placed in 2-6, and column 1 left
   empty as an annotation margin. That margin was real on about.html, which put
   its section eyebrows in it, and dead on index.html, which had its eyebrows
   removed — but the cost was paid on both: content started 340px in from the
   left at 1920 and ran flush to the right edge, so every section was 170px
   right of the viewport's centre and the page read as though it had slid
   sideways. Nothing was wrong with any individual section, which is why this
   survived several passes: each one measured correct against the grid, and the
   grid was the thing that was off-centre.

   A margin on one side cannot be balanced by a flush edge on the other. So the
   rail is mirrored: two of them, content in tracks 2-5, and the block is
   symmetric about the viewport centre at every width by construction rather
   than by arithmetic. Every existing `grid-column: 2 / 6` placement still means
   "the content block", and `2 / 5`, `4 / 6`, `2 / 4` still mean the same
   fractions of it — the line numbers were already written against a six-line
   grid, they were just anchored to the wrong edge.

   --rail IS A LENGTH, NOT AN `fr`, since 2026-08-27 — see §2 for the whole
   argument. It changes nothing here: the track list is unchanged, tracks 2-5 are
   still the content block, and the symmetry above is still BY CONSTRUCTION
   rather than by arithmetic, because both rails are the same token and so
   resolve to the same length at every width, whatever that length is. The
   property that had to be protected while retuning the rail was this one. */
.container {
  display: grid;
  grid-template-columns: var(--rail) repeat(4, minmax(0, 1fr)) var(--rail);
  column-gap: var(--gutter);
  width: 100%;
  max-width: var(--max);
  margin-inline: auto;
  padding-inline: var(--outer);
}

/* Grid children must be allowed to shrink below their min-content width,
   otherwise a single nowrap element (a spec pill) widens the whole track
   and pushes the page past the viewport. */
.container > * { min-width: 0; }

/* Column placements. Mobile is always full-bleed; desktop indents to the
   grid so that column 1 is left free as an annotation margin. */
.col-full  { grid-column: 1 / -1; }
.col-label { grid-column: 1 / -1; }
.col-main  { grid-column: 1 / -1; }
.col-wide  { grid-column: 1 / -1; }

@media only screen and (min-width: 834px) {
  .col-label { grid-column: 1 / 2; }
  .col-main  { grid-column: 2 / 5; }
  .col-wide  { grid-column: 2 / 6; }
}

/* --------------------------------------------------------------------------
   5. Type utilities
   -------------------------------------------------------------------------- */

.display {
  font-size: var(--fs-display);
  font-weight: 400;
  letter-spacing: var(--ls-display);
  line-height: 1.02;
}

.h2 {
  font-size: var(--fs-h2);
  font-weight: 400;
  letter-spacing: var(--ls-h2);
  line-height: 1.08;
}

.h3 {
  font-size: var(--fs-h3);
  font-weight: 400;
  letter-spacing: var(--ls-h2);
  line-height: 1.14;
}

.lead {
  font-size: var(--fs-lead);
  font-weight: 400;
  letter-spacing: var(--ls-lead);
  line-height: 1.24;
}

.mono {
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  font-weight: 400;
  letter-spacing: var(--ls-mono);
  line-height: 1.4;
  text-transform: uppercase;
}

.muted   { color: var(--t2); }
.muted-d { color: var(--d2); }
.accent  { color: var(--navy); }

.prose > * + * { margin-top: 1.6em; }
.prose { color: var(--t2); }
.prose strong { color: var(--t1); font-weight: 400; }

/* The gap between a section's statement headline and the body it introduces.

   This was an inline `style="margin-top:6vw"` on each .prose, and a viewport
   unit cannot do this job. Measured:

     1600px   96.0px    wider than the headline's own 77.5px line box, so the
                        heading floats free of the copy it belongs to
     1280px   76.8px
      390px   23.4px    NARROWER than the 25.6px between two body paragraphs

   At the small end the hierarchy inverts outright: the headline ends up bound
   less tightly to its own body than the body's paragraphs are to each other.
   One declaration should not reverse a relationship across the range.

   So it is set as what it is — a typographic interval, in rem, clamped to stay
   inside its two neighbours at every width: always wider than the paragraph
   rhythm below it, always narrower than the section padding above it.

     1600px   54.4px      390px   32.0px   (vs 25.6 paragraph, 70.2 padding)

   Expressed as a sibling relationship rather than a class, so it cannot be
   applied to a .prose that is not answering a headline — and note that an
   inline style would still beat it, which is why those had to come out of the
   markup rather than just being overridden here.

   §24's .split needs the same interval, but it CANNOT be selected the same way.
   The split is a grid child of .container, a sibling of the .col-main that
   holds the headline — so .statement and .split have different parents and
   `.statement + .split` matches nothing at all. It is the .col-main that is
   adjacent to the split, not the heading inside it.

   Both selectors read the same token so the two intervals cannot drift. */
.statement + .prose,
.col-main + .split { margin-top: var(--gap-statement); }

/* NOT `.col-label + .split`. Where a section leads with a numbered eyebrow and
   no headline — About's mission block — the eyebrow occupies column 1 and the
   split columns 2-6, so grid auto-placement seats them on the SAME row, exactly
   as it already does for .col-label + .col-main. A top margin there would push
   the copy out of alignment with its own eyebrow rather than open a gap under
   a heading. */

/* A .lead closing a .prose — currently only the one that ends 01, The
   challenge. It wants more air than a paragraph break, and `1.6em` does give it
   more, but by accident and never by the same amount: `em` resolves against the
   lead's own clamped font size, not the body's, so the step is 1.75x the
   paragraph gap at 1600px and 1.19x at 390px. The emphasis quietly evaporates
   on the viewports that need it most.

   Pinned to the body's own rhythm instead — 2.56rem is what `1.6em` comes to at
   the 16px body size — so the step is a deliberate one-and-a-half beats
   everywhere rather than a side effect of the clamp. */
.prose > * + .lead { margin-top: calc(2.56rem * 1.5); }

.section-ink .prose { color: var(--d2); }
.section-ink .prose strong { color: var(--d1); }

/* Measure caps — long-form copy never runs the full grid width. */
.measure    { max-width: 62rem; }
.measure-lg { max-width: 78rem; }
.measure-sm { max-width: 40rem; }

/* --------------------------------------------------------------------------
   6. Header / navigation
   -------------------------------------------------------------------------- */

.header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  height: var(--header-h);
  display: flex;
  align-items: center;
  transition: background var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
  border-bottom: 1px solid transparent;
}

.header.is-stuck {
  background: rgba(255, 255, 255, 0.88);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
  backdrop-filter: saturate(180%) blur(14px);
  border-bottom-color: var(--rule);
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2.4rem;
  width: 100%;
  max-width: var(--max);
  margin-inline: auto;
  padding-inline: var(--outer);
}

.brand {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  color: var(--navy);
}

.brand svg { width: 3.2rem; height: auto; flex: none; }

.brand-name {
  font-size: 1.5rem;
  letter-spacing: -0.015em;
  color: var(--t1);
  white-space: nowrap;
}

@media only screen and (max-width: 359px) {
  .brand-name { font-size: 1.3rem; }
}

/* — Desktop nav — */

.nav-list {
  display: none;
  align-items: baseline;
  gap: 2.8rem;
}

@media only screen and (min-width: 900px) {
  .nav-list { display: flex; }
}

.nav-item {
  display: flex;
  align-items: baseline;
  gap: 0.7rem;
}

.nav-index {
  font-family: var(--font-mono);
  font-size: 1rem;
  letter-spacing: var(--ls-mono);
  color: var(--t3);
  transition: color var(--dur) var(--ease);
}

.nav-link {
  position: relative;
  font-size: var(--fs-small);
  letter-spacing: -0.01em;
  padding-block: 0.4rem;
}

.nav-link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur) var(--ease);
}

.nav-link:hover::after,
.nav-link[aria-current="page"]::after { transform: scaleX(1); }

.nav-item:hover .nav-index { color: var(--navy); }

/* — Burger — */

.burger {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 4rem;
  height: 4rem;
  align-items: flex-end;
}

@media only screen and (min-width: 900px) {
  .burger { display: none; }
}

.burger span {
  display: block;
  width: 2.2rem;
  height: 1px;
  background: var(--t1);
  transition: transform var(--dur) var(--ease),
              opacity var(--dur-fast) var(--ease);
}

.burger[aria-expanded="true"] span:first-child {
  transform: translateY(3px) rotate(45deg);
}

.burger[aria-expanded="true"] span:last-child {
  transform: translateY(-3px) rotate(-45deg);
}

/* — Mobile panel — */

.mobile-nav {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--header-h) var(--outer) 6rem;
  background: var(--surface-ink);
  color: var(--d1);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-slow) var(--ease),
              visibility var(--dur-slow) var(--ease);
}

.mobile-nav.is-open { opacity: 1; visibility: visible; }

.mobile-nav-title {
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--d3);
  padding-bottom: 1.6rem;
  border-bottom: 1px solid var(--rule-dark);
}

.mobile-nav-list { margin-top: 1.6rem; }

.mobile-nav-list li { border-bottom: 1px solid var(--rule-dark); }

.mobile-nav-list a {
  display: flex;
  align-items: baseline;
  gap: 1.4rem;
  padding: 1.8rem 0;
  font-size: 2.4rem;
  letter-spacing: -0.02em;
}

.mobile-nav-list .nav-index { color: var(--d3); font-size: 1.1rem; }

.mobile-nav-foot {
  margin-top: auto;
  padding-top: 3.2rem;
  color: var(--d3);
}

body.nav-open { overflow: hidden; }

/* --------------------------------------------------------------------------
   7. Sections
   -------------------------------------------------------------------------- */

.section {
  padding-block: 18vw;
  position: relative;
}

@media only screen and (min-width: 834px) {
  .section { padding-block: 8.4vw; }
}

.section-alt { background: var(--paper-alt); }

.section-ink {
  background: var(--surface-ink);
  color: var(--d1);
}

.section-ink .rule,
.section-ink .row { border-color: var(--rule-dark); }

.rule { border-top: 1px solid var(--rule); }

/* Section eyebrow — the mono annotation that sits alone in column 1 */
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  line-height: 1.4;
  color: var(--t3);
  margin-bottom: 3.2rem;
}

.section-ink .eyebrow { color: var(--d3); }

@media only screen and (min-width: 834px) {
  .eyebrow {
    margin-bottom: 0;
    position: sticky;
    top: calc(var(--header-h) + 3.2rem);
  }
}

.eyebrow-num { display: block; color: var(--navy); }
.section-ink .eyebrow-num { color: var(--d2); }

/* --------------------------------------------------------------------------
   8. Hero
   -------------------------------------------------------------------------- */

.hero {
  background: var(--paper-alt);
  color: var(--t1);
}

.hero-inner {
  grid-template-rows: auto 1fr auto;
  /* Not a full screen. The lockup used the top half and the rest was empty
     desk - 614px of 1080 at 1920, with nothing to say the page continued. At
     76svh the hero's own proportion puts the lockup's optical centre on the
     photograph's lens (41.4% against 42%, measured) and leaves 259px of the
     next section in view.

     Shared with about.html on purpose: its centred hero had a 278px dead band
     under the spectrum for the same reason - a 1fr centre track absorbing the
     slack of a full viewport. And it is a min-height, so a hero whose content
     is taller than this simply grows; it cannot clip. */
  min-height: 76svh;
  padding-top: calc(var(--header-h) + 14vw);
  padding-bottom: 8vw;
  row-gap: 4rem;
}

@media only screen and (min-width: 834px) {
  .hero-inner {
    /* 5vw -> 4vw. The air above the lockup has to come down with the height or
       the lockup sits low in a shorter frame: at 5vw the eyebrow's box top
       measured 26.0% of the hero, at 4vw it is 23.5%, against the comp's 21.4%
       of its own canvas. It also keeps 114px of clearance under the 79px
       header. */
    padding-top: calc(var(--header-h) + 4vw);
    padding-bottom: 3vw;
    row-gap: 0;
  }
}

.hero-eyebrow {
  grid-column: 1 / -1;
  grid-row: 1;
  align-self: start;
  text-align: center;
  color: var(--t3);
}

.hero-title-block {
  grid-column: 1 / -1;
  grid-row: 2;
  align-self: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-block: 6vw;
}

.hero-title {
  text-align: center;
  max-width: 18ch;
  text-wrap: balance;
}

.hero-title em {
  font-style: normal;
  color: var(--navy-1);
}

/* .hero-spectrum DELETED 2026-08-21 — the page's single power-map gradient, the
   3px multicolour hairline under each hero lockup. Removed on direction, from
   both pages: "Remove multicolour line. Do not use this element."

   The `--spectrum` TOKEN in §2 now has ZERO consumers — `grep "var(--spectrum)"`
   returns nothing. It is left defined deliberately, and this is the correction
   of an earlier claim in this very comment that §10b still used it: §10b's
   readout chip declares its OWN `linear-gradient(90deg, ...)` and only
   DESCRIBES itself as "the power-map spectrum's cool half, windowed". The two
   ramps are duplicated by hand, not shared through the token.

   So the token is the single place the brand's power-map ramp is written down
   as a ramp, and §10b's copy of it is the thing that would drift if this were
   deleted. If anyone unifies them, point the chip at the token rather than
   removing the token. */

.hero-foot {
  grid-column: 1 / -1;
  grid-row: 3;
  align-self: end;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3.2rem;
  text-align: center;
}

/* DELETED 2026-08-21, the deliberate dead-CSS pass — .hero-standfirst (already
   gone), .hero-aside, .hero-aside-label, .hero-specs, and .hero-foot's
   three-column desktop grid. All of them were kept alive only by about.html's
   hero, which was merged into #mission earlier the same day; the "Track record"
   chips they held were removed on direction. §38 existed solely to seat those
   chips in the right track and is deleted with them.

   .hero-foot itself STAYS: index.html still uses it, and §8b overrides it to a
   flex row on the content axis. Only the three-column grid above the breakpoint
   is gone, which about.html was the sole consumer of.

   NOT deleted, though the list this pass worked from said they were dead:
   .eyebrow, .eyebrow-num and .col-label. motion-test.html and snellen-test.html
   both load this stylesheet and both still use all three — the list had been
   compiled against index.html and about.html only. */

.hero-scroll {
  display: none;
  align-items: center;
  gap: 0.8rem;
  color: var(--t3);
}

@media only screen and (min-width: 834px) {
  .hero-scroll { display: flex; }
}

.hero-scroll svg {
  width: 1rem;
  animation: nudge 2.4s var(--ease) infinite;
}

@keyframes nudge {
  0%, 60%, 100% { transform: translateY(0); }
  30%           { transform: translateY(3px); }
}

@media (prefers-reduced-motion: reduce) {
  .hero-scroll svg { animation: none; }
}

/* --------------------------------------------------------------------------
   8b. Index hero — left-anchored ground for the stepped headline

   Scoped to .hero-left, which only index.html carries. about.html runs the
   centred composition off the same .hero-* classes and must not move.
   -------------------------------------------------------------------------- */

.hero-left .hero-eyebrow { text-align: left; }

.hero-left .hero-title-block {
  align-items: flex-start;
  /* The comp puts the lockup's left axis at 428.84 of 1920 = 22.3%. That is no
     longer "within 1%" of the grid - it is EXACT. The container is six tracks
     now (see 4) and --rail is 1.08fr precisely because 1.08 puts the content
     block's left edge on 428px of 1920. Measured: content block 428.1, lockup
     axis 428.1 at 1920; 301.5/301.5 at 1440; 268.0/268.0 at 1280. The old note
     about column 2 starting at 23.35vw described the five-column grid and is
     stale. Anything that moves this horizontally breaks the agreement. */
  /* THE PARAGRAPH ABOVE IS NO LONGER TRUE BELOW 1840px, and it is kept only as
     the record of what 1.08 was measured for. Read §2 before acting on it.

     Two things expired under it. (a) This whole section is scoped to
     `.hero-left`, and `.hero-left` was replaced by the Option B centred lockup
     (§44) on 2026-08-26 — it appears on neither page now, only inside HTML
     comments saying so. Nothing renders through these rules, so nothing reads
     the comp's axis. (b) Because of (a), --rail was free to be retuned, and on
     2026-08-27 it became a clamped LENGTH that ramps from 0px at 1024 to the old
     1.08fr value at --max, to take the 1024 cliff and the constant 20.9% margin
     out of the range that browser zoom lands on. The 428.1/428.1 agreement
     survives at 1920 and above, where the cap binds; at 1440 the content block's
     left edge is 232.5, not 301.5.

     So "anything that moves this horizontally breaks the agreement" was true of
     a hero that no longer exists. If the left-anchored lockup is ever revived,
     the agreement has to be re-established deliberately — either by pinning the
     rail again above some width, or by giving the lockup its own axis rather
     than borrowing the grid's. Do not assume it still holds.

     Note also that §8c's copy of this claim CANNOT be corrected in place:
     tools/build_hero_css.py regenerates §8c from images/HeroConcept.svg and its
     template carries the same sentence, so a re-run would put it straight back.
     Fix the generator, not the output. */
  grid-column: 2 / 6;
  /* The comp is a 1920x1080 canvas — a real viewport — and puts the eyebrow
     baseline 22.2% down it. So the lockup is anchored near the top rather than
     centred in the hero's middle track. */
  align-self: start;
  /* Halved from 6vw, then halved again with the hero's height. The headline is
     a four-tier block with its own internal rhythm; the old figure was one line
     and needed the surrounding air to hold the centre of the viewport. At 76svh
     there is no viewport centre left to hold. */
  padding-block: 2vw;
}

.hero-left .hero-foot {
  align-items: flex-start;
  text-align: left;
}

@media only screen and (min-width: 834px) {
  /* Only the scroll cue is left in this hero's foot, so the three-column grid
     about.html needs has nothing left to place. */
  .hero-left .hero-foot {
    display: flex;
    flex-direction: row;
    justify-content: flex-start;

    /* The foot inherits 1 / -1 from section 8, which used to mean the page edge
       and now means the annotation RAIL - so the cue landed at 132px while the
       lockup above it sits at 428px. On a left-anchored composition that is an
       artefact of the six-track grid, not a decision. about.html keeps 1 / -1
       deliberately: its hero is centred and its foot is a three-column grid
       pinned to the frame, symmetric about the viewport centre. */
    grid-column: 2 / 6;
  }
}

/* The rule now underscores a left-anchored block, so it needs less clearance
   than it did floating under a centred figure - but the clearance has to be a
   fraction of the TYPE, not of the viewport, or the lockup grows and the rule
   creeps up underneath it. 0.92 of --hl reproduces the tuned 44px at the comp's
   own 48px exactly, and follows --hl-scale from there. --hl is declared on
   .hero-title-block and inherits down. */
/* `.hero-left .hero-spectrum { margin-top: calc(var(--hl) * 0.92); }` was here —
   the comp's own clearance under the lockup, in em of the headline so it scaled
   with --hl. Deleted with the element on 2026-08-21; kept as a note because the
   0.92 is a measured value off images/HeroConcept.svg, not a chosen one, and it
   is the only record of it left if the rule ever comes back. */

/* --------------------------------------------------------------------------
   8c. The index headline, typeset from images/HeroConcept.svg

   Measured in Chromium off the comp's 1920 x 1080 canvas, then expressed in em
   of its 48px display size, so one variable (--hl) scales the whole lockup and
   the stagger stays proportional to the type rather than to the column.

     type      Nohemi SemiBold 48px      display lines        #111
               Space Grotesk 500 16px    eyebrow + outcome    #3f3f3f
               Impact 58px               the comma
               (the ampersand is an outlined path, not type — see below)

     x-offset  Clear 0.4702em   Comfortable 0   ampersand 0.4531em
               eyebrow 0.0271em   outcome 0.1856em
     leading   display 0.8847   outcome 1.375
     size      48/1920 = 2.5vw, capped at the comp's 48px, then x --hl-scale

   --hl-scale is the ONE knob. The comp's own size stays visible in the clamp so
   the file still says where 48px came from, and the departure from it is a
   single number rather than a set of retuned font sizes. Everything else in this
   section is in em, so the eyebrow, the three display lines, every per-line
   indent, the stepped outcome block and the ampersand all move by the same
   factor and the composition survives. Set it in the generator (SCALE), not
   here — see tools/hero-spec.md for how 1.25 was arrived at.

   The comp puts the lockup's left axis at x=428.84 of 1920 = 22.3%. The site's
   container is now SIX tracks — an annotation rail, four content columns, a rail
   — with --rail at 1.08fr chosen so that the content block's left edge lands on
   428px of 1920. So the lockup on grid-column 2/6 sits on the comp's own axis
   exactly, rather than within 1% of it — see 8b and section 4.

   THIS SECTION HAS NO CONSUMER as of 2026-08-26. index.html's hero is
   `.hero-centred` now (the Option B lockup, css/site.css §44) and everything
   below is scoped to `.hero-left`, so none of it applies to either page. It is
   regenerated and kept because it is the only written record of the comp's
   measurements — if the left-aligned lockup is ever wanted back, this is it.

   FONT LICENSING — settled, and no longer a launch risk:
     Instrument Sans  what actually ships. SIL OFL, SELF-HOSTED from
                      assets/fonts/, declared in css/site.css §0.
     IBM Plex Mono    ditto, for the mono labels and index numerals.
     Nohemi           COMMERCIAL (Pangram Pangram), never licensed for the web.
                      It could not ship, and what it did instead was fall back
                      silently to Space Grotesk — so the display face was an
                      accident, not a decision, and nobody noticed because both
                      faces are installed on the authoring machine.
     Space Grotesk    gone with it. It was only ever the fallback.
     Impact           gone. The comp's oversized comma went with the Option B
                      lockup, and Impact ships on Windows and macOS only. Anton
                      is the OFL analogue if that device is wanted back.
     (Cascadia Mono is no longer needed: the comp outlines the ampersand.)

   CAUTION if this section is ever revived: the ampersand vector below has
   fill="#111" baked into its path, and the per-line indents (.hl-1, .hl-3),
   .hero-outcome and .ho-* are NOT scoped to .hero-left despite the line below.
   §44 has to zero them explicitly for exactly that reason.

   Scoped to .hero-left throughout, so about.html's centred hero is untouched.
   -------------------------------------------------------------------------- */

.hero-left .hero-title-block {
  /* One knob for the whole lockup. 2.5vw hits the comp's 48px at 1920; the
     scale is the deliberate enlargement on top of it, and the only number in
     this section that is not the comp's own. */
  --hl-scale: 1.25;
  --hl: calc(clamp(2.6rem, 2.5vw, 4.8rem) * var(--hl-scale));
  width: 100%;
}

.hero-left .hero-eyebrow {
  margin-left: calc(var(--hl) * 0.0271);
  font-family: var(--font-ui);
  font-size: calc(var(--hl) * 0.3333);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--hero-grey);
  /* Baseline-to-baseline in the comp is 53px; the line boxes eat the rest. */
  margin-bottom: calc(var(--hl) * 0.28);
}

.hero-left .hero-title {
  font-family: var(--font-display);
  font-size: var(--hl);
  font-weight: 600;
  line-height: 0.8847;
  letter-spacing: 0;
  color: var(--hero-ink);
  /* Undo the shared centred hero. */
  text-align: left;
  max-width: none;
  text-wrap: nowrap;
}

.hl-line { display: block; }

.hl-1 { margin-left: 0.4702em; }
.hl-2 { margin-left: 0; }
.hl-3 { margin-left: 0.4531em; }

/* Impact, deliberately oversized against the Nohemi it punctuates. */
.hl-comma {
  font-family: var(--font-impact);
  font-size: 1.208em;
  font-weight: 400;
  line-height: 0;
}

/* The comp outlines the ampersand rather than setting it, so it is reproduced
   as the exact vector. A background image rather than an inline <svg>: an <svg>
   inside the H1 would make the reveal splitter skip the line and review mode
   treat the whole headline as uneditable. Colour is baked into the path, so it
   will not follow --hero-ink if that changes. */
.hl-3::before {
  content: "";
  display: inline-block;
  width: 0.5456em;
  height: 0.6817em;
  margin-right: 0.1167em;
  /* An inline-block contributes its MARGIN box to the line box, so a negative
     top margin equal to its height makes that contribution zero. Without this
     the ampersand — taller than the text's line box — pushes line 3's baseline
     down and the leading goes uneven. It still paints in full; it just stops
     participating in layout. */
  margin-top: -0.6817em;
  vertical-align: 0.1229em;
  background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%27-1529.41%20338.62%2026.19%2032.72%27%3E%3Cpath%20fill%3D%27%23111%27%20d%3D%27M-1520.31%2C371.34c-2.73%2C0-4.93-.74-6.6-2.23-1.67-1.49-2.5-3.46-2.5-5.93s.74-4.38%2C2.23-5.92c1.49-1.54%2C3.58-2.51%2C6.29-2.91l2.46%2C2.73c-2.06.4-3.65%2C1.12-4.77%2C2.14-1.12%2C1.02-1.68%2C2.27-1.68%2C3.73%2C0%2C1.27.47%2C2.3%2C1.4%2C3.08s2.17%2C1.18%2C3.71%2C1.18c2.96%2C0%2C5.24-1.17%2C6.86-3.52%2C1.61-2.35%2C2.42-5.67%2C2.42-9.97h4.3c0%2C5.6-1.23%2C9.94-3.7%2C13.01-2.46%2C3.07-5.94%2C4.6-10.42%2C4.6ZM-1509.1%2C370.89l-11.56-12.68c-1.94-2.14-3.34-4.05-4.19-5.75-.85-1.7-1.28-3.33-1.28-4.92%2C0-2.85.8-5.05%2C2.39-6.6%2C1.59-1.55%2C3.84-2.32%2C6.76-2.32l.09%2C4.12c-1.36%2C0-2.42.42-3.17%2C1.25-.75.84-1.13%2C2.03-1.13%2C3.59%2C0%2C1.09.29%2C2.16.86%2C3.2.58%2C1.05%2C1.36%2C2.14%2C2.36%2C3.29l14.75%2C16.81h-5.87ZM-1513.27%2C345.75c0-.97-.32-1.71-.95-2.23-.63-.52-1.53-.77-2.68-.77l-.09-4.12c2.7%2C0%2C4.8.62%2C6.29%2C1.86%2C1.49%2C1.24%2C2.23%2C3%2C2.23%2C5.27h-4.8Z%27%2F%3E%3C%2Fsvg%3E") no-repeat center / contain;
}

.hero-outcome {
  margin-left: calc(var(--hl) * 0.1856);
  margin-top: calc(var(--hl) * 0.16);
  font-family: var(--font-ui);
  font-size: calc(var(--hl) * 0.3333);
  font-weight: 500;
  line-height: 1.375;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--hero-grey);
}

/* The comp indents these with 0, 4 and 8 literal spaces. HTML collapses those,
   so they are reproduced as margins in em of the outcome's own size. */
.ho-line { display: block; }
.ho-1 { margin-left: 0; }
.ho-2 { margin-left: 1.12em; }
.ho-3 { margin-left: 2.24em; }

@media only screen and (min-width: 834px) {
  /* Sized so they never wrap; wrapping would break the staircase. */
  .ho-line { white-space: nowrap; }
}

/* Below the breakpoint the lockup would overflow, so let it wrap and pull the
   stagger in proportionally. */
@media only screen and (max-width: 833px) {
  .hero-left .hero-title { text-wrap: balance; }
  .hl-1, .hl-3 { margin-left: 0.24em; }
  .ho-2 { margin-left: 0.56em; }
  .ho-3 { margin-left: 1.12em; }
}

/* --------------------------------------------------------------------------
   9. Spec pills

   Reconstructed after being removed in error alongside the adoption map.
   Both pages use them: index.html for the platform envelope figures,
   about.html for the "Track record" chips.
   -------------------------------------------------------------------------- */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 0.8rem;
  padding: 0.6rem 1.4rem;
  border: 1px solid var(--rule);
  border-radius: var(--r-pill);
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--ls-mono);
  line-height: 1.4;
  text-transform: uppercase;
  color: var(--t2);
  white-space: nowrap;
}

.pill-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--navy);
  flex: none;
}

.section-ink .pill { border-color: var(--rule-dark); color: var(--d2); }
.section-ink .pill-dot { background: var(--d1); }

/* --------------------------------------------------------------------------
   10. Intro block (label left, large copy right)
   -------------------------------------------------------------------------- */

.intro-copy {
  font-size: var(--fs-lead);
  letter-spacing: var(--ls-lead);
  line-height: 1.26;
  max-width: 62rem;
}

.intro-copy > * + * { margin-top: 1.1em; }

/* The payoff line moved out of .intro-copy and below the readout — see
   .intro-payoff. Nothing else in .intro-copy carries .accent. */

/* .intro-sub DELETED 2026-08-21. index#platform's copy was re-tiered to
   .prose/.lead on direction — it had been 14px, i.e. SMALLER than the body it
   was meant to lead — and About's went with the mission merge. */

/* --------------------------------------------------------------------------
   10b. Snellen chart — the Introduction's right-hand column

   Eight rows of Snellen optotypes on a descending size ramp, pinned while the
   reader scrolls, resolving from 1.5 D of defocus to fully corrected.

   Optical model
   -------------
   For a planar chart at a fixed distance the retinal blur circle has ONE
   constant angular size across the whole chart — it is a property of the eye,
   not of the letter. So every row carries the identical absolute blur. What
   differs per row is the stroke width that blur has to destroy: row 6/N has a
   letter height of 5N/6 arcmin and a stroke of N/6. The top row's strokes are
   10 arcmin wide, the 6/6 row's are 1. One constant blur leaves the first soft
   but legible and erases the second — the differential legibility is emergent,
   not authored.

   The model itself lives in site.js, which writes --sigma. CSS only scales and
   applies it, so the render and the on-screen readout cannot drift apart.

   --arcmin  one arcmin expressed in px; the chart's entire scale
   --sigma   blur standard deviation, in arcmin. Defaults to 0, so with JS off
             or under reduced motion every row is legible. Nothing readable is
             withheld — the optotypes are random letters — so a reader who
             never scrolls loses no information.
   -------------------------------------------------------------------------- */

.intro-scope {
  position: relative;
  height: 220vh;              /* scroll runway for the resolve */
}

.intro-sticky {
  --p: 1;        /* scroll progress 0 -> 1; fails to "resolved" */
  --sigma: 0;
  --arcmin: clamp(1.8px, min(0.28vw, 0.38vh), 3.6px);

  position: sticky;
  top: 0;
  min-height: 100vh;
  display: grid;
  align-content: center;
  padding-block: 18vw;
  overflow: hidden;
}

.intro-text,
.intro-chart { grid-column: 1 / -1; }

.intro-chart { margin-top: 6.4rem; }

/* The payoff line lands on the last of the resolve, as in the Snellen test.
   Scoped to .intro-copy so other .accent uses are unaffected. Because --p
   defaults to 1 this is fully visible with JS off, and opacity never hides
   text from assistive technology — the line is always in the accessibility
   tree, only its arrival is timed. */
/* The payoff line. It now sits below the readout rather than inside
   .intro-copy, so the selector is scoped to its own class instead of the
   descendant it used to rely on. Still arrives on the last of the resolve.

   Because --p defaults to 1 this is fully visible with JS off, and opacity
   never hides text from assistive technology — the line is always in the
   accessibility tree, only its arrival is timed. */
.intro-payoff {
  display: block;
  font-size: var(--fs-lead);
  letter-spacing: var(--ls-lead);
  line-height: 1.26;
  /* x6 rather than the test page's x5.5 so --in reaches a clean 1 by p = 0.99
     instead of stopping at 0.99. */
  --in: clamp(0, (var(--p) - 0.82) * 6, 1);
  opacity: var(--in);
  transform: translateY(calc((1 - var(--in)) * 0.6rem));
}

.sn-chart { border-top: 1px solid var(--rule); }

.sn-row {
  display: grid;
  grid-template-columns: 6rem 1fr;
  align-items: baseline;
  gap: 0 1.6rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--rule);
}

/* The annotation layer stays sharp. Physically the printed ratios on a real
   chart would blur too, but these are our margin notes, not part of the scene
   being viewed — and blurring mono captions just reads as a bug. */
.sn-acuity {
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  line-height: 1;
  color: var(--t3);
  white-space: nowrap;
}

.sn-letters {
  /* font-size = cap height / 0.716 (Arial cap ratio), in arcmin x px */
  font-family: var(--font-sans);
  font-size: calc(var(--h) * 1.397 * var(--arcmin));
  font-weight: 600;
  line-height: 1;
  /* Snellen optotypes sit on a 5x5 grid, spaced one letter width apart. */
  letter-spacing: 0.62em;
  color: var(--t1);
  display: block;
  filter: blur(calc(var(--sigma) * var(--arcmin)));
}

/* True Snellen angular letter heights, in arcmin — a 12:1 spread. */
.sn-row:nth-child(1) .sn-letters { --h: 50;   }
.sn-row:nth-child(2) .sn-letters { --h: 30;   }
.sn-row:nth-child(3) .sn-letters { --h: 20;   }
.sn-row:nth-child(4) .sn-letters { --h: 15;   }
.sn-row:nth-child(5) .sn-letters { --h: 10;   }
.sn-row:nth-child(6) .sn-letters { --h: 7.5;  }
.sn-row:nth-child(7) .sn-letters { --h: 5;    }
.sn-row:nth-child(8) .sn-letters { --h: 4.17; }

/* The live optical readout. It sits in the left column under the body copy —
   it is the argument stated in numbers, and under the optotypes it was too
   easy to miss. It is set as a filled state chip rather than a caption: the
   fill is the thing that changes, so the readout stops being a label and
   becomes the instrument.

   The fill is the power-map spectrum's cool half, windowed. The full ramp runs
   Oxford Blue -1 -> Oxford Blue -2 -> Spring Green +2 -> Spring Green +1 ->
   Spring Green; the chip shows a fifth of it at a time (background-size 500%)
   and slides that window from the blue end to the green end as --p goes 0 -> 1.
   So the chip is near-flat at any instant but never quite flat, and the colour
   IS the correction state: deep Oxford Blue fully uncorrected, Spring Green
   corrected. --p is set on .intro-sticky by js/site.js and inherits down here;
   the 0 fallback keeps the uncorrected end on screen before the script runs.

   Sizing: 1.75x the mono scale (11px -> 19.25px) at 700. That clears the WCAG
   large-text threshold (18.66px bold), which is what makes the mid-ramp teals
   usable as a text background at all — see .sn-readout.is-lit below. */
.sn-readout {
  display: inline-block;
  margin-block: 4rem 3.2rem;
  padding: 0.8rem 1.4rem 0.7rem;

  font-family: var(--font-mono);
  font-size: calc(var(--fs-mono) * 1.75);
  font-weight: 700;
  letter-spacing: var(--ls-mono);
  line-height: 1.5;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;

  /* The label flips UNCORRECTED (19 chars) -> CORRECTED (16), which would snap
     the chip narrower at the moment of correction. Hold the wider of the two:
     19 monospace advances (1ch) plus their tracking, plus the side padding. */
  min-width: calc(19ch + 19 * 0.1em + 2.8rem);

  background-image: linear-gradient(90deg,
    #003399 0%, #006699 25%, #009999 50%, #33CC99 75%, #66FF99 100%);
  background-size: 500% 100%;
  background-position: calc(var(--p, 0) * 100%) 0;

  color: var(--white);
  /* Only the ink transitions. The fill is scrubbed every frame and a
     transition on background-position would just make it lag the chart. */
  transition: color 140ms linear;
}

/* Past roughly the halfway teal the fill is too light to carry white, so the
   ink flips to Oxford Blue in one step — a crossfade would pass through a
   mid-grey that reads worse on teal than either endpoint. js/site.js sets the
   class at the crossover. Worst case across the whole ramp is ~3.5:1, above
   the 3:1 large-text floor; the two resting states (deep blue / Spring Green)
   are 12.4:1 and 13.0:1. */
.sn-readout.is-lit { color: var(--navy); }

@media only screen and (min-width: 834px) {
  .intro-scope  { height: 280vh; }
  .intro-sticky { padding-block: 8.4vw; }
  .intro-text   { grid-column: 2 / 4; }
  .intro-chart  { grid-column: 4 / 6; margin-top: 0; }
}

/* Reduced motion: no runway, no pin, no scrub — the corrected state, which is
   the honest default. */
@media (prefers-reduced-motion: reduce) {
  .intro-scope  { height: auto; }
  .intro-sticky { position: static; min-height: 0; }
  .intro-payoff { opacity: 1; transform: none; }
}

/* --------------------------------------------------------------------------
   11. Statement (oversized single line)
   -------------------------------------------------------------------------- */

.statement {
  text-wrap: balance;
  max-width: 20ch;
}

.statement em { font-style: normal; color: var(--navy-1); }
.section-ink .statement em { color: var(--d2); }

/* --------------------------------------------------------------------------
   12. Hairline rows (Key outcomes / Strategic benefits)
   -------------------------------------------------------------------------- */

.rows { margin-top: 6vw; }

.row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.2rem;
  padding: 2.8rem 0;
  border-top: 1px solid var(--rule);
  transition: background var(--dur) var(--ease),
              padding-inline var(--dur) var(--ease);
}

.row:last-child { border-bottom: 1px solid var(--rule); }
.section-ink .row:last-child { border-bottom-color: var(--rule-dark); }

@media only screen and (min-width: 834px) {
  .row {
    grid-template-columns: 4rem minmax(0, 1.1fr) minmax(0, 1.6fr);
    gap: var(--gutter);
    align-items: start;
    padding: 3.2rem 0;
  }

  .row:hover {
    background: rgba(7, 1, 23, 0.03);
    padding-inline: 1.6rem;
  }

  .section-ink .row:hover { background: rgba(255, 255, 255, 0.04); }
}

.row-num {
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--ls-mono);
  color: var(--t3);
  padding-top: 0.5rem;
}

.section-ink .row-num { color: var(--d3); }

.row-title {
  font-size: var(--fs-title);
  letter-spacing: -0.02em;
  line-height: 1.2;
}

.row-body {
  color: var(--t2);
  font-size: var(--fs-small);
  line-height: 1.55;
  max-width: 54rem;
}

.section-ink .row-body { color: var(--d2); }

/* --------------------------------------------------------------------------
   13. DELETED 2026-08-21 — the Applications accordion

   The whole .acc-* set: .acc, .acc-item, .acc-head, .acc-num, .acc-title,
   .acc-sign, .acc-panel, .acc-body. The accordion was replaced on 2026-08-20 by
   the scroll-stepped pinned figure in §29/§33, and its markup and its click
   handler went with it that day; only the stylesheet was still carrying it.

   One thing worth keeping from it: .acc-num rendered BOLD, because a <button>
   inside an <h3> inherits the UA's h3 weight through `button { font: inherit }`.
   .row-title had the identical leak and was fixed in §31c. If a component ever
   puts a button inside a heading again, set the weight explicitly.

   js/editor-agent.js and js/review.js still NAME .acc-* in their selector
   lists. That is harmless — they query for elements that no longer exist and
   get nothing back — and tools/test_reveal.js asserts the absence of the
   markup, which still passes. The number is kept so existing references to §13
   in README.md and HANDOVER.md still resolve.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   14. Stat grid (proof points)
   -------------------------------------------------------------------------- */

.stats {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  margin-top: 6vw;
  border-top: 1px solid var(--rule);
  border-left: 1px solid var(--rule);
}

/* Five stats in two columns leaves an orphan; let the last one span the row
   so the grid closes cleanly. */
@media only screen and (max-width: 833px) {
  .stat:last-child:nth-child(odd) { grid-column: 1 / -1; }
}

@media only screen and (min-width: 834px) {
  .stats { grid-template-columns: repeat(5, 1fr); }
}

.section-ink .stats { border-color: var(--rule-dark); }

.stat {
  padding: 2.8rem 2rem 3.2rem;
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

.section-ink .stat { border-color: var(--rule-dark); }

.stat-value {
  font-size: clamp(3.2rem, 3.8vw, 5.2rem);
  font-weight: 400;
  letter-spacing: -0.035em;
  line-height: 1;
  color: var(--navy);
}

.section-ink .stat-value { color: var(--d1); }

.stat-label {
  margin-top: 1.2rem;
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--t2);
}

.section-ink .stat-label { color: var(--d3); }

/* --------------------------------------------------------------------------
   15. Expertise list
   -------------------------------------------------------------------------- */

.disc-list { margin-top: 4rem; border-top: 1px solid var(--rule); }
.section-ink .disc-list { border-color: var(--rule-dark); }

.disc-list li {
  display: flex;
  align-items: baseline;
  gap: 1.6rem;
  padding: 1.6rem 0;
  border-bottom: 1px solid var(--rule);
  font-size: var(--fs-body);
  letter-spacing: -0.01em;
}

.section-ink .disc-list li { border-color: var(--rule-dark); }

.disc-list .row-num { padding-top: 0; }

/* --------------------------------------------------------------------------
   16. DELETED 2026-08-21 — the partner-with-us checklist

   The whole `.tick-list` set. Its only instance was the "Partner with us to"
   lead-in and its six items in about.html's #contact, removed on direction the
   same day. Verified dead across all four pages that load this stylesheet,
   including motion-test.html and snellen-test.html, and verified with HTML
   comments STRIPPED — the surviving mentions of the name are all prose about
   its removal, which a plain grep reports as live markup.

   The 700px breakpoint here was the only one in the file that was not 834 —
   worth remembering as a loose end that is now closed rather than a value to
   reintroduce. The number is kept so existing §16 references still resolve.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   17. Contact
   -------------------------------------------------------------------------- */

.contact-email {
  display: inline-block;
  margin-top: 4rem;
  font-family: var(--font-mono);
  font-size: clamp(1.8rem, 2.6vw, 3.2rem);
  letter-spacing: 0.01em;
  text-transform: none;
  color: var(--d1);
  padding-bottom: 0.6rem;
  border-bottom: 1px solid var(--rule-dark);
  transition: color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
  word-break: break-word;
}

.contact-email:hover {
  color: var(--white);
  border-bottom-color: var(--white);
}

.section:not(.section-ink) .contact-email {
  color: var(--navy);
  border-bottom-color: var(--rule);
}

.section:not(.section-ink) .contact-email:hover { border-bottom-color: var(--navy); }

/* --------------------------------------------------------------------------
   18. Strapline band
   -------------------------------------------------------------------------- */

.strapline {
  background: var(--navy);
  color: var(--white);
  padding-block: 16vw;
}

@media only screen and (min-width: 834px) { .strapline { padding-block: 7.5vw; } }

.strapline-text {
  grid-column: 1 / -1;
  font-size: var(--fs-h3);
  font-weight: 400;
  letter-spacing: -0.025em;
  line-height: 1.24;
  max-width: 26ch;
  text-wrap: balance;
}

@media only screen and (min-width: 834px) { .strapline-text { grid-column: 2 / 6; } }

/* --------------------------------------------------------------------------
   19. Footer
   -------------------------------------------------------------------------- */

.footer {
  background: var(--surface-ink);
  color: var(--d1);
  padding-block: 12vw 4vw;
}

@media only screen and (min-width: 834px) { .footer { padding-block: 5vw 3vw; } }

.footer-brand { grid-column: 1 / -1; margin-bottom: 5.6rem; }

/* TWO tracks, not one, since 2026-08-27 — the brand gained its wordmark on
   direction ("the oxford optical logo can have the oxford optical text, as per
   the nav bar") and an 80px eye plus "Oxford Optical" does not fit one track.

   Measured, not estimated: at 1440 a single content track is 183.3px
   (1301.9px of padded width, less 172.6px of gutters, over 6.16fr). The eye is
   80px and the wordmark about 213px at the size set below, so the horizontal
   lockup needs ~304px. One track would have overflowed; shrinking the pair to
   fit would have left ~5px of slack, which is exactly the knife edge open item
   M is about — a string that needs 744px in a 744px block, where a different
   platform's font metrics silently wrap it.

   Two tracks give 401px at 1440, so the lockup sits in 304px of it with 24% to
   spare. .footer-contact moves to 4 / 6 to make room; see its own note. */
/* ONE track since 2026-08-27 (second change that day), because the footer
   gained link columns - see the block below. The note above is the record of
   why it was two; it no longer applies. The lockup is STACKED now, eye over
   wordmark, so its width is the wordmark alone rather than eye+gap+wordmark. */
/* TWO bands, and the reason is measured. Four columns of one track each only
   works once a track is wide enough for the wordmark: the lockup needs 158px,
   and a track is 231px at 1920, 218px at 1440, 211px at 1280 - but only 164px
   at 834, where the rails have collapsed. Six pixels of slack is the open-item-M
   knife edge, and `nowrap` means another platform's metrics would overflow
   rather than wrap.

   So 834-1024 gets a 2x2 of two-track columns (~347px each), and four across
   starts at 1024 where the rails open and the tracks widen. Four 164px columns
   was cramped for link lists regardless. */
@media only screen and (min-width: 834px) {
  .footer-brand { grid-column: 2 / 4; margin-bottom: 0; }
}

@media only screen and (min-width: 1024px) {
  .footer-brand { grid-column: 2 / 3; }
}

/* The footer mark is an inline <svg> now, not a raster - see the note in the
   markup. 8rem against the old 12rem because the eye was about 67% of the old
   lockup's width, so this keeps it the same size on the page. Colour comes from
   currentColor; the footer sits on --surface-ink, so it is set explicitly here
   rather than left to inherit whatever .footer happens to set. */
.footer-brand {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1.6rem;
  color: var(--d1);
}

.footer-brand svg {
  width: 8rem;
  height: auto;
  display: block;
  flex: none;
  color: var(--d1);
}

/* The wordmark, mirroring .brand-name in the header rather than inventing a
   second treatment: same face, same negative tracking, same nowrap. It is
   scaled to the footer's larger mark instead of the header's 32px one — 3.2rem
   against the eye's 8rem is a 2.5:1 ratio where the header runs 2.13:1, chosen
   a step smaller than exact proportion to keep the slack above.

   nowrap is load-bearing: "Oxford Optical" breaking after "Oxford" would read
   as the brand system's stacked lockup done badly, and the whole point of this
   change was the header's horizontal pairing. */
/* 2.4rem, DOWN from 3.2rem on 2026-08-27, and the number is measured rather
   than chosen. The lockup now sits in ONE content track: 217.9px at 1440 and
   211.2px at 1280. "Oxford Optical" runs about 6.66px of width per px of font
   size, so 3.2rem needed ~213px - which fits 1440 with 5px to spare and
   OVERFLOWS at 1280. That 5px is the open-item-M knife edge exactly. 2.4rem
   needs ~160px, so it clears the narrowest track by 24%.

   nowrap stays load-bearing: "Oxford Optical" breaking after "Oxford" would
   read as the brand's stacked lockup done badly. With the mark now above it
   rather than beside it, a wrap is what the extra width would buy. */
.footer-brand-name {
  font-size: 2.4rem;
  letter-spacing: -0.015em;
  color: var(--d1);
  white-space: nowrap;
}

/* DELETED 2026-08-21 on direction — .footer-tagline (the "OO-Adaptive Optics
   Platform" mono line under the logo), and the whole .footer-cols /
   .footer-col-title / .footer-col set: three columns of in-site links.

   "Remove all the links to places on the site. Remove the oo-adaptive optics
   platform text under the logo. Keep the email address in there for quick
   access to get in contact."

   So the footer is now three things: the mark, one mailto, and a two-item rail.
   The email is the only link left anywhere in it, which is why it gets a rule
   of its own below rather than inheriting a column's link styling. */

.footer-contact {
  grid-column: 1 / -1;
  margin-top: 4rem;
}

/* LAST track, 2026-08-27. It has been 3/6, then 4/6, now 5/6 - each move
   recorded because the reason changed each time, not the taste. The footer is
   four columns of one track now (brand, Explore, Company, Contact), so the
   email sits in the last one with the address under it.

   TOP-aligned now, not bottom. The old `align-self: end` existed because one
   line of text beside a 127px mark read as a stranded fragment. With three
   sibling columns of stacked content either side of it, the shared top edge is
   what makes them read as one row - so the reason for that rule expired with
   the composition it was written for. */
@media only screen and (min-width: 834px) {
  .footer-contact {
    grid-column: 4 / 6;
    margin-top: 4.8rem;
  }
}

@media only screen and (min-width: 1024px) {
  .footer-contact { grid-column: 5 / 6; margin-top: 0; }
}

/* Kept at --fs-body while the column links below sit at --fs-small: the email
   is still the footer's call to action, and one step of size is what marks it
   as such now that it is no longer the only link. Underlined for the same
   reason - the column links are not, so the underline distinguishes it rather
   than merely announcing that it is clickable. */
.footer-contact a {
  font-size: var(--fs-body);
  color: var(--d1);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.35em;
  transition: color var(--dur) var(--ease);
}

.footer-contact a:hover { color: var(--d2); }

/* The footer address, under the email in the last column. --fs-small and --d2:
   it is reference information, not a call to action, and the email above it has
   to stay the brighter thing in that column. */
.footer-address {
  margin: 1.6rem 0 0;
  font-size: var(--fs-small);
  line-height: 1.6;
  color: var(--d2);
}

/* --- the link columns, REVIVED 2026-08-27 -------------------------------
   These names - .footer-col, .footer-col-title - are deliberately the ones
   deleted on 2026-08-21, not new ones. README.md and HANDOVER.md both refer to
   the old set by name, and reusing them keeps those references resolving to a
   real component instead of a tombstone. The class set is the same; the layout
   is not (one track each, on the content grid, rather than a nested
   .footer-cols wrapper).

   WHY THEY ARE BACK, because this reverses a direction and that should not be
   silent: on 2026-08-21 the instruction was "remove all the links to places on
   the site". On 2026-08-27 the footer was reported as looking "very bare", four
   directions were drawn, and the columns option was chosen. It is also what the
   brand system asks for - `OO Stitch design.md` specifies footers with "link
   columns in 14-16px" and a mono rail - so the stripped footer had been the
   departure, and this is a return.

   It closes open item S as a side effect: the Company column carries the only
   links anywhere on the site to About's inner sections. */
.footer-col { grid-column: 1 / -1; margin-top: 4rem; }

@media only screen and (min-width: 834px) {
  .footer-col { margin-top: 0; }
  .footer-col-explore { grid-column: 4 / 6; }
  .footer-col-company { grid-column: 2 / 4; margin-top: 4.8rem; }
}

@media only screen and (min-width: 1024px) {
  .footer-col-explore { grid-column: 3 / 4; }
  .footer-col-company { grid-column: 4 / 5; margin-top: 0; }
}

/* Reuses .mono's role rather than re-declaring it: same face, 11px, 0.10em,
   uppercase. --d3 is the label tier and this is a label. */
.footer-col-title {
  margin: 0 0 1.6rem;
  color: var(--d3);
}

.footer-col ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* gap, not margins on the items: it survives a reorder or a deletion, which a
   per-item margin does not. */
.footer-col a {
  font-size: var(--fs-small);
  color: var(--d2);
  text-decoration: none;
  transition: color var(--dur) var(--ease);
}

.footer-col a:hover { color: var(--d1); }

.footer-rail {
  grid-column: 1 / -1;
  /* Overridden to the content block above the breakpoint — see below. */
  display: flex;
  flex-wrap: wrap;
  gap: 1.2rem 2.4rem;
  justify-content: space-between;
  /* 4.8rem, down from 8rem on 2026-08-27. The chosen footer direction was
     tightened on direction ("reduce some of the whitespace"), and measuring the
     draft showed the interval above the rail was carrying most of it - the rail
     had been bottom-pushed in a frame taller than its content, leaving 240px of
     void. 48px is the interval that reads as attached to the columns above
     without the rail crowding them. */
  margin-top: 4.8rem;
  padding-top: 2rem;
  border-top: 1px solid var(--rule-dark);
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--d3);
}

/* The statutory links live IN the rail rather than in a restored .footer-cols.
   The rail is already mono, uppercase, --fs-mono and quiet, which is exactly
   the register legal links want, and reviving the deleted column component to
   hold two links would have re-added a whole layout for them.

   Note this is NOT a reversal of the 2026-08-21 direction "remove all the links
   to places on the site" — that was about navigation. These are statutory, and
   were asked for separately on 2026-08-27.

   The rail's flex `justify-content: space-between` already puts the copyright
   left and this group right, so no new grid work is needed. */
.footer-legal {
  display: flex;
  flex-wrap: wrap;
  gap: 1.2rem 2.4rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* --d2, not the rail's own --d3. The rail sits at 4.9:1 on ink — AT the AA
   floor, which the token's own comment says — and that is a defensible place
   for a static copyright line but not for the only interactive controls in the
   footer. --d2 is 8.8:1 on ink. This is the same trap that has bitten this
   codebase five times (see the §5 traps list); here it is being avoided in
   advance rather than measured after the fact.

   Underlined for the same reason .footer-contact is: with almost no other links
   on the page there is no context left to signal that these are clickable. */
.footer-legal a,
.footer-legal button {
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: var(--d2);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.35em;
  transition: color var(--dur) var(--ease);
}

/* The button needs the reset a link does not: a UA button carries its own
   background, border, padding and font. `font: inherit` above is what stops
   .footer-legal button rendering in the platform UI face — the same leak that
   made .row-title bold and .acc-num inherit through `button { font: inherit }`
   is being closed deliberately here rather than discovered later. */
.footer-legal button {
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

.footer-legal a:hover,
.footer-legal button:hover { color: var(--d1); }

/* --------------------------------------------------------------------------
   20. Scroll reveal
   -------------------------------------------------------------------------- */

[data-reveal] {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 600ms var(--ease), transform 600ms var(--ease);
}

[data-reveal].is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  [data-reveal] { opacity: 1; transform: none; transition: none; }
}

/* No-JS safety net — never hide content if the script fails. */
.no-js [data-reveal] { opacity: 1; transform: none; }

/* --------------------------------------------------------------------------
   21. Section background image

   The brand has no photographic-ground component: every section until now has
   been one of the three flat grounds in §12. This adds the fourth case, and it
   exists because a photograph behind type is the one place the system can break
   its own contrast guarantees — so the rule that keeps it legible needs to live
   here, in the stylesheet, rather than being improvised per section.

   Usage: .section-photo on the section, with the image set as an inline
   background-image. The editor writes both; nothing else should.

     <section class="section section-ink section-photo"
              style="background-image:url('assets/photos/focus-01.jpg')">

   The URL is deliberately NOT passed through a custom property. A relative URL
   inside a custom property resolves against the stylesheet that declares it,
   not the document using it — so `assets/photos/x.jpg` written on an element in
   index.html was being fetched from /css/assets/photos/x.jpg and 404ing. An
   inline background-image always resolves against the document.

   .section-ink is not optional — it is what flips the text to the --d tier. The
   scrim is what makes that flip sufficient: over an unknown photograph, white
   text alone is not guaranteed AA, and a scrim of this weight holds the ink
   tokens at their stated ratios over any image we have tested. Lightening it is
   how this component silently stops being accessible.
   -------------------------------------------------------------------------- */

.section-photo {
  position: relative;
  /* background-image comes from the element's own style attribute — see above */
  background-size: cover;
  background-position: var(--photo-pos, 50% 50%);
  isolation: isolate;
}

.section-photo::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(
    180deg,
    rgba(7, 1, 23, var(--photo-scrim-top, 0.82)) 0%,
    rgba(7, 1, 23, var(--photo-scrim-bot, 0.68)) 100%);
}

/* The flat ground would otherwise paint over the photograph. */
.section-photo.section-ink,
.section-photo.section-alt { background-color: transparent; }

/* --d3 IS NOT SAFE OVER A PHOTOGRAPH.

   The token comment in §2 says it plainly: d3 is held "at the WCAG AA floor for
   normal text" — 4.9:1, and that is against FLAT ink. A photograph underneath
   moves the ground around, and measured over one it lands at 3.7:1 to 4.4:1.
   The scrim holds the mean; it cannot hold the bright patches, and d3 has no
   headroom left to absorb them.

   So the whole tier moves up inside a photographic section, by redefining the
   token rather than by listing its consumers — the eyebrow, the row numbers,
   the stat labels, the discipline marks and the figure captions all read it,
   and enumerating them is how one gets missed the next time a component lands
   on a photo. Measured by tools/check_section_photo.js.

   The cost is that d2 and d3 collapse to one value here, so the small-label
   tier stops being tonally distinct from body copy. That is the same trade the
   hero makes in §23 for the same reason, and it is the right way round:
   legible-but-flatter beats hierarchical-but-unreadable. */
.section-photo { --d3: rgba(255, 255, 255, 0.66); }

/* Parallax is tempting here and wrong: fixed attachment janks on every mobile
   browser and disables the scrim's compositing. Scroll the image with the page. */
.section-photo { background-attachment: scroll; }

/* --------------------------------------------------------------------------
   22. Section background video

   Layers a silent, looping film over §21's photographic ground. The two are
   used together, not as alternatives:

     <section class="section rule section-ink section-photo section-video"
              style="--photo:url('assets/video/platform-lens-poster.jpg')">
       <video class="section-video-media" …></video>
       <div class="container">…</div>
     </section>

   The poster is the section's own background image, so it is painted before the
   video has a single frame and stays underneath it for the whole page life.
   That is what makes the reduced-motion and no-autoplay cases free: hide the
   video and a properly composed still is already there, with the same scrim
   over it and the same text colours on top. There is no second code path to
   get wrong, and no flash of bare ink while the file loads.

   Paint order inside the section's own stacking context:
     the poster        the element's background-image   (§21)
     the video         z-index -2
     the scrim         z-index -1, ::before              (§21)
     the copy          auto
   -------------------------------------------------------------------------- */

.section-video { position: relative; isolation: isolate; }

.section-video-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -2;

  /* cover scales proportionally and crops the overflow — it can never stretch
     the picture. The master is cut to 2.4:1, which is close to this section's
     measured desktop proportions, so the trim is small until the layout goes
     portrait on a phone. The subject is centre-frame throughout the clip, which
     is why a centre position survives even that. */
  object-fit: cover;

  /* Nudged right of centre so the lens sits clear of the copy. NOTE, 2026-08-21:
     this was measured when the copy was `.intro-sub`, capped at 46rem / 736px.
     That component is deleted and #platform's copy is now `.measure-lg`, 78rem
     / 1248px - so the clearance this line describes is no longer the clearance
     that exists. This whole section is the retired video ground and matches
     nothing, but if it is ever revived, re-measure rather than trusting this. */
  object-position: 62% 50%;

  /* Decorative and behind the copy: it must never take a click or a caret. */
  pointer-events: none;
}

/* The scrim §21 inherits is flat, because a photograph behind a full-width
   block has to be held down everywhere. Here the copy stops well short of the
   right edge, and a flat scrim heavy enough to carry text was crushing the
   whole frame — the lab's teal came through as a muddy purple and the lens read
   as an anonymous dark shape rather than as a lens.

   So the weight follows the copy: heavy where the words are, light where they
   are not. The footage survives on the right, and the text keeps a ground that
   clears AA against --d2 regardless of what the film is doing underneath.

   Only above the breakpoint. Below it the copy runs the full width and there is
   no empty side to give away, so §21's flat scrim is still the right one. */
@media only screen and (min-width: 834px) {
  .section-video.section-photo::before {
    background: linear-gradient(
      95deg,
      rgba(7, 1, 23, 0.94) 0%,
      rgba(7, 1, 23, 0.88) 42%,
      rgba(7, 1, 23, 0.58) 68%,
      rgba(7, 1, 23, 0.44) 100%);
  }
}

/* An autoplaying background film is exactly what this preference is asking us
   not to do. The poster underneath is already the same frame, correctly cropped
   and already scrimmed, so removing the video leaves a finished section. */
@media (prefers-reduced-motion: reduce) {
  .section-video-media { display: none; }
}

/* --------------------------------------------------------------------------
   23. Hero photograph

   Lives here rather than beside the rest of the hero in §8 because
   tools/build_hero_css.py regenerates everything between the §8c and §9
   comment banners. A section written in that gap is deleted the next time the
   generator runs — which is exactly how §8d and §9 were lost once already.

   Why this hero does NOT reuse §21/§22
   ------------------------------------
   Those two lay an ink scrim over the photo and flip the copy to the --d tier,
   because they carry text over unknown footage. Here the photograph was chosen
   for this lockup: measured over the type's own footprint the tabletop already
   returns 4.1:1 to 7.0:1 against --hero-ink, so the comp's dark-on-light
   typesetting survives intact and an ink scrim would only throw away a picture
   that is lit from the front.

   What the picture is doing, in hero coordinates after the crop below. These
   are measured by tools/check_hero_photo.js, not estimated — it finds the lens
   by hue (oak always runs r > g > b; the coating breaks that ordering) and
   prints the box at every viewport:

     lens          x 47-88%   y 29-51%      the subject, kept clear
     lockup        x 25-40%   y 22-48%      beside it, on the same optical line
     back edge     the top ~11% of the frame, shadowed and out of focus

   So the two scrims are shaped to that, not applied flat:

     left      lifts the tabletop under the lockup. A flat scrim heavy enough
               for the type washed the wood grain out to a flat tan and cost
               the shot its only texture.
     top       the fixed header's brand and nav are --navy and --t1, and the
               shadowed back edge of the table puts them at 2.4:1. This is the
               band that fixes that; it stops by 26%, well above the lockup.

   Both are tuned against measurement, and the margins are genuinely thin — the
   third outcome line is the binding constraint at 4.7:1 against a 4.5 floor,
   because it is the most indented and so sits in the thinnest part of the ramp.
   Lightening either scrim, or stretching the left one's tail out towards the
   middle of the frame, is how this hero silently stops being accessible. Re-run
   the checker after any change to the picture or to these numbers.

   Together they read as one raking light from the top left, which is the
   direction the photograph is already lit from.
   -------------------------------------------------------------------------- */

.hero-photo {
  position: relative;
  /* Contains the negative inset on .hero-media, and keeps the media and scrim
     z-indexes from escaping into the page's own stacking order. */
  isolation: isolate;
  overflow: hidden;
  /* §8 sets `background: var(--paper-alt)`. It has to stay as the ground the
     mobile band sits on, but it must not paint over the photograph. */
  background-color: var(--paper-alt);
}

/* Warm, not neutral. --paper-alt is #EEEEEE and over oak it greys the wood out;
   this is sampled off the lit front edge of the tabletop, so the scrim reads as
   more of the same light rather than as a panel laid on top. */
.hero-photo { --hero-scrim: 244, 239, 233; }

/* THE COMP'S SECOND GREY DOES NOT SURVIVE THIS PHOTOGRAPH.

   §8c sets the eyebrow and the outcome lines in #3F3F3F — the lighter of the
   comp's two greys, and the thing that gives the lockup its tonal step. On this
   tabletop, measured, that grey lands at 3.4:1 to 4.4:1. Every one of those four
   lines failed AA.

   The ground cannot be raised to meet it. Getting #3F3F3F to 4.5:1 needs the
   left scrim at roughly 0.72, which stops being a scrim and becomes a panel —
   the wood goes to flat milky tan and the shot loses the only texture it has.
   Darkening the type costs a fraction as much: this is the darkest grey that
   still reads as a step below the #111 display lines, and it clears AA with
   margin (worst case 4.9:1) instead of failing.

   So the hierarchy is preserved but compressed — 17/38 rather than 17/63. If
   the photograph is ever replaced with a paler one, delete this and let §8c's
   own grey come back; tools/check_hero_photo.js will say whether it can. */
.hero-photo { --hero-grey: #262626; }

/* THE BAND — the default, and the only safe treatment on a tall viewport.

   `cover` on a portrait box crops horizontally, and the taller the box the
   harder it crops: a 1024x1366 hero enlarges this picture until the lens is
   1.5x the width of the headline and runs off the right edge. So full-bleed is
   the exception below, gated on proportion, and this is the fallback.

   As a band the photograph keeps its own aspect, the lockup keeps a clean paper
   ground above it, and the lens stays in frame instead of being blown up into
   an anonymous patch of oak. */
.hero-media {
  position: absolute;
  inset: auto 0 0 0;
  height: 48%;
  z-index: -2;
  display: block;
  /* Feathers the band's top edge so it meets the paper as light, not as a rule. */
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 14%);
          mask-image: linear-gradient(180deg, transparent 0, #000 14%);
  /* Decorative and behind the copy: it must never take a click or a caret. */
  pointer-events: none;
}

.hero-media img {
  width: 100%;
  height: 100%;
  /* cover scales proportionally and crops the overflow — it can never stretch
     the picture. */
  object-fit: cover;
  /* The band is far squarer than the picture, so `cover` still crops the sides.
     Nudged right of centre to hold the whole lens inside that window. */
  object-position: 58% 46%;
  display: block;
}

.hero-photo::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(var(--hero-scrim), 0.30) 0%,
    rgba(var(--hero-scrim), 0)   34%);
}

/* FULL BLEED — only where the viewport is wide AND broad enough to take it.

   Both conditions are load-bearing. The width test alone lets a portrait tablet
   through, which is the case that breaks; the proportion test alone lets a
   short, wide phone through, where the lockup would run across the lens. 13/10
   is where `cover` stops enlarging this 1.79:1 picture past the point the type
   can share a frame with it. */
@media only screen and (min-width: 834px) and (min-aspect-ratio: 13/10) {
  /* Overhangs the section top and bottom, so `cover` is given a box taller than
     the hero and the frame is trimmed at both ends: the shadowed back edge goes
     before it can reach the header, and enough empty foreground goes to bring
     the lens onto the lockup's own optical line rather than leaving it stranded
     in the top corner.

     Negative insets rather than a background-position, because at these
     proportions the picture is almost exactly as wide as `cover` needs it to
     be — there is no vertical overflow left for a position to move. Percentages
     on top/bottom resolve against the container's height, so the crop holds its
     proportion at every viewport instead of drifting with the aspect ratio. */
  .hero-media {
    /* Re-balanced with the hero's height, and both numbers are load-bearing.

       The SPLIT: -12/-20 put the lens's centre at 46.5% of the hero and the
       lockup's at 32.3% - 14 points apart, while this section's own comment
       claimed they were on one optical line. Moving 8 points of overhang from
       the bottom to the top raises the lens to 42% against the lockup's 41.4%,
       and takes the shadowed back edge of the desk fully out of frame instead
       of part way. Visible window is now picture y 18.8%-81.3%.

       The SUM matters as much as the split. At 76svh the old 1.32 sum brings
       the media box to 1.77:1, almost exactly the picture's own 1.79:1, so
       cover stops cropping - and the full frame drags the shelf edge and the
       far wall in behind the headline. That was rendered and rejected. 1.60
       keeps enough vertical overhang that cover still crops 18% off the sides
       and the lens stays at its shot size on the right of the frame.

       Measured at 1920x1080: lens x 55-76%, y 35-49%; upscale 1.40x, down from
       1.52x, which partly answers the resolution item in HANDOVER.md. */
    inset: -30% 0 -30% 0;
    height: auto;
    -webkit-mask-image: none;
            mask-image: none;
  }

  .hero-media img { object-position: 50% 50%; }

  .hero-photo::before {
    background:
      /* Lighter, because the crop above removed the thing it was fighting. It
         existed to hold the fixed header off the shadowed back edge of the
         desk, and at -30% that edge is out of frame - the ground under the
         header is lit tabletop now. Measured with it at 0.34 the header brand
         reads 10.60:1 mean / 10.15:1 worst against 10.29/10.07 before, so it is
         both lighter AND better. The reach stays at 26%: the header is 79px and
         the hero is shorter, so 26% is still 213px of cover. */
      linear-gradient(180deg,
        rgba(var(--hero-scrim), 0.34) 0%,
        rgba(var(--hero-scrim), 0.14) 12%,
        rgba(var(--hero-scrim), 0)    26%),
      /* The stops are the old ones scaled by 1.111, which is exactly how much
         the lockup grew (widest ink right edge 40.5% -> 45.0% of the frame), so
         every line sits at the same point in the ramp as before and the
         measured contrast carries over rather than having to be re-tuned by
         eye.

         The longest thing in the lockup is NOT the outcome lines, as the old
         comment here said - it is the EYEBROW, at 45.0%. The lens's leading
         edge measures 55.5%. So the ramp is spent inside that 10.5% and dies
         4.5 points clear of the subject; check_hero_photo.js's column-by-column
         probe reads 0.0/255 over the lens at every viewport. Do not stretch the
         tail out towards the middle of the frame - that is what put a
         measurable 13/255 wash on it once already. */
      linear-gradient(90deg,
        rgba(var(--hero-scrim), 0.58) 0%,
        rgba(var(--hero-scrim), 0.54) 27%,
        rgba(var(--hero-scrim), 0.46) 40%,
        rgba(var(--hero-scrim), 0.16) 45%,
        rgba(var(--hero-scrim), 0)    51%);
  }
}

/* The scroll cue is the one element the photograph breaks, and it breaks twice.

   In the band layout it sits ON the picture — the bottom-left of the frame is
   the shadowed near corner of the table, where --t3 measures 1.8:1. There is no
   colour that is safe there over a photograph that will be re-cropped, and the
   cue is already absent below 834px, so it is absent in the whole band family:
   it appears only where the lit corner exists to hold it.

   In the full-bleed layout it does have that corner, but --t3 is set at the AA
   floor for normal text *on white* and lands at 4.4:1 on oak. It moves to the
   hero's own grey — the one the eyebrow and the outcome lines are already set
   in, which the same measurement clears. */
.hero-photo .hero-scroll { display: none; }

@media only screen and (min-width: 834px) and (min-aspect-ratio: 13/10) {
  .hero-photo .hero-scroll { display: flex; color: var(--hero-grey); }
}

/* Forced-colours mode drops background images and the scrim with them, which
   would leave the lockup on whatever ground the UA picks. Drop the photograph
   too, rather than half of the treatment. */
@media (forced-colors: active) {
  .hero-media { display: none; }
}

/* --------------------------------------------------------------------------
   24. Split — a copy column beside a figure column

   Four of the design sketches ask for the same shape: a section's copy on one
   side, a single image or line figure on the other. So it is one component with
   a side modifier rather than four bespoke layouts.

   It spans .col-wide's tracks (2/6) and divides them in two, rather than
   placing copy and figure directly on the page's five columns. That keeps
   column 1 free as the annotation margin the rest of the site preserves, and it
   means the internal gutter is the grid's own --gutter at every width.

   DOM ORDER IS ALWAYS COPY THEN FIGURE, on every instance. Reading order,
   keyboard order and the no-CSS fallback therefore stay copy-first everywhere,
   which is what a screen reader and a stylesheet-less browser want; the picture
   is never the thing you have to wade through to reach the sentence.

   `.split-flip`, which used `order` to put the picture on the left above the
   breakpoint for About's hero, was DELETED 2026-08-21 — §35 turned that hero
   into a full background image instead, and the merge into #mission finished
   the job. If a picture-left split is ever wanted again, re-add the modifier
   rather than reordering the DOM: that is the whole point of the note above.
   -------------------------------------------------------------------------- */

.split {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  row-gap: clamp(3.2rem, 5vw, 6rem);
  align-items: start;
}

@media only screen and (min-width: 834px) {
  .split {
    grid-column: 2 / 6;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--gutter);
  }

}

/* The copy side carries no width cap of its own: it is already half of
   .col-wide, which is narrower than the 62rem .measure would give it. Adding
   .measure here as well would double-constrain it and strand the column. */
.split-copy > * + * { margin-top: 1.6em; }

.split-figure { min-width: 0; }

/* --------------------------------------------------------------------------
   25. Figure — the picture panel itself

   Two kinds, because the sketches ask for two kinds:

     .figure-photo   a photograph, cropped to an aspect and clipped to the
                     brand's 16px radius. The art direction sanctions radius-16
                     photo panels for section imagery; its prohibition on
                     rounded rectangles is specifically about the HERO, which
                     still carries no panel.
     .figure-line    a line drawing, which gets no radius, no crop and no
                     ground — it is ink on the section's own surface, and it
                     inherits `color` so one asset works on light and on ink.

   Aspect is a modifier rather than an inline style so the ratio lives with the
   design system and every instance of a given ratio stays identical.
   -------------------------------------------------------------------------- */

.figure { margin: 0; }

/* The radius and the clip live on the FIGURE, not on the image inside it.

   They were on the image, which worked while the image was the only thing in
   the box and never moved. It moved when the photographic panels were given
   `data-rv="photo"` — css/reveal.css's focus pull, which blurs 24px and scales
   1.06 on the way in. `filter: blur()` samples beyond the element's own box, and
   outside an <img> there is nothing, so a blurred image fades to transparent at
   all four edges and reads as a soft grey vignette inside a rounded panel. The
   6% scale exists to push those bled edges out of sight — and it can only do
   that if something is clipping them. Nothing was.

   So the figure owns the shape and the clip, the image fills it, and the reveal
   happens inside a crop that stays crisp for the whole 1400ms. */
.figure-photo {
  border-radius: var(--r-lg);
  overflow: hidden;
  /* A visible ground while the file loads, so the column does not flash. */
  background: var(--paper-alt);
}

.figure-photo img {
  display: block;
  width: 100%;
  height: auto;
  background: var(--paper-alt);
}

.section-ink .figure-photo,
.section-ink .figure-photo img { background: rgba(255, 255, 255, 0.06); }

/* Cropped ratios. `object-fit: cover` can only ever crop — it cannot stretch —
   so a supplied image of any proportion lands correctly. */
.figure-ar img { object-fit: cover; height: auto; }
.figure-2x3 img { aspect-ratio: 2 / 3; }
.figure-3x2 img { aspect-ratio: 3 / 2; }
/* .figure-4x5 deleted 2026-08-21 — no consumer on either page. */

.figure-line {
  color: var(--t1);
}

.section-ink .figure-line { color: var(--d1); }

.figure-line svg {
  display: block;
  width: 100%;
  height: auto;
}

/* The "what's next" figure holds two devices — AR glasses and a VR headset —
   drawn on one shared optical axis so that swapping them reads as one object
   changing rather than two objects trading places. The wireframe asks for a
   morph between them.

   A true path morph is not available: the two are arc-heavy and the
   correspondences are semantic, not geometric (two spectacle rims against one
   headset shell and an inset visor cannot interpolate cleanly). So it is a slow
   cross-fade — long enough to read as a transformation rather than a flicker,
   and the shared centre is what makes a fade legible as a morph at all.

   No JavaScript: the animation is the whole behaviour, so the figure works with
   scripting off. The .is-active class the asset ships is only consulted when
   the animation is switched off below. */
/* The vs-swap cross-fade that stood here is gone, with its keyframes and its
   reduced-motion override.

   It existed because the old figure held two devices - AR spectacles and a VR
   headset - on one shared axis, and the wireframe asked for a morph between
   them. The figure that replaced it holds one optical system: an adaptive
   element, sectioned, in front of a human eye. Its "dynamic" is drawn rather
   than animated, as the element's two extremes of travel in phantom line, so
   there is nothing left to cross-fade and nothing that needs switching off for
   prefers-reduced-motion. The arrival is still handled by data-rv="focus".

   Deleted rather than left in place: .vs-device matches nothing now, and a rule
   animating an absent element is a thing someone has to read and dismiss. */

.figure-caption {
  margin-top: 1.2rem;
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--t3);
}

.section-ink .figure-caption { color: var(--d3); }

/* --------------------------------------------------------------------------
   26. DELETED 2026-08-21 — the pull quote

   `.pullquote` lifted one line out of 02 The solution at display scale. The
   user directed "Remove: This simplifies the product experience." on
   2026-08-21, which was its only instance on either page. The idea survives as
   the title of /02 in the outcome rows; only the display-scale restatement of
   it is gone.

   Worth keeping from it: the component had no quotation marks and no panel,
   because a tinted box behind text is explicitly prohibited by the brand and
   the site separates with hairlines and nothing else. Any future pulled line
   should be built the same way. The number is kept so existing §26 references
   still resolve.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   27. DELETED 2026-08-21 — the facts strip

   The whole .facts / .fact / .fact-num / .fact-label set: five proof numbers as
   one full-width ruled row, built on 2026-08-20 from the original art
   direction's "at-a-glance strip" and removed from index#why on direction later
   the same day. §28's discipline grid, below, was written against this
   component's ruled-cell language and still uses it — that is why the reasoning
   is preserved here rather than thrown away with the rules.

   Two things worth keeping:

   - The two arrangements were mutually exclusive media ranges, not a mobile
     default that desktop overrode. `.fact:last-child` had to span the pair at
     the narrow end and must NOT at the wide end, and a min-width rule on the
     bare `.fact` selector loses that fight on specificity — the span survives
     into the five-column row and takes the fifth cell with it.
   - Dividers were per-cell borders, never a gap-and-background trick. The gap
     trick needs every cell to repaint the section's own ground, so it breaks
     silently the moment the strip lands on a different surface.

   NOTE, and it is not a CSS matter: "5M+ units" was one of these five figures
   and now appears NOWHERE on either page — see HANDOVER.md open item L. The
   claim was never withdrawn, it was lost to a layout change twice in two days.
   The number is kept so existing §27 references still resolve.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   28. Discipline grid — "our multidisciplinary team", reformatted

   Was a vertical list with /01../04 numerals. The sketch asks for it "formatted
   differently", as a row of marked items.

   It reuses §27's ruled-cell language deliberately, so the About page reads as
   the same system as the homepage rather than introducing a third way to divide
   a row. §27 itself was deleted on 2026-08-21 — the reasoning behind that
   language is preserved in its tombstone above, because this grid is now the
   only thing still speaking it.

   ON ICONS: the sketch labels the marker column "icons". The brand nominates
   Material Symbols but the reference visual language uses bare geometric marks,
   which is why every other list on this site carries a 5px dot or a mono
   numeral and no icon set is bundled. Introducing one is a brand decision, not
   a layout one, so the numeral stays as the marker and `.disc-mark` is the slot
   an icon drops into if that decision is taken. Nothing depends on the numeral.
   -------------------------------------------------------------------------- */

/* There are SEVEN disciplines, not a number that divides into four. So the
   block's outer edges are closed by the container and the internal rules are
   drawn per cell — rather than giving every cell a bottom border, which would
   run the last row's rule under three cells out of four and leave the lattice
   visibly ragged where the eighth cell is missing. */
.disc-grid {
  grid-column: 1 / -1;
  margin-top: clamp(3.2rem, 4vw, 5.6rem);
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  list-style: none;
}

.disc-item {
  padding: clamp(1.8rem, 2vw, 2.6rem) 0;
  font-size: var(--fs-small);
  line-height: 1.4;
  color: var(--t2);
}

/* Stacked: a rule between each pair, none above the first or below the last —
   the container already closes those. */
@media only screen and (max-width: 833px) {
  .disc-item + .disc-item { border-top: 1px solid var(--rule); }
}

@media only screen and (min-width: 834px) {
  .disc-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }

  .disc-item {
    padding: clamp(1.8rem, 2vw, 2.6rem) clamp(1.4rem, 1.6vw, 2.4rem);
    border-left: 1px solid var(--rule);
  }

  /* First cell of each row sits on the block's own left edge. Written as a
     4n+1 step rather than :first-child so it keeps working on the second row —
     with seven items, cell 5 starts a row and must not carry a divider. */
  .disc-item:nth-child(4n + 1) { border-left: 0; }

  /* Everything after the first row gets the horizontal rule. Stated as a
     range, so it holds whatever the item count grows to. */
  .disc-item:nth-child(n + 5) { border-top: 1px solid var(--rule); }
}

.disc-mark {
  display: block;
  margin-bottom: 1.1rem;
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--ls-mono);
  color: var(--t3);
}

.section-ink .disc-grid {
  border-top-color: var(--rule-dark);
  border-bottom-color: var(--rule-dark);
}

.section-ink .disc-item { color: var(--d2); }
.section-ink .disc-mark { color: var(--d3); }

@media only screen and (max-width: 833px) {
  .section-ink .disc-item + .disc-item { border-top-color: var(--rule-dark); }
}

@media only screen and (min-width: 834px) {
  .section-ink .disc-item { border-left-color: var(--rule-dark); }
  .section-ink .disc-item:nth-child(n + 5) { border-top-color: var(--rule-dark); }
}

/* --------------------------------------------------------------------------
   29. Applications — the scroll-stepped figure

   The sketch draws a head wearing eyewear, an arrow to the eyewear reading
   "app changes", an arrow to the head reading "head stays the same", and a
   vertical arrow marked "scroll 02 03 04". So: one figure that holds still
   while the device on it and the copy beside it step through the four
   application areas as the reader scrolls.

   Mechanically this is the Introduction's Snellen pin (§10b and js/site.js),
   reused rather than reinvented: a tall .appl-scope runway, a .appl-sticky
   pinned inside it, and one scroll handler turning progress through the runway
   into state. The difference is that the Snellen resolve is continuous and this
   is discrete — four steps — so the handler sets a class rather than a
   --progress custom property.

   Runway is 4 x 85vh. The Introduction already spends 220vh immediately after
   the hero, and the README flags that as a lot; this is deliberately shorter
   per step than a full viewport so four steps cost less than four screens.

   DEGRADATION. Both the no-JS case and prefers-reduced-motion drop the runway,
   unpin the figure, and un-stack the steps into a plain ordered list with every
   state visible. That is also exactly what the markup looks like with CSS off,
   so there is one fallback rather than three.

   The steps are stacked with grid-area rather than absolute positioning, so the
   container is sized by its tallest step and the pinned panel never changes
   height as the state changes.
   -------------------------------------------------------------------------- */

.appl-scope {
  position: relative;
  height: 340vh;
}

.appl-sticky {
  position: sticky;
  top: 0;
  min-height: 100svh;
  display: grid;
  align-content: center;
  overflow: hidden;
}

.appl-pair {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  row-gap: clamp(2.4rem, 4vw, 4rem);
  align-items: center;
}

@media only screen and (min-width: 834px) {
  .appl-pair {
    grid-column: 2 / 6;
    grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
    column-gap: var(--gutter);
  }
}

/* — The figure — */

/* `color` was the drawing's ink. The SVG was stroke="currentColor", so one asset
   worked on a light ground and on ink - the point of .figure-line in section 24. The
   states are PNGs now and inherit nothing from it, so this is dead. It is left in
   place, and named as dead, because the section is white today and the declaration
   would have to come back with any future ink treatment. */
.appl-figure { color: var(--t1); }

/* No `display` here, deliberately. The four states are shown one at a time by the
   pair of class rules below; a `display: block` on the element selector would be a
   lower-specificity rule permanently losing to them, which reads as a bug. */
.appl-figure img { width: 100%; height: auto; }

/* One device at a time - by OPACITY, not by `display`, and the difference is the
   reason the first switch to each state used to flash.

   `display: none` was the obvious way to write this and it is what the SVG did,
   where it cost nothing: the four states were <g> elements inside one already-
   painted document. As four <img> it behaves differently. A display:none image is
   never laid out and never painted, so the browser has the compressed bytes -
   loading="eager" saw to that - but no bitmap. Flipping it to display:block asks
   for the decode at the moment it must appear, and decoding="async" expressly
   allows presenting the frame before it lands. One blank frame, then the picture:
   a flash, on first view of each state only, because the bitmap is cached after.

   Stacking all four in one grid cell and swapping opacity keeps every state laid
   out and painted from the start, so all four are decoded long before anyone
   scrolls to them. js/site.js also calls decode() on them at startup; belt and
   braces, because "an opacity:0 layer is painted" is a reasonable expectation
   rather than a guarantee.

   justify/align-items: start is load-bearing. Grid items stretch by default, and
   an <img> with `width: auto` that gets stretched loses its aspect ratio - the
   same squash that section 33 has already been fixed for once. Keeping them at
   their natural size in the top-left of the cell also reproduces exactly how the
   block-level images sat before.

   No transition: the swap stays instant, in step with the tick and the copy. A
   cross-fade would only need a duration here now, which is the one thing this
   arrangement makes easy that `display` never could. */
.appl-figure { display: grid; justify-items: start; align-items: start; }
.appl-figure .af-eyewear { grid-area: 1 / 1; opacity: 0; }
.appl-figure .af-eyewear.is-active { opacity: 1; }

/* — The stepped copy — */

.appl-steps {
  display: grid;
  /* No explicit rows: every step occupies the same single implicit cell. */
}

.appl-step {
  grid-area: 1 / 1;
  opacity: 0;
  visibility: hidden;
  transform: translateY(0.8rem);
  transition: opacity var(--dur-slow) var(--ease),
              transform var(--dur-slow) var(--ease),
              visibility var(--dur-slow) var(--ease);
}

.appl-step.is-active {
  opacity: 1;
  visibility: visible;
  transform: none;
}

.appl-step-num {
  display: block;
  margin-bottom: 1.4rem;
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--ls-mono);
  color: var(--t3);
}

.appl-step-title {
  font-size: var(--fs-h3);
  font-weight: 400;
  line-height: 1.14;
  letter-spacing: var(--ls-h2);
  color: var(--t1);
}

.appl-step-body {
  margin-top: 1.6rem;
  max-width: 46rem;
  color: var(--t2);
}

/* — The scroll indicator: four ticks, the active one filled — */

/* Placed on the same tracks as .appl-pair, not left to auto-placement. As a
   bare grid child of .container it started at column 1 — the annotation margin
   the rest of the site keeps empty — which put the ticks 297px to the left of
   the figure they belong to. */
.appl-ticks {
  grid-column: 1 / -1;
  display: flex;
  gap: 0.8rem;
  margin-top: clamp(2.4rem, 3vw, 3.6rem);
  list-style: none;
}

@media only screen and (min-width: 834px) {
  .appl-ticks { grid-column: 2 / 6; }
}

.appl-tick {
  width: 2.4rem;
  height: 2px;
  background: var(--rule);
  transition: background var(--dur) var(--ease);
}

.appl-tick.is-active { background: var(--navy); }

/* — Degradation: no JS, or the reader asked for less motion — */

.no-js .appl-scope,
.no-js .appl-sticky { height: auto; min-height: 0; position: static; }

.no-js .appl-steps { row-gap: clamp(3.2rem, 5vw, 6rem); }

.no-js .appl-step {
  grid-area: auto;
  opacity: 1;
  visibility: visible;
  transform: none;
}

.no-js .appl-ticks { display: none; }

/* NOT :first-of-type. The eyewear groups are <g>, and so is the head group that
   precedes them, so :first-of-type selects the head and the figure loses its
   glasses. The markup instead ships state 01 already carrying .is-active, which
   the rule above turns into the visible one with no extra selector — and that
   same class is what the scroll handler then moves. */

@media (prefers-reduced-motion: reduce) {
  .appl-scope { height: auto; }
  .appl-sticky { position: static; min-height: 0; }
  .appl-steps { row-gap: clamp(3.2rem, 5vw, 6rem); }

  .appl-step {
    grid-area: auto;
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: none;
  }

  .appl-ticks { display: none; }

  /* The figure stays on state 01 rather than showing all four devices stacked
     on one head, which is what revealing them together would actually look
     like. It becomes a static illustration beside a plain list of the four
     areas — honest, and legible. */
}

/* --------------------------------------------------------------------------
   30. Section background image — the PAPER case

   §21 is the ink case: an unknown photograph, a flat ink scrim over all of it,
   and the copy flipped to the --d tier. That is the right treatment for a
   picture with real mid-tones, and it is the wrong one for the shots this site
   actually has. Four of the seven assigned photographs are near-white
   cleanroom frames — a lens in a gloved hand, a lens edge at macro, a bench in
   a bright room — and §21's scrim renders every one of them as the same muddy
   grey field. Measured on sec-solution.jpg: mean luminance 214/255 before the
   scrim, 47/255 after. The subject survives; the photograph does not.

   So this is the fourth ground, and it keeps the hero's logic (§23) rather
   than inventing a new one: the picture stays bright, the type stays dark, and
   the scrim is SHAPED around the copy instead of covering the frame. Paper is
   laid over the side the words are on and dies before it reaches the subject.

   Usage — .section-photo-paper INSTEAD of .section-ink, never alongside it:

     <section class="section section-photo section-photo-paper"
              style="background-image:url('assets/photos/sec-why.jpg')">

   and one of the two side modifiers, which says where the COPY is, not where
   the picture's subject is:

     (default)              copy on the left  — scrim ramps left to right
     .section-photo-right   copy on the right — scrim ramps right to left

   The side is a layout fact, so it is a class and not a guess: get it wrong
   and the scrim sits over the empty half while the type sits on bare
   photograph. tools/check_section_photo.js measures which is which.
   -------------------------------------------------------------------------- */

.section-photo-paper {
  /* THE SCRIM'S STRENGTH, AS FOUR NUMBERS INSTEAD OF THREE HARD-CODED GRADIENTS.

     It was written as literal stops, three times over — once flat for mobile,
     once as a 95deg ramp, once as a 265deg ramp — and then a fourth time as a
     radial for the corner case. Four sections consume it and they have nothing
     in common photographically: two are near-white frames with no subject in
     them, one has a sharp machine in the middle of the copy, one has a press in
     hard focus on the left. One set of stops cannot be right for all four, and
     when the whole thing had to come down on direction there was no single place
     to turn it down.

     So the weight is tokens and the SHAPE is the gradient. Tune a number.

       --scrim-max   under the copy, where the type needs a ground
       --scrim-mid   the ramp's shoulder
       --scrim-min   over the subject, where the photograph should win
       --scrim-win   the corner variant's clear window, at its centre

     WHY THEY CAME DOWN SO FAR. The old plateau was 0.95. Nobody had ever
     measured the RAW photograph — every number in this section was measured
     through a scrim that was already there — and with it disabled the frames
     turn out to be much brighter than the scrim was built for. Under-text 5th
     percentiles, scrim off, at 1920:

       #platform   169-173/255   nothing dark in the frame at all
       #mission    177-185/255   likewise
       #why         75-168/255   dark only behind the headline
       #solution    38-200/255   the sharp assembly sits under the row bodies

     So #platform and #mission never needed a scrim for legibility; they were
     being washed out for nothing, which is exactly what was reported. #solution
     is the only one that genuinely needs weight, and only in the band its
     outcome rows occupy. Per-section overrides are below each section's own
     rule.

     The ink moved up to pay for it — see the note under this one. */
  --scrim-max: 0.58;
  --scrim-mid: 0.44;
  --scrim-min: 0.18;
  --scrim-win: 0.04;

  /* --t2 AND --t3 ARE NOT SAFE OVER A PHOTOGRAPH — the mirror of §21's note
     about --d2/--d3, and it bites for the identical reason. The §2 token
     comments say it outright: t3 is held "at the WCAG AA floor for normal
     text", 4.8:1, and that is against FLAT white. Over a picture the ground
     moves, and measured over these it landed at 2.99:1 to 4.39:1 — the 11px
     row numbers on 02 and every fact label on 04. The scrim holds the mean;
     it cannot hold the dark patches, and t3 has no headroom left to absorb
     them.

     So the tier moves, by redefining the token rather than by listing its
     consumers. §21's comment is the reason: the eyebrow, the row numbers, the
     fact labels, the figure captions and the mono labels all read this, and
     enumerating them is how the next component to land on a photograph gets
     missed. This is the second time that has been true, and it was written
     down the first time.

     0.74 for both, which collapses them to one value — the same trade §21
     makes, in the same direction and for the same reason. The cost is that the
     small-label tier stops being tonally distinct from body copy; it is still
     distinct by size and by case, which is how this site actually separates
     them. Legible-but-flatter beats hierarchical-but-unreadable.

     --d3 is mapped onto the same value so that any .section-ink rule reached by
     a component sitting in here resolves to the paper tier and not to white. */
  --t2: rgba(7, 1, 23, 0.88);
  --t3: rgba(7, 1, 23, 0.88);
  --d3: rgba(7, 1, 23, 0.88);

  /* 0.88, up from 0.74, and this is what BUYS the lighter scrim rather than
     being a separate decision. Contrast is a relationship: it can be paid for
     from either side, and 26% of the ink's own alpha was being left on the table
     while the ground was flattened to compensate. Measured on #solution's
     14px row bodies over the darkest patch of the assembly (raw 38/255): at
     0.74 alpha the ground has to reach 0.95 to clear AA; at 0.88 it clears at
     0.44. That is the whole reduction, in one token.

     The cost is the same one §21 and §30 already accepted twice: t2 and t3 sit
     so close to t1 here that the small-label tier stops being tonally distinct
     from body copy. It stays distinct by size and by case, which is how this
     site actually separates them. Legible-but-flatter beats
     hierarchical-but-unreadable, and this time it also means the photograph
     survives. */
  color: var(--t1);
  background-color: var(--paper);
}

/* Cancel §21's flat ink scrim outright rather than trying to lighten it: a
   0.82 ink layer cannot be tinted into a paper one.

   This is the BELOW-breakpoint scrim. It stays the heaviest of the four shapes,
   because below the breakpoint the copy runs the full width and there is no
   empty side for a shaped scrim to retreat into — a phone gets the picture as a
   texture behind the words, not as a picture you look at, so there is nothing
   on offer to give back.

   It reads the same tokens as the rest, so a section that turns its scrim down
   turns it down here too, in proportion. */
.section-photo-paper::before {
  background: linear-gradient(
    180deg,
    rgba(255, 255, 255, calc(var(--scrim-max) + 0.16)) 0%,
    rgba(255, 255, 255, calc(var(--scrim-mid) + 0.16)) 100%);
}

/* Above the breakpoint the copy occupies one half and the picture's subject the
   other, so the scrim can follow the copy and let the photograph go.

   The stops are the hero's, re-measured for this ground: opaque enough at the
   text edge to carry --t2 body copy at 16px, and fully clear by 72% — which is
   where every one of these crops puts its subject (see tools/build_photos.py,
   which is what guarantees that 72% means the same thing on all of them).

   95deg rather than 90deg for the same reason §22 uses it: a hair of tilt keeps
   the scrim's edge off the vertical, so it reads as light falling across the
   frame rather than as a panel laid on top of it. */
@media only screen and (min-width: 834px) {
  .section-photo.section-photo-paper::before {
    background: linear-gradient(
      95deg,
      rgba(255, 255, 255, var(--scrim-max)) 0%,
      rgba(255, 255, 255, var(--scrim-max)) 38%,
      rgba(255, 255, 255, var(--scrim-mid)) 62%,
      rgba(255, 255, 255, var(--scrim-min)) 100%);
  }

  /* Stops pulled left of the left-hand version's mirror image, because the
     copy they protect is wider: the right-hand column starts at 38% of the
     section, not 51%, so a plateau ending at 38% would leave the first
     character of every line on bare photograph. */
  .section-photo.section-photo-paper.section-photo-right::before {
    background: linear-gradient(
      265deg,
      rgba(255, 255, 255, var(--scrim-max)) 0%,
      rgba(255, 255, 255, var(--scrim-max)) 52%,
      rgba(255, 255, 255, var(--scrim-mid)) 72%,
      rgba(255, 255, 255, var(--scrim-min)) 100%);
  }

  /* THE THIRD SHAPE, and the one the other two cannot cover.

     A side scrim works because the copy is on one side and the picture on the
     other. 02 The solution has no empty side: the headline, the paragraph and
     the pull quote are on the left, and then the four outcome rows run the FULL
     grid width — a row number, a title and a body, out to the right edge. A
     left-to-right ramp leaves the row bodies sitting on bare photograph, and
     measured, that is exactly what happened: 14px --t2 body copy at 2.36:1 over
     the sharp assembly in sec-solution.jpg. It read as fine at the mean, 5.36:1,
     and failed on the dark patches the mean hides. The same lesson as the hero,
     one component further on.

     What that section DOES have is an empty corner — top right, between the
     headline's 20ch measure and the first row's rule. So the clear window is a
     corner rather than a side: an ellipse anchored at 100% 0%, transparent at
     its centre and at full weight by the time it reaches any text. Everything
     below and left of it is protected at the same weight as a flat scrim.

     The stops are tuned against the rendered section, not chosen: 34% is where
     the pull quote's right end begins, 62% is where the first row's body starts.
     Re-measure with tools/check_section_photo.js after any change to this
     section's copy — the window is cut to the copy's shape and the copy moving
     is what invalidates it. */
  .section-photo.section-photo-paper.section-photo-corner::before {
    background: radial-gradient(
      116% 78% at 100% -2%,
      rgba(255, 255, 255, var(--scrim-win)) 0%,
      rgba(255, 255, 255, var(--scrim-min)) 30%,
      rgba(255, 255, 255, var(--scrim-mid)) 52%,
      rgba(255, 255, 255, var(--scrim-max)) 70%,
      rgba(255, 255, 255, var(--scrim-max)) 100%);
  }
}

/* Every component that §21 flips to the --d tier has to stay on the --t tier
   here. Listed rather than tokenised because these are `.section-ink`
   descendant rules being undone, and a token cannot undo a selector. */
.section-photo-paper .prose { color: var(--t2); }
.section-photo-paper .prose strong,
.section-photo-paper .statement,
.section-photo-paper .row-title,
.section-photo-paper .appl-step-title { color: var(--t1); }
.section-photo-paper .statement em { color: var(--navy-1); }
.section-photo-paper .row-num,
.section-photo-paper .eyebrow,
.section-photo-paper .figure-caption,
.section-photo-paper .mono { color: var(--t3); }
.section-photo-paper .row-body { color: var(--t2); }
.section-photo-paper .row,
.section-photo-paper .row:last-child,
.section-photo-paper .rule { border-color: var(--rule); }
.section-photo-paper .row:hover { background: rgba(7, 1, 23, 0.03); }

/* The copy column when it sits on the right. .col-main and .col-wide both start
   at column 2 by design — the annotation margin — and that is the wrong axis for
   a right-hand column, which has to end flush with the grid's right edge
   instead. */
@media only screen and (min-width: 834px) {
  /* Three of the four content tracks, not two. Two was right when a track was
     294px wide; on the re-centred grid (§4) a track is 231px and a two-track
     column is 509px, which put "A strategic partner for growth" on three lines
     and gave the body copy a 62-character measure. Three tracks is 764px — the
     same measure the left-hand columns get, so a section reading right-to-left
     is not also a section set narrower. */
  .section-photo-right > .container > .col-main { grid-column: 3 / 6; }
  .section-photo-right > .container > .col-wide { grid-column: 2 / 6; }
  /* A `.facts` rule stood here. Deleted 2026-08-21 with the component (§27). */

  /* A right-hand column needs a right AXIS, and a measure cap alone does not
     give it one — it gives it a left axis and a ragged gap. Measured on About's
     mission: a 46rem block in a 764px column left 304px of blank between the
     last word and the edge of the page, on every line, which reads as the copy
     having drifted rather than as a margin.

     So the blocks are flushed right and capped. Both are needed: without the
     cap the body copy runs 95 characters, and the craft floor for a measure is
     65-75; without the flush the cap is what creates the gap.

     54rem is the widest of the three caps the site already uses (--measure is
     62rem for 16px prose, 46rem for the 14px tiers) held to about 68 characters
     at the sizes that land in these columns. `margin-inline-start` rather than
     `margin-left`, since the rest of the file is logical-property clean. */
  .section-photo-right > .container > .col-main > * {
    max-width: 54rem;
    margin-inline-start: auto;
  }
}

/* --------------------------------------------------------------------------
   31. Alignment corrections from the 2026-08-20 design review

   Three blocks were sitting off the axis their own section is drawn on. Each is
   a one-property fix, and each was invisible in the source because the property
   that was wrong is the one that was never written.
   -------------------------------------------------------------------------- */

/* (a) The facts strip ran the FULL grid — column 1 to -1 — so it started 340px
   left of the headline above it and of the copy beside it. The wireframe draws
   it starting on the same vertical as both, and ending under the right edge of
   the picture. That is columns 2-6, which is what every other full-width block
   in a section uses. Below the breakpoint nothing changes: everything is
   full-bleed there. */
/* The `.facts` rule this block existed for went with the component on
   2026-08-21 (§27). Kept as a note because the REASONING above is about the
   six-track grid, not about the strip: any full-width block in a section
   belongs on columns 2-6, and the next one to land here will need it. */

/* (b) The split's two columns were top-aligned only, so a four-paragraph copy
   column beside a 2:3 portrait left 226px of dead space under the words while
   the picture ran on. Both wireframes that use this shape draw a double-headed
   arrow between the bottom of the copy and the bottom of the picture: align
   them.

   The copy is the element whose height is not ours to choose, so the copy has
   to set the row and the picture has to fill whatever that comes to.

   `align-items: stretch` alone does NOT do that, which is the trap here — it
   was tried first and the bottoms stayed 223px apart. Stretch sizes each item
   to the ROW, but the row is still the max-content of both items, and a
   1600x2400 image at `width: 100%` contributes 625px of content height. The
   picture went on winning; all stretch did was make the copy's invisible box
   as tall as it.

   So the image is taken out of flow. With the figure holding nothing in-flow
   its content height is zero, the row is the copy's alone, the stretched figure
   takes that height, and the absolutely-positioned image fills it. `cover` can
   only crop, so no ratio is distorted whatever height the copy comes to.

   min-height is the guard on the one bad case: a section whose copy is short
   would otherwise reduce the picture to a letterbox strip. Below that floor the
   picture wins and the bottoms part company, which is the right way to fail. */
@media only screen and (min-width: 834px) {
  .split-bottom { align-items: stretch; }

  /* No radius or overflow here — §25's .figure-photo owns both now, for every
     photo panel rather than only the bottom-aligned ones. This adds only what
     bottom-alignment itself needs. */
  .split-bottom > .split-figure {
    position: relative;
    min-height: 32rem;
  }

  .split-bottom > .split-figure img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    aspect-ratio: auto;
    object-fit: cover;
  }
}

/* (c) `.row-title` rendered BOLD on a site whose whole premise is one weight.
   §12 sets no font-weight and the class is only ever used on an <h3>, so the UA
   default applied — 10 instances on index, 4 on about. The site's own headings
   all declare 400 explicitly; this one was the omission, not the exception. */
.row-title { font-weight: 400; }

/* --------------------------------------------------------------------------
   32. Row lead-in — the sentence that runs into the outcome rows

   The wireframe for 02 The solution replaces the generic "Key outcomes" label
   with a sentence that the rows complete: "Embedding our OO-Adaptive Optics
   Platform into devices unlocks" then "/01 Expand the addressable market".

   It was built with `.mono` because that is what the label it replaced used.
   That is the mistake: at 11px, uppercase, with 0.10em tracking and no measure
   cap, a fifty-eight-character sentence sets as one 1315px line of small caps
   and reads as a legal disclaimer. A label can be small because nobody has to
   read it in sequence; this one has to be read as a sentence, and it is the
   sentence four headings hang off — so it is heading text and it is set as
   heading text.

   Sized at --fs-lead rather than --fs-h3 deliberately: it must be clearly
   subordinate to the section's own headline while still reading as a heading,
   and the four .row-title lines below it sit at --fs-title. That puts it in
   the gap between them, which is where it belongs in the hierarchy.
   -------------------------------------------------------------------------- */

.row-lead {
  margin-top: clamp(4.8rem, 6vw, 8rem);
  font-size: var(--fs-lead);
  font-weight: 400;
  line-height: 1.24;
  letter-spacing: var(--ls-lead);
  color: var(--t1);
  /* Two lines at the desktop widths, three at the narrow end. Long enough to
     read as prose, short enough that the eye finds the /01 beneath it. */
  max-width: 34ch;
  text-wrap: balance;
}

.section-ink .row-lead { color: var(--d1); }

/* .accent is the one emphasis colour on the site — Oxford Blue on a paper
   ground — and it had never met an ink one. About's "What's next" gained a
   photographic ink ground on 2026-08-20 and its accent line measured 1.21:1:
   #0A1A4A against #070117 is two dark blues, and no scrim helps because both
   move together.

   On ink the emphasis has to be carried by luminance instead, the same trade
   .section-ink .statement em already makes. --d1 rather than --d2, because the
   surrounding .prose is already --d2 and an accent that matches its own body
   copy is not an accent. */
.section-ink .accent { color: var(--d1); }
.section-photo-paper .row-lead { color: var(--t1); }

/* Whatever the sentence runs into follows it as a paragraph break, not a
   section break. §12's 6vw — 115px at 1920 — reads as the latter and breaks the
   join. TWO consumers as of 2026-08-21: index's outcome rows and About's
   discipline grid. The third, About's "Partner with us to" checklist, was
   removed on direction, and `.tick-list` went with it entirely (§16) — so the
   selector is dropped here rather than left dangling.

   Listed rather than written as a universal sibling rule so that a future block
   placed after a .row-lead has to opt in deliberately. */
.row-lead + .rows,
.row-lead + .disc-grid { margin-top: clamp(2.4rem, 2.6vw, 4rem); }

/* --------------------------------------------------------------------------
   33. Applications — the pinned composition, corrected

   §29 built the mechanism and it works: the pin holds to the pixel, all four
   states are reached, and the figure and the copy never disagree. What was
   wrong was WHAT was pinned. The section's headline and lead-in sat above the
   runway, so the first screen of scrolling carried them away and left a
   drawing and a paragraph alone in an empty viewport — with 235px of nothing
   above the figure and its shoulders cut off by the panel's own overflow at
   the bottom. It read as a broken section rather than as a pinned one, which
   is exactly what was reported.

   The wireframe draws ONE composition and holds it: headline, lead-in, then
   figure left / step copy right / scroll indicator far right. So the headline
   and lead-in are inside .appl-sticky now (see index.html), and this section
   is what makes that composition fit a viewport and sit on the grid.

   Everything in §29 that was right is untouched: the runway, the sticky, the
   grid-area stacking, the state machine in js/site.js, and both degradation
   paths. This overrides four things and adds two.
   -------------------------------------------------------------------------- */

/* The panel now holds a headline as well as the pair, so it needs the header's
   height as padding: `top: 0` with `align-content: center` centres the
   composition in the VIEWPORT, and the top 79px of that viewport is behind a
   fixed header. Centring inside the padding box puts it in the space the
   reader can actually see.

   overflow stays hidden from §29 — but it is now a backstop rather than a
   cropping tool, because the figure below is sized by height. */
.appl-sticky {
  padding-top: var(--header-h);
  /* vh in the middle term for the same reason as .appl-pair's margin: this is
     spending the viewport, and at 1280x800 a 3vw padding-bottom was the last
     15px of a 815px composition in an 800px box. */
  padding-bottom: clamp(1.2rem, min(2.6vw, 2.6vh), 4rem);
}

/* The head block sits on the same tracks as the pair, so the headline, the
   figure and the tick rail all share one left axis. Not .col-main: that stops
   at column 5 and the pair runs to 6, which would set the headline on a
   different right edge from everything under it. */
.appl-head { grid-column: 1 / -1; }

@media only screen and (min-width: 834px) {
  .appl-head { grid-column: 2 / 6; }
}

/* §5's `.statement + .prose` interval does not reach this: the lead is a .lead,
   not a .prose, and it was carrying `style="margin-top:3.2rem"` inline —
   which is both the wrong value and unoverridable from here, so it came out of
   the markup. Same token as every other headline-to-body interval on the site.

   The measure was an inline 56rem. As a rule it belongs with the component,
   and 52rem is the value that holds this sentence to three lines at 1920
   instead of four, which is what makes the composition fit. */
.appl-lead {
  margin-top: var(--gap-statement);
  /* 62rem, not 52. In a pinned panel the height budget is fixed and every line
     of this paragraph is spent from it: at 52rem it set to six lines and 208px
     and the composition overflowed a 900px viewport by 24px. 62rem is four
     lines and 139px, and 69 characters — still inside the 45-75 the rest of
     the site's measures sit in. The narrower measure was the more beautiful
     one in isolation and the wrong one here. */
  max-width: 62rem;
}

/* The gap between the head block and the stepped pair. Wider than the
   headline-to-lead interval, narrower than a section break: the pair is a new
   movement inside one composition, not a new section.

   In vh as well as vw, because this is the one interval on the site that is
   spending a fixed budget — a viewport — rather than dividing an unbounded
   page. The vw term keeps it proportionate on a wide short screen. */
.appl-pair { margin-top: clamp(2.4rem, min(4.4vw, 4.4vh), 7rem); }

@media only screen and (min-width: 834px) {
  /* Three columns now — figure, copy, scroll rail. The figure track is much
     narrower than §29's 0.9fr: the figure is sized by height here and its
     retargeted viewBox is portrait, so a wide track would only be padding.
     The copy takes what it gives up. The rail is `auto` — exactly as wide as
     two mono digits and a tick. */
  .appl-pair {
    grid-template-columns: minmax(0, 0.55fr) minmax(0, 1.45fr) auto;
    column-gap: var(--gutter);

    /* start, not center. The step bodies differ by 130px between /03 and /04,
       and with the row centred that difference moved the step's title and its
       /0n label up and down every time the state changed — the two things that
       are supposed to be the fixed points of a pinned panel. Top-aligned, only
       the length of the paragraph changes. */
    align-items: start;
  }
}

/* — The figure, sized by HEIGHT — */

/* §29 gave the SVG `width: 100%`, which makes the drawing as tall as its column
   is wide: 571px inside a 1080px viewport that also has to hold a headline, a
   lead-in and a step. It overflowed and was silently cropped by the panel.

   Height is the constrained axis in a pinned panel, so height is what this is
   sized on, and the width follows from the asset's own ratio. The clamp's
   ceiling is what keeps the composition inside 1080; its floor is the smallest
   the head reads at. */
@media only screen and (min-width: 834px) {
  /* max-height, NOT height — and the difference is a bug that shipped once.

     While this was an <svg> the pair `height: <fixed>; max-width: 100%` was safe:
     an SVG with a viewBox has an intrinsic aspect ratio it preserves, so a
     clamped width just letterboxed the drawing inside its box. An <img> does not
     do that. Give it a fixed height and let max-width bite, and the width shrinks
     while the height stays put — the picture is squashed, by 18% at 834x900,
     where the column is narrow but 30vh is not.

     Sizing on max-height instead lets the image satisfy both constraints by
     scaling, which is what preserves the ratio. Height is still the axis this is
     sized on in the common case — the intent of the rule is unchanged — it just
     yields to the column instead of distorting when the column is the tighter of
     the two. tools/check_appl_figure.js asserts the rendered ratio now. */
  .appl-figure img {
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: clamp(18rem, 30vh, 36rem);
  }
}

/* The shoulders are drawn running off the bottom edge — deliberate in the asset,
   and it worked when the figure filled a column inside a clipping panel. Now
   that the figure is height-sized with clear space beneath it, two open strokes
   stopping dead in the middle of the page read as a rendering fault rather than
   as a crop. Fade them instead: the last 14% of the drawing's height ramps to
   transparent, so the lines taper out the way a pencil would leave them.

   Only the mask — no reflow, no change to the drawing, and it degrades to the
   hard edge it already has if mask-image is unsupported. */
.appl-figure img {
  -webkit-mask-image: linear-gradient(180deg, #000 86%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 86%, transparent 100%);
}

/* — The scroll rail: the wireframe's vertical "scroll 02 03 04" — */

.appl-ticks {
  /* Below the breakpoint it is a horizontal row under the pair, as before: a
     vertical rail beside a full-width column has nothing to sit beside. */
  grid-column: 1 / -1;
  display: flex;
  gap: 0.8rem;
  margin-top: clamp(2.4rem, 3vw, 3.6rem);
  list-style: none;
  counter-reset: none;
}

.appl-tick {
  display: block;
  width: 2.4rem;
  height: 2px;
  background: var(--rule);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}

/* The number is for the desktop rail only; in the horizontal row the ticks are
   2px tall and there is nowhere to put it. */
.appl-tick span { display: none; }

.appl-tick.is-active { background: var(--navy); }

@media only screen and (min-width: 834px) {
  .appl-ticks {
    grid-column: auto;
    flex-direction: column;
    align-items: flex-end;
    gap: 1.6rem;
    margin-top: 0.6rem;
  }

  .appl-tick {
    display: flex;
    align-items: center;
    gap: 1rem;
    width: auto;
    height: auto;
    background: none;
    font-family: var(--font-mono);
    font-size: var(--fs-mono);
    letter-spacing: var(--ls-mono);
    color: var(--t3);
  }

  .appl-tick span { display: inline; }

  /* The tick itself, as a pseudo-element, so the marker and its number are one
     flex row and the number can carry the colour change with it. */
  .appl-tick::after {
    content: "";
    width: 2.4rem;
    height: 2px;
    background: var(--rule);
    transition: background var(--dur) var(--ease);
  }

  /* `.appl-tick { background: none }` above does NOT undo the base
     `.appl-tick.is-active { background: navy }` — one class against two, so the
     base rule wins inside this media query and the active tick rendered as a
     49x24 navy BLOCK beside its number instead of a hairline. Restated at
     matching specificity rather than by raising the other one. */
  .appl-tick.is-active { background: none; color: var(--navy); }
  .appl-tick.is-active::after { background: var(--navy); }
}

/* — Degradation, extended to what §29 did not know about — */

/* Both fallbacks unpin the panel and show all four steps in sequence. The
   padding that held the composition clear of the fixed header is meaningless
   once nothing is pinned, and the figure goes back to filling its column
   because there is no longer a viewport to fit inside. */
.no-js .appl-sticky,
.no-js .appl-head { padding-top: 0; }

  /* max-height: none is load-bearing. The min-width:834px rule caps height so the
   pinned figure fits its panel; every rule here UNPINS, and `height: auto` no
   longer cancels that cap now that it is max-height rather than height. Leave it
   standing and it fights `width: 100%` - the width fills the column, the height
   stops at 30vh, and the picture squashes. Measured at 1280x640: 60% too wide. */
.no-js .appl-figure img { width: 100%; height: auto; max-height: none; }

@media (prefers-reduced-motion: reduce) {
  .appl-sticky { padding-top: 0; }
  /* max-height: none is load-bearing. The min-width:834px rule caps height so the
     pinned figure fits its panel; every rule here UNPINS, and `height: auto` no
     longer cancels that cap now that it is max-height rather than height. Leave it
     standing and it fights `width: 100%` - the width fills the column, the height
     stops at 30vh, and the picture squashes. Measured at 1280x640: 60% too wide. */
  .appl-figure img { width: 100%; height: auto; max-height: none; }
}

/* THE THIRD DEGRADATION, and the one §29 did not have because §29 pinned less.

   A pinned composition is a fixed height budget, and this one now holds a
   three-line headline, a four-line lead-in, a figure and a step. Measured, it
   needs 791px at 1280 wide. Below roughly 700px of viewport height there is no
   arrangement of it that fits, and a sticky panel taller than its own scrollport
   does not pin — it scrolls, slowly and wrongly, which is worse than not
   pinning at all. A short window is a real case (a laptop with devtools open is
   the common one), so it gets the fallback rather than a broken pin.

   Same treatment as no-JS: unpin, un-stack, show all four. The runway collapses
   with it, so the page also gets 240vh shorter — which on a short screen is the
   right answer anyway. */
/* THRESHOLD MOVED to 579px — see §44(c). This block is the one that does the
   work; the number lives in both places only because §44 explains why it
   changed, and two gates at different heights would open a band where §33
   unpins while §44 still expects a pin. One number, stated twice, matching.
   It has been 699, then 659, now 579 — if you change it, change BOTH. */
@media only screen and (max-height: 579px) {
  .appl-scope { height: auto; }
  .appl-sticky {
    position: static;
    min-height: 0;
    padding-top: 0;
    overflow: visible;
  }
  /* max-height: none is load-bearing. The min-width:834px rule caps height so the
     pinned figure fits its panel; every rule here UNPINS, and `height: auto` no
     longer cancels that cap now that it is max-height rather than height. Leave it
     standing and it fights `width: 100%` - the width fills the column, the height
     stops at 30vh, and the picture squashes. Measured at 1280x640: 60% too wide. */
  .appl-figure img { width: 100%; height: auto; max-width: 32rem; max-height: none; }
  .appl-steps { row-gap: clamp(3.2rem, 5vw, 6rem); }
  .appl-step {
    grid-area: auto;
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: none;
  }
  .appl-ticks { display: none; }
}

/* --------------------------------------------------------------------------
   34. Centration — what the six-track grid needs on top of §4

   §4 mirrors the annotation rail so the content block sits on the viewport's
   centre line. Three components were placed against the OLD grid's flush right
   edge and have to move with it, and one heading needs a different measure now
   that the block it sits in is narrower.
   -------------------------------------------------------------------------- */

/* The footer rail — the hairline and the legal line under the footer columns.
   `1 / -1` spanned the full padded width, which was the same thing as the
   content block on the old grid and is 340px wider than it on this one, so the
   rule would have stuck out past every column it underlines. */
@media only screen and (min-width: 834px) {
  .footer-rail { grid-column: 2 / 6; }
}

/* 03 Applications' headline was cramped: "Enabling the next generation of
   visual computing" broke to three lines in a 601px box while 460px of its own
   column sat empty beside it. That is `.statement`'s 20ch cap doing the right
   thing for a four-word headline and the wrong thing for a seven-word one, and
   the narrower content block makes it worse rather than better.

   The cap is what has to give, not the type size — this is the longest heading
   on the site and it is the one that should use the width available to it. 30ch
   sets it to two lines at 1920 and 1440 and three at 1280, with the break
   falling after "generation of" at every width, which is where the sense
   divides. It is also the only heading on the site inside a PINNED panel, so
   every line it saves is a line the composition below it gets back.

   Scoped to this section rather than raised on `.statement`, because the 20ch
   cap is correct for the other four headings: they are three to five words and
   a wider cap would set them as one long line instead of the stacked pair the
   design wants. */
.appl-head .statement { max-width: 30ch; }

/* --------------------------------------------------------------------------
   35. Mission — the macro lens as a ground rather than a panel

   Directed 2026-08-20: "For our mission, use the image as a background image."

   It was a 4:5 portrait panel beside the copy (§24's .split, flipped). As a
   ground it stops being an illustration of the section and becomes the section's
   surface, which suits this shot: IMG_0517 is an extreme macro with no subject
   to read, only the lens edge and the interference colours along it. There is
   nothing in it to look AT, which is exactly what makes it a good ground and
   made it a slightly inert panel.

   Copy on the right, over the pale bokeh, with §30's paper scrim ramping
   right-to-left — the same treatment as 04 Why us, and for the same reason: the
   subject holds one side of the frame and the type takes the other.

   About's mission section has no headline of its own; the About hero's "Vision
   without compromise" is the heading it belongs to, and the wireframe draws it
   that way. With the section eyebrows now removed the copy is the whole
   content, so it gets the vertical room a headline would otherwise have taken.
   -------------------------------------------------------------------------- */

#mission {
  /* Taller than a standard section: with a photograph as the ground and only two
     paragraphs on it, the default 8.4vw block padding left the picture as a
     thin band. This is the one section on either page whose height is set by
     wanting to see the photograph. */
  padding-block: clamp(12rem, 16vw, 24rem);
}

/* The intro-copy tier is About's opening voice — larger than body copy. Over a
   photograph it needs the same tier lift §30 gives everything else, which it
   gets from the --t2 override there; this only holds the measure, since the
   right-hand column is narrower than the full-width block it was written for. */
#mission .intro-copy { max-width: 46rem; }

/* No `.accent` override here. It was written and then deleted: `.accent` is
   already Oxford Blue from §5 and §30 does not touch it, so the rule was
   redundant — and `tools/test_intro.js` asserts that no `.intro-copy .accent`
   selector exists at all, because one used to and became stale when the
   Introduction's payoff line moved out of `.intro-copy`. A live rule that trips
   a staleness canary is worse than no rule: the next person deletes the wrong
   one. If this ever does need an override, scope it to `#mission .accent`. */

/* --------------------------------------------------------------------------
   36. Height for the photographic sections

   .section's 8.4vw block padding was written for sections whose height is set
   by their copy on a flat ground. A section whose ground is a photograph has a
   second thing asking for height — the picture — and three of them were losing
   that argument badly. Measured at 1920:

     #platform   565px tall, 3.4:1 — the lens in IMG_0464 is 2320px of a 4000px
                 frame, so a 3.4:1 window through a 2:1 asset showed a horizontal
                 slice of bright blur with the lens cut off top and bottom. The
                 band read as empty.
     #why        502px tall, 3.8:1 — same fault, and the assembly press that is
                 the whole left half of the frame was reduced to its middle third.
     #mission    handled in §35 for the same reason.

   These are the only three sections on the site whose height is a picture's
   argument rather than a paragraph's, so it is set per section rather than by
   loosening .section for everything. Not `min-height` in vh: a viewport unit
   would make the copy's own leading the variable that changes when the window
   is resized, and these bands are not full-bleed compositions — they are bands.
   -------------------------------------------------------------------------- */

#platform { padding-block: clamp(9rem, 12vw, 19rem); }
#why      { padding-block: clamp(9rem, 12vw, 19rem); }

/* --------------------------------------------------------------------------
   37. The header, on the content axis

   `.header-inner` is a flex row with the container's own max-width and outer
   padding, so it ran 132px to 1788px at 1920 — the full padded width. That was
   the same line the page's content ran on, and the two agreed.

   They stopped agreeing when the grid was re-centred (§4). The content block is
   now 428 to 1492 and the header was left spanning 1656px around it: the
   wordmark 296px left of every headline on the page, and the navigation 296px
   right of every paragraph, with the whole page apparently floating in the
   middle of its own chrome. Nothing was wrong with the header; the thing it was
   measured against moved.

   So it reads the same numbers the grid does. Not by becoming a `.container` —
   it is a flex row and does not want four tracks — but by padding in from the
   page edge by exactly one rail plus one gutter, which is the definition of the
   content block's left edge. `1.08fr` of a four-track grid resolves to 23.5% of
   the block's own width; expressed against the padded width, which is what this
   element has, that is the same fraction of the same box.

   Below 1024 the rails collapse and so does this, in the same media query, so
   the header and the grid cannot disagree at any width.
   -------------------------------------------------------------------------- */

/* One rail plus one gutter — the definition of the content block's left edge —
   at every width, in one declaration, reading the grid's own token.

   THIS WAS TWO RULES AND A RE-DERIVATION. `--outer + --gutter` below 1024, and a
   media query above it that rebuilt the rail from first principles:

     (min(100%, var(--max)) - 2 * var(--outer) - 5 * var(--gutter)) * 0.1753

   It had to, because `--rail` was `1.08fr` and an `fr` is not a length a padding
   can use. But it was a second copy of §4's grid arithmetic, and a copy is a
   thing that can drift — it already carried its own rounding, 0.1753 against the
   exact 1.08/6.16, which put the wordmark 0.05px left of the content block at
   1920 (measured; the harness reported Δ -0.05px at 1920, 1680 and 1440).

   §2's rail is a LENGTH now, so the header reads the same token the grid reads
   and the two cannot disagree at any width by any amount. That matters more than
   it used to: the rail RAMPS between 1024 and --max, and a re-derivation would
   have had to reproduce the clamp, the slope and the cap term for term to stay
   on the same axis through that range. Below 1024 --rail is 0px, so this
   collapses to `--outer + --gutter` on its own, which is exactly what the
   deleted first rule said — one declaration now covers both cases.

   Kept from the deleted comment because it is the record of a real bug: a
   PERCENTAGE padding resolves against the CONTAINING BLOCK, which for this
   element is `.header` at the full viewport width — but `.header-inner` is
   itself capped at --max and centred, exactly as `.container` is, so above
   1840px the percentage was being taken of 1920 while the grid took its share
   of 1840, and the wordmark landed on 442 against the content block's 428.
   var(--rail) contains no percentage and cannot reintroduce that. */
.header-inner { padding-inline: calc(var(--outer) + var(--rail) + var(--gutter)); }

/* --------------------------------------------------------------------------
   38. DELETED 2026-08-21 — About's hero foot, the "Track record" chips

   Written on 2026-08-20 and dead within a day: it existed only to seat
   `.hero-aside` in the third track of `.hero-foot`, and the chips, the aside and
   that three-column grid have all been deleted (see §8). Deleted the day after
   it was retired, in the same pass.

   The reasoning is the part worth keeping, so it stays here: a grid track that
   exists only to hold two things APART will silently swallow the second one if
   auto-placement is left to decide. The symptom is a component declaring
   `text-align: right` while actually sitting in the centre track — it was
   measured at 1920 with the chips at x 1048, i.e. 316px short of the right edge
   they were aligned to. Naming the track was the whole fix. Worth recognising
   again in a component that is still alive.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   39. The hero's height, below the breakpoint

   §8's 76svh was measured on the desktop composition, where the lockup sits
   beside the photograph and both share the frame's width. Below 834px they
   share its HEIGHT instead: the picture becomes a feathered band across the
   bottom 48% (§23) and the lockup stacks above it.

   76svh does not survive that. At 390x844 the hero came to 641px, the band
   started at 334, and the lockup — 25% larger since the type scale landed —
   ran to 357, so the spectrum rule finished 23px INSIDE the band's own 43px
   feathered edge and the last outcome line sat on the picture's top corner.
   Both still clear AA (13.0:1, measured) because that corner is pale wall, but
   the composition was crowded rather than composed, and the next person to
   nudge --hl-scale would have pushed the type onto the desk.

   88svh gives it 743px at 844: the band starts at 386, the lockup ends at 357,
   and there are 29px of paper between them. The gate is the same 834px the rest
   of the layout uses, so there is one breakpoint to reason about, not two.
   -------------------------------------------------------------------------- */

@media only screen and (max-width: 833px) {
  .hero-inner { min-height: 88svh; }
}

/* --------------------------------------------------------------------------
   40. The merged About opening — an H1 living inside a .section

   About's hero and its mission section were merged on direction. The mission
   section is the base, so its photographic ground and its right-hand copy
   column stay, and the page's H1 moves in above the copy.

   That breaks three assumptions the .hero-* rules were written under, and each
   one has to be undone rather than left to resolve by luck:

     .hero-eyebrow   is `grid-column: 1 / -1; grid-row: 1` and centred, because
                     in .hero-inner it was a row of a three-row grid. Here it is
                     a flow child of .col-main, so the grid properties are inert
                     and only `text-align: center` still bites.
     .hero-title     is centred and capped at 18ch, for a lockup that sat in the
                     middle of a full screen.
     .hero-spectrum  was the third of these — `flex: none`, `width: min(30rem,
                     46vw)`, sized for a flex column and with a clearance tuned
                     against a centred figure. DELETED on direction 2026-08-21;
                     what it leaves behind is the interval note further down.

   Both survivors become left-aligned, because the column they are now in is
   left-aligned text flushed to a right-hand axis (section 30). The section is
   not centred just because it sits on the right.
   -------------------------------------------------------------------------- */

.hero-merged .hero-eyebrow,
.hero-merged .hero-title {
  text-align: left;
}

/* 18ch was for a display line centred in a viewport. Here the column already
   caps at 54rem (section 30's right-hand rule), and a ch cap on top of that
   would fight it — at --fs-display's 76px, 18ch is 684px against a 540px
   column, so the ch value never applies and only confuses the next reader.
   The column owns the measure. */
.hero-merged .hero-title { max-width: none; }

/* The interval under the headline. This used to be TWO rules, because the
   spectrum rule sat between the H1 and the copy and a mark needs a different
   gap above it than below it:

     .hero-merged .hero-spectrum          { margin-top: var(--gap-statement); }
     .hero-merged .hero-spectrum + .intro-copy { margin-top: clamp(2.4rem, 2.8vw, 4.4rem); }

   `.hero-spectrum` was deleted on direction on 2026-08-21 (see §8), which took
   the second rule's adjacency with it — an adjacency selector stops matching
   the moment the element between the two sides goes, silently and with no
   error, and the copy would have closed right up under the H1. So the interval
   moves onto the copy itself and takes the section's own headline-to-body
   value, which is what the H1-plus-paragraph pair wanted in the first place. */
.hero-merged .intro-copy { margin-top: var(--gap-statement); }

/* The eyebrow is the page's location marker and sits above everything, so it
   gets the interval a section eyebrow had before those were removed. */
.hero-merged .hero-eyebrow { margin-bottom: clamp(2rem, 2.4vw, 3.6rem); }

/* .hero-title carries .display, which is --fs-display — the largest tier on the
   site and correct for an H1. Nothing here changes the size: the merge was
   about the wrapper, not about the type. */

/* Cap the COLUMN, not each child.

   Section 30 right-flushes and caps every direct child of a right-hand
   .col-main individually — `> * { max-width: 54rem; margin-inline-start: auto }`
   — which is right when the children are all text blocks of the same measure:
   each one's right edge lands on the same axis and their left edges land there
   too, because they are all the same width.

   It stops being right the moment the children have different widths. Measured
   here: the H1 is uncapped so it fills the 764px column and starts at 951.9,
   the copy is capped at 540px and right-flushes to start at 1176, and the
   spectrum rule is 300px wide so it floats 464px right of the headline it
   underlines. Three different left edges in one block, one of them a rule that
   is supposed to be attached to the type above it.

   So the cap moves up one level: the column is 54rem wide and flushed right,
   and its children fill it and share both edges. One axis for the whole
   opening, which is what the merge is for. */
.hero-merged > .container > .col-main {
  max-width: 54rem;
  margin-inline-start: auto;
}

.hero-merged > .container > .col-main > * {
  max-width: none;
  margin-inline-start: 0;
}

/* --------------------------------------------------------------------------
   41. The nav numerals over a photograph — the FOURTH instance of one trap

   `.nav-index` is the mono "01 02 03 04 05" beside each nav link, at --t3.
   Measured over index's hero tabletop: **3.62:1 at 1920, 3.59:1 at 1440**,
   against a 4.5 floor. It has presumably been failing since the hero gained a
   photographic ground, and it was never caught because the two checkers that
   look at the header each sample something else — check_hero_photo.js measures
   `header nav` as one box, whose MEAN ground is fine, and the numerals are a
   smaller, paler element inside it; check_section_photo.js hides the header
   outright, because an element screenshot composites whatever overlaps it.
   Between them, this element had no owner. tools/_hdr_contrast.js was written
   to close that gap and is what found it.

   Same root cause as the three before it, verbatim from §2's own token comment:
   t3 is held "at the WCAG AA floor for normal text" — 4.8:1 against FLAT white
   — so a photograph, which moves the ground around, has nothing to eat into.
   The previous three were --d3 (§21), --t2/--t3 (§30) and .accent (§35).

   Fixed where the ground is, not on the component: the header is a fixed bar
   that travels the whole page, so darkening `.nav-index` globally would make
   the numerals as dark as the links they annotate on every flat section, and
   the numerals are meant to be quieter than the links. Scoped to the case that
   fails — the header sitting over the hero's photograph, which is only the case
   before the header takes its own opaque background at 40px of scroll.

   0.78 rather than 0.74: the numerals are 10px, the smallest type on the site,
   and this ground is oak rather than cleanroom white — 5.9:1 measured, which
   leaves the margin §2 says t3 does not have. */
.hero-photo ~ .header .nav-index,
.header:not(.is-stuck) .nav-index {
  color: rgba(7, 1, 23, 0.78);
}

/* Once the header has its own background the numerals are back on a flat, known
   surface and can be quiet again. `.is-stuck` is added by js/site.js at 40px of
   scroll, which is well before the hero's picture leaves the viewport, so the
   override above only ever applies over the top of the photograph. */
.header.is-stuck .nav-index { color: var(--t3); }


/* --------------------------------------------------------------------------
   42. Per-section scrim weight

   Section 30 makes the scrim four numbers. These are the four numbers, per
   section, and each set is the LIGHTEST that still clears AA at all four of
   check_section_photo.js's viewports — measured, then pushed down until it
   failed, then backed off one step. Re-run that checker after touching any of
   them; the raw figures each set was derived from are in section 30's own note.
   -------------------------------------------------------------------------- */

/* The platform band. Raw under-text 5th percentile 169-173/255 — there is
   nothing dark anywhere in this frame, so the scrim was doing no work for
   legibility and all of the washing-out. This is as close to nothing as the
   component goes: enough to keep the copy's own ground from tracking the
   picture's highlights as it scrolls, and no more. */
#platform {
  --scrim-max: 0.20;
  --scrim-mid: 0.12;
  --scrim-min: 0.04;
}

/* 02 The solution keeps section 30's defaults and is the reason they are what
   they are. The sharp assembly seen through the lens reads 38/255 at its
   darkest, directly under the 14px row bodies — the one genuinely dark ground
   on the site. Note that the clear window (--scrim-win) is 0.04, so the part of
   the photograph the section was cropped around is now essentially unscrimmed;
   the weight is spent only where the rows are. */

/* 04 A strategic partner. REVERSED 2026-08-21, on direction: "Add the effect
   back in on the image, so the text is readable again. Use the same effect/
   values as before."

   The 2026-08-21 reduction took this section to 0.40/0.28/0.10. It cleared AA
   at the four checked viewports and was still too light to READ comfortably,
   which is the distinction this file had not been drawing: AA is a floor, not a
   target, and this is the one of the four frames with real tonal range under
   the copy (raw 75/255 behind the headline, 109-168 under the body — a doorway
   sits in the crop's right side). A floor-grazing scrim over a ground that
   MOVES that much leaves no margin for the eye even when the ratio passes.

   Back to the pre-reduction weight. `--scrim-max` is the 0.95 plateau every
   paper section shared before the reduction; mid and min keep the ramp shape
   the four-token structure introduced, at the same proportions the other sets
   use (mid ~0.70 of max, min ~0.25). NOT a return to the old hard-coded
   gradients — those are gone — but the same delivered density.

   The other three sections keep their reduced weights: the complaint was about
   this section, and #platform and #mission were the two that measured as never
   having needed a scrim at all. */
#why {
  --scrim-max: 0.95;
  --scrim-mid: 0.66;
  --scrim-min: 0.24;
}

/* About's mission, which is now also About's HERO — the page's H1 lives in this
   section since the 2026-08-21 merge. Halved on direction: "Make background a
   little brighter, but not as much as before. maybe 50% strength as before."
   So exactly half of the 0.22/0.14/0.05 this section was reduced to earlier the
   same day, rather than a return to raw.

   It has the headroom for it: raw 177-185/255, the brightest of the four, and
   the largest type on either page (a 76px H1 and 28px copy, both of which clear
   AA on far less ground than 14px body copy needs). The lens edge and its
   interference colours now come back essentially untouched, which is the whole
   reason this frame was chosen. */
#mission {
  --scrim-max: 0.11;
  --scrim-mid: 0.07;
  --scrim-min: 0.025;
}

/* About's #next, "Building the vision standard for human-centred computing".
   Converted from §21's ink ground to §30's paper ground on direction: "Use dark
   fonts and bring the background image up to be bright instead. Use the 50%
   strength as with the hero section." So it takes #mission's halved set.

   THIS IS THE RISKY ONE OF THE FIVE, and the reason is provenance:
   sec-next.jpg was chosen FOR the ink case, because it is the one frame with
   real mid-tones rather than a near-white cleanroom field. Every other paper
   section is a bright frame that never needed scrimming; this is a mid-tone
   frame being asked to carry dark type on 50% of the lightest scrim on the
   site. Measured, not assumed — see the note left beside whichever value
   actually survived check_section_photo.js. */
#next {
  --scrim-max: 0.11;
  --scrim-mid: 0.07;
  --scrim-min: 0.025;
}


/* --------------------------------------------------------------------------
   43. Two measures that were capping the wrong thing

   Both directed 2026-08-21, both the same shape of fault: a cap chosen for one
   context and then inherited by another where it does the opposite of what it
   was for.
   -------------------------------------------------------------------------- */

/* (a) The closing strapline. "Increase the width that this takes up to be
   closer to the full body width of the page."

   `.strapline-text` is --fs-h3 with `max-width: 26ch`, and it rendered 492px
   wide inside a 1064px content block at 1920 — five lines using 46% of the
   width it had. 26ch is a sensible cap for a display line that has to hold the
   centre of a band; it is the wrong cap for the site's closing statement, which
   is the last thing the reader sees and wants to land as a block rather than a
   column.

   The cap comes off and the content block governs, which is what "the full body
   width of the page" means on this grid — 1064px at 1920, the same width the
   outcome rows and the discipline grid use. `text-wrap: balance` stays: at this
   width the last line would otherwise strand a word or two. */
.strapline-text { max-width: none; }

/* (b) `.row-lead` must set on ONE line.

   Section 32 gave it `max-width: 34ch` and `text-wrap: balance`, sized for
   "Embedding our OO-Adaptive Optics Platform into devices unlocks" as a
   deliberate two-line block. Directed now: one line, same size. Both strings
   were shortened for it — the index one lost its "our" and gained a colon.

   `balance` is the half that actually breaks it: it distributes text evenly
   across whatever lines it decides on, so it will choose two lines over one even
   when one fits. Both go, and the column becomes the only constraint.

   Measured at --fs-lead's 28px: "Embedding OO-Adaptive Optics Platform into
   devices unlocks:" is 58 characters and wants about 810px; the content block is
   1064px at 1920 and 836px at 1440, so it sets on one line at both. At 1280 the
   block is 744px and it wraps to two — which is correct rather than a failure,
   since the alternative is either shrinking the type the user asked to keep or
   letting it overflow. Below the breakpoint it wraps freely, as it must. */
.row-lead {
  max-width: none;
  text-wrap: normal;
}

/* "Have it slightly closer to the table below." Section 32 set this interval as
   a paragraph break rather than a section break; this takes it in one step
   further, so the sentence and the row it completes read as one unit. The rows'
   own top hairline supplies the visual separation, so the gap does not have to. */
.row-lead + .rows,
.row-lead + .disc-grid { margin-top: clamp(1.6rem, 1.7vw, 2.6rem); }

/* --------------------------------------------------------------------------
   44. Where the Applications panel locks, relative to the reader's window

   Directed 2026-08-21: "Change the point at which the page stops scrolling when
   the scrolling block with the applications is in the middle of the page, so the
   scrolling portion is lined up with the middle height level on the users
   browser."

   Measured, with the pin engaged, composition top..bottom against the window:

     1920x1080   comp  207..924   centre 565   window centre 540   +25
     1920x 920   comp  150..825   centre 488   window centre 460   +28
     1512x 860   comp  124..792   centre 458   window centre 430   +28
     1440x 760   comp   79..745   centre 412   window centre 380   +32   sticky 765 > 760
     1280x 720   comp   79..759   centre 419   window centre 360   +59   sticky 777 > 720

   TWO faults, and the second is the one that would have been reported.

   (a) It sits low everywhere, by 25 to 32px. `align-content: center` centres
       inside the PADDING box, and §33 gave the panel `padding-top:
       var(--header-h)` (79px) against a padding-bottom of about 28px. A 51px
       asymmetry centres the content 25px below the middle. The padding-top is
       there for a good reason — a fixed header covers the top 79px, so centring
       on the window would push the composition under it — but then the target
       is the middle of the VISIBLE area, not the middle of the window, and the
       two differ by 40px. It was aiming at neither.

   (b) Below about 800px of window height the panel is TALLER than the window —
       765px in a 760px window, 777px in a 720px one — and a sticky element
       taller than its own scrollport does not pin. It scrolls, slowly, which is
       worse than not pinning: the composition creeps upward with its bottom cut
       off and its top jammed under the header, which is exactly "not lined up
       with the middle height level". §33's fallback only unpins below 699px, so
       700-800 was a band where the pin was broken rather than absent. That band
       is most laptop windows once browser chrome is taken off a 1080 screen.

   Fixed in three parts: balance the paddings, make the composition small enough
   to fit further down, and move the fallback threshold to where it genuinely
   stops fitting.
   -------------------------------------------------------------------------- */

/* (a) Centre on what the reader can see.

   For the content's centre to land on the visible area's centre, the padding
   difference has to equal the header's height exactly: padding-top minus
   padding-bottom = --header-h. So padding-top carries the header AND whatever
   padding-bottom is, which keeps one number to tune instead of two. */
.appl-sticky {
  --appl-pad: clamp(1.2rem, min(2.6vw, 2.6vh), 4rem);
  padding-top: calc(var(--header-h) + var(--appl-pad));
  padding-bottom: var(--appl-pad);
}

/* (b) Make it fit further down.

   Between 800px and 900px of height the composition fits but with almost
   nothing to spare, and the thing eating the budget is the head block: an h2
   plus a four-to-five-line lead-in, 320-340px of a 620px allowance. So at short
   heights the lead-in gives up its own tier and drops to body size — it is a
   lead-IN, the sentence that sets up the four steps, not the section's argument,
   and on a short screen a reader would rather have the figure and the step than
   a larger setup line. Measured saving: 58px at 1280x720, which is what closes
   the overflow there.

   Height-only query. A wide short window is exactly the case this is for, so
   width must not be part of the condition. */
@media only screen and (max-height: 899px) {
  .appl-lead {
    font-size: var(--fs-body);
    line-height: 1.5;
    max-width: 56rem;
    margin-top: clamp(1.6rem, 2vh, 3.2rem);
  }

  .appl-pair { margin-top: clamp(1.6rem, 3vh, 4rem); }
}

/* (c) And where it still does not fit, unpin honestly.

   §33 put this threshold at 699px, which was measured against the composition
   as it stood before the head block moved into the panel. With (b) applied the
   composition fitted down to about 660px, so the threshold moved to just under
   that — and the effect survives on every window that can actually hold it,
   which is the point of moving it rather than raising it.

   NOW 579px, re-derived 2026-08-27 — and this was a REPORTED BUG, not a tidy-up.
   A colleague on a 14-inch laptop found the Applications section "doesn't
   scroll" on the laptop screen, while the same page worked when she dragged it
   to an external monitor. Cause: her panel is 1920x1080 at 150% scaling, which
   is a 1280x720 CSS viewport, and Chrome's tab strip, address bar and bookmarks
   bar take about 100 CSS px of it — leaving ~620px, under the 659 gate. So she
   was getting the honest-unpin fallback, working exactly as designed. The
   fallback was catching a mainstream configuration, not the edge case (a laptop
   with devtools open) it was written for.

   The 659 number was known to be too conservative — it was derived WITH
   `.appl-lead` in the composition, and that element was deleted on 2026-08-21,
   giving the panel its height back at every viewport. HANDOVER.md open item T
   recorded that the gate wanted re-deriving from a fresh measurement rather than
   an edit made on inference. This is that measurement.

   MEASURED, at 1280 wide, walking the real code path with the gate temporarily
   lowered so the pin actually engaged:

     vh 660  panel 660  ink 146..593  figure 198px   fits, 118px of slack
     vh 620  panel 620  ink 127..572  figure 186px   fits          <- her case
     vh 580  panel 580  ink 108..551  figure 180px   fits
     vh 560  panel 560  ink  98..541  figure 180px   fits
     vh 540  panel 550 > 540                         FAILS - creeps
     vh 520  panel 548 > 520, 15px below the fold    FAILS

   The composition genuinely stops fitting at about 550, because the figure's
   `max-height: clamp(18rem, 30vh, 36rem)` bottoms out on its 18rem floor at
   ~600px of viewport and cannot shrink further. 579 sits just above that with a
   step of margin, rather than on the exact edge — font metrics differ across
   platforms and this is a height budget, not a width one.

   Verified after the change at 1280x620/600/580, 1536x650, 1707x700, 1920x600,
   1440x590, 1024x620 and 834x600: pin holds, panel never exceeds the viewport,
   nothing under the header, nothing below the fold, all four states reached and
   figure/copy agreeing at all 31 samples.

   STILL OPEN, and this does not fix it: the gate is HEIGHT-ONLY, so a
   narrow-but-tall window still sails over it carrying a panel that does not fit.
   At 390x844 the panel is 1079px in an 844px viewport and creeps. That is open
   item Y and it needs a width condition, which is a separate design call.

   The rules themselves are in §33; this only moves the gate. Kept as a separate
   query rather than edited into §33 so that the reasoning above stays attached
   to the number. */
@media only screen and (max-height: 579px) {
  .appl-scope { height: auto; }
  .appl-sticky {
    position: static;
    min-height: 0;
    padding-top: 0;
    overflow: visible;
  }
  /* max-height: none is load-bearing. The min-width:834px rule caps height so the
     pinned figure fits its panel; every rule here UNPINS, and `height: auto` no
     longer cancels that cap now that it is max-height rather than height. Leave it
     standing and it fights `width: 100%` - the width fills the column, the height
     stops at 30vh, and the picture squashes. Measured at 1280x640: 60% too wide. */
  .appl-figure img { width: 100%; height: auto; max-width: 32rem; max-height: none; }
  .appl-steps { row-gap: clamp(3.2rem, 5vw, 6rem); }
  .appl-step {
    grid-area: auto;
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: none;
  }
  .appl-ticks { display: none; }
}

/* --------------------------------------------------------------------------
   45. The second directed-notes pass, 2026-08-21

   Three unrelated fixes that arrived as one list. Each carries its direction.
   -------------------------------------------------------------------------- */

/* — 02 The solution: a light panel under the outcome rows —

   "For the 4 points in the table, use a slight light white background that is
   subtle, to make the text easier to read."

   This section is the one paper ground with a genuinely dark photograph under
   its copy: the sharp assembly seen through the lens reads 38/255 at its
   darkest, directly beneath the 14px row bodies (§30's own note). It is why
   #solution carries the heaviest scrim of the four, and it is still the hardest
   of the four to read, because a linear scrim is a gradient and these rows run
   the full width of it.

   A local white panel is the right instrument rather than more scrim: scrim
   darkens the whole band to help the type, which is what the user objected to
   everywhere else on the page. This lifts the ground only where the type
   actually is, and leaves the rest of the photograph at the weight the section
   was tuned to.

   ON THE BRAND PROHIBITION: "a tinted box behind text" is prohibited, and §26's
   pull quote was built without one for that reason. This is not that. The
   prohibition is about boxes used as a SEPARATING device in place of hairlines;
   the hairlines here are untouched and still do all the dividing. This is a
   legibility ground over a photograph — the same job §21 and §30 already do
   with ink, in the one place where ink is the wrong colour to do it with. */
#solution .row {
  background: rgba(255, 255, 255, 0.55);

  /* The panel needs its own inline padding or the type sits on the cut edge.
     The base component only gains padding-inline ON HOVER, as a 1.6rem nudge —
     with an invisible ground that reads as a highlight, but with a visible
     panel it would read as the panel jumping wider under the cursor. So the
     padding is permanent here and hover changes the tint alone. */
  padding-inline: clamp(1.6rem, 1.8vw, 2.4rem);
}

/* Square, deliberately. A radius would make these read as four cards, which is
   the tinted-box language the brand rules out; a field with hairlines through
   it stays the site's own language. */
@media only screen and (min-width: 834px) {
  #solution .row:hover {
    background: rgba(255, 255, 255, 0.72);
    padding-inline: clamp(1.6rem, 1.8vw, 2.4rem);
  }
}

/* — 03 Applications: the gap above the tags —

   "Increase the gap between the application text and the little tags such as
   'over 45s'."

   `.pill-list` HAD NO CSS RULE ANYWHERE, despite four uses on index.html and
   being a named component in js/editor-agent.js. (An earlier version of this
   comment said five; counted in the live markup with comments stripped it is
   four, all of them in the Applications steps.) That is the whole bug and it
   is worse than the reported symptom:

     - the gap above it was 0, so the tags sat directly against the body copy;
     - the 3.8px between pills was COLLAPSED WHITESPACE between inline-flex
       items, not a gap — i.e. it depended on how the HTML happened to be
       indented;
     - and at 390px, where the row wraps, the vertical clearance was 0.00px:
       the pill borders touched.

   So this authors the component rather than patching the margin. One `gap`
   fixes the horizontal accident and the wrapped-row collision at the same
   time, because `gap` applies on both axes. */
.pill-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.8rem;
  /* The requested interval. Larger than .appl-step-body's own 1.6rem top
     margin: these are metadata about the step, so they need to read as a
     separate register rather than as the paragraph's last line. */
  margin-top: clamp(2.4rem, 2.6vw, 3.6rem);
}

/* NOTE for whoever next touches §44: its `@media (max-height: 899px)` block
   still restyles `.appl-lead`, which was DELETED from index.html on direction
   in this same pass. That rule now matches nothing. It is left in place rather
   than removed because §44's height budget was calculated WITH the lead in it —
   the unpin gate at 659px and the "no overflow at 720" measurements both
   assumed it. The panel has gained back whatever the lead occupied, so the gate
   is now conservative rather than wrong, and re-deriving it wants a fresh
   measurement pass rather than an edit made on inference. */

/* — 00 The platform: the gap under the magazine lede — WITHDRAWN 2026-08-27 —

   This carried `#platform .prose > .lead + p { margin-top: 0.7em; }`. The rule
   is deliberately gone rather than retuned; the note stays because the next
   person to re-tier this block needs to know the compensation existed.

   WHY IT EXISTED. The re-tier split ONE SENTENCE across two block elements at
   two different sizes: the `.lead` ended on "…in seconds," and the paragraph
   below it opened lowercase on "transforming". At §5's standard
   `.prose > * + *` beat that read as two broken fragments rather than as a
   sentence continuing at a smaller size, because the beat is sized for a
   paragraph BREAK. Tightening to 0.7em — 11.2px against 25.6px — was as far as
   CSS could carry a defect that only the copy could fix. It was open item R.

   WHY IT IS GONE. The copy was fixed. Directed 2026-08-27, the `.lead` now ends
   "…in seconds." and the body opens "By transforming", so both boundaries in
   this three-paragraph block are ordinary sentence boundaries and there is
   nothing left to compensate for. Keeping the tightening would have set the
   block's two sentence breaks at two different intervals: measured at 1920 and
   1440 with the new copy, the lead→body ink gap was 13.9px against 30.4px for
   the body→body break below it — a 2.2x difference inside one block, where the
   tightened join says CARRY ON and the full stop says STOP. Withdrawn, they
   read 28.3px and 30.4px. They are not identical because the lead's
   half-leading is 3.36px against the body's 4.40px, which is why the join wants
   the same MARGIN as the break below it rather than the same optical gap.

   IF THIS LEDE IS EVER RE-CUT at a clause boundary instead of a sentence
   boundary, the rule comes back. Keep the `.lead + p` selector if so: the third
   paragraph ("By embedding…") is a separate sentence either way and must keep
   the full beat. And keep the warning that 0.7em resolved against the
   PARAGRAPH's own 16px, not the lead's 28px — `em` on a margin resolving
   against the element's own size has caused two separate bugs in this file.

   See COPY-CHANGES.md §5g. One editorial item is still open and was created by
   the fix: "By transforming vision correction … into a driver of accessibility,
   adoption and growth." is now a fragment in its own right, a prepositional
   phrase with no finite main verb. Set as directed, raised with the user. */


/* --------------------------------------------------------------------------
   44. Hero — Option B, the centred dark lockup

   This supersedes §23's full-bleed treatment on index.html. §23 stays in the
   file: its BAND fallback is still the record of how the oak plate was tuned,
   and about.html's hero still runs off the shared .hero-* rules. But every rule
   of §23 that painted a scrim or positioned the media on index is overridden
   here.

   WHAT CHANGED, AND WHY NONE OF IT IS OPTIONAL

   The plate is a different photograph, not a different crop. hero-lens.* was a
   lens on an oak tabletop lit from the front: 112/255 mean, which is why the
   comp's dark-on-light typesetting survived it and why §23 spent two pale
   scrims lifting the wood under the copy. hero-dark.* is 33/255 mean. On that
   ground:

     - #111111 display ink and #262626 labels are invisible. The tokens flip.
     - The two lifting scrims have no job. A pale scrim heavy enough to matter
       would now be a panel laid over a black photograph.
     - The fixed header's --navy mark and --t1 wordmark are invisible too, so
       the header's ink flips with the hero's — see the end of this section.

   THE LENS USED TO BE OFF-CENTRE IN THE PLATE, AND IT IS NOT ANY MORE.

   In the source photograph the solid object (rim and glass, not the bright
   coating) spans x 53.6%-80.1%, centre 66.86%. Option B's composition puts it
   on the frame's centre line, and centring an off-centre subject inside a
   `cover` box cannot be done with object-position: that maps the image's p point
   onto the box's p point, so the fraction which centres the subject depends on
   how much overflow there is, which depends on the viewport's aspect. One value
   is right at one aspect and wrong at every other. The first pass shipped the
   lens 18.1% off centre at every viewport under its gate.

   The workaround was a wrapper 151.4% of the hero wide - a floor forced by
   geometry, since covering the frame AND centring a subject at 66.86% needs
   W >= 50/(1-0.6686) = 150.9%. It centred the lens exactly, and it was still
   wrong, because it scaled the plate with the hero's WIDTH while the hero grows
   with the window's HEIGHT. Measured at 3799x1961: the object came out 947px
   tall, half the hero, cropped 133px at the top, with a 500px void between it
   and the lockup.

   So THE PLATE was fixed instead of the CSS. `hero-centred.*` has the ground
   extended rightwards to 3680 so the lens lands on exactly 50%, and 400px
   cropped off the top so the object sits high enough to clear the lockup. See
   tools/build_hero_photo.py for both operations and why neither is a mirror or
   a guess.

   What that buys, and it is worth stating because it deleted three rules and a
   media query:

     - The lens is centred because it is centred IN THE PICTURE. No wrapper, no
       translateX, no aspect-dependent percentage.
     - `cover` matches HEIGHT at any box aspect below the plate's own 3.24, which
       is every real viewport, so there is no vertical overflow and the object
       holds a CONSTANT fraction of the hero's height everywhere: measured
       18.0%-52.6% of the plate, and therefore of the hero.
     - Above aspect 3.24 (an ultra-wide desktop) `cover` matches width instead
       and crops vertically; object-position 50% keeps it centred and it still
       clears the lockup - checked at 3840x1080.

   The object's measured extent on the shipped plate: x 41.0%-59.9%, centre
   50.4%; y 18.0%-52.6%. Those two y numbers are what the lockup's placement is
   set against, and they are the ones to re-measure if the plate is ever rebuilt.
   -------------------------------------------------------------------------- */

.hero-centred.hero-photo {
  /* §8 sets --paper-alt on .hero and §23 repeats it on .hero-photo, both for a
     light band that no longer exists here. The ground behind and around the
     plate is ink now. */
  background: var(--ink);
  background-color: var(--ink);
  color: var(--d1);

  /* The lockup's three tiers. The tonal STEP the comp built with #111 against
     #3F3F3F survives, inverted: white against 66% white is the same
     relationship read the other way up. */
  --hero-ink:   var(--d1);
  --hero-grey:  var(--d2);
  --hero-quiet: var(--d3);
}

/* §23's scrim pair. Not lightened — removed. They existed to raise a wooden
   tabletop under dark ink, and there is neither a tabletop nor dark ink here. */
.hero-centred.hero-photo::before { content: none; }

/* --- the plate ---------------------------------------------------------- */

.hero-centred .hero-plate {
  position: absolute;
  inset: 0;
  z-index: -2;
  overflow: hidden;
  pointer-events: none;
}

/* <picture> is inline by default, so it needs a box of its own before the <img>
   inside it can fill one. */
.hero-centred .hero-media,
.hero-centred .hero-media img {
  display: block;
  width: 100%;
  height: 100%;
}

.hero-centred .hero-media {
  /* §23 makes .hero-media the positioned band with a feathered mask. Here it is
     just a box for the picture to be placed inside — but a POSITIONED one, since
     the portrait branch below absolutely positions the <img> within it. */
  position: relative;
  inset: auto;
  height: 100%;
  overflow: hidden;
  -webkit-mask-image: none;
          mask-image: none;
}

.hero-centred .hero-media img {
  object-fit: cover;
  /* Both fractions are 50% and neither is a tuned value. Horizontally the lens
     is centred in the picture, so centring the picture centres the lens.
     Vertically there is no overflow to move at any normal aspect - see the note
     at the top of this section - so this only matters on an ultra-wide desktop,
     where centring is also what is wanted. */
  object-position: 50% 50%;
}

.hero-centred .hero-vignette {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /* Lighting, not a filter, and deliberately single-hue: never the power-map
     spectrum, which is banned from this hero by direction. Reaches nothing
     until 40% of the way out, so the lens and the lockup both sit in clean air.

     OXFORD BLUE since 2026-08-27, was `--ink` (7, 1, 23). Directed: use "the
     slightly lighter dark blue colour in the palette", identified by the user
     as the colour already behind the closing strapline — which is `--navy`,
     #0A1A4A. Read through the token so it cannot drift from the strapline.

     Consequence to know: Oxford Blue is LIGHTER than the ink it replaced
     (relative luminance 0.0128 against 0.0053), so the frame's edges are less
     dark than before, not more. The hero's own type is white and its tightest
     element — the scroll cue — sits low in the frame where the ramp is
     strongest, so this is a contrast-relevant change, not only a hue one.
     Re-run `check_hero_photo.js` after touching these stops. */
  background: radial-gradient(ellipse 66% 84% at 50% 28%,
    rgba(var(--navy-rgb), 0)    0%,
    rgba(var(--navy-rgb), 0)    40%,
    rgba(var(--navy-rgb), 0.5)  76%,
    rgba(var(--navy-rgb), 0.88) 100%);
}

/* --- the lockup -------------------------------------------------------- */

.hero-centred .hero-inner {
  /* 96svh, against §8's shared 76svh. Option B was composed on a 1440x860 frame
     - a hero 0.597 of its own width - and 76svh of a 900px window is 684, only
     0.475. The lockup and the lens were sharing a third less height than the
     composition was drawn for. §8's value is left alone because about.html
     shares it and has no photograph to make room for. */
  min-height: 96svh;

  /* THE LOCKUP IS PLACED BY svh, NOT PINNED TO THE BOTTOM, and that is the
     second half of the large-screen fix.

     Pinned to the bottom (align-self: end) the lockup sat a fixed number of
     PIXELS above the hero's lower edge while the object sat at a fixed FRACTION
     of its height, so the gap between them grew with the window: about 500px of
     void at 3799x1961. Anchored at 56svh instead, both the object and the lockup
     are proportional to the same thing and the composition is identical at every
     viewport.

     56svh of a 96svh hero is 58.3% of it. The object's lower rim is at 52.6%, so
     the clearance is 5.7% of the hero - 49px at a 900px window, 107px at 1961 -
     and it never inverts. This supersedes §8's
     `padding-top: calc(var(--header-h) + 4vw)`; at 900px, 56svh is 504px, well
     clear of the 79px header. */
  padding-top: 56svh;

  /* min(2vw, 3.2svh), not a flat 2vw. On a short ultra-wide viewport 2vw is
     77px, and that alone was enough to push the hero's content past its own
     96svh: the hero then stretched, and because the object is a FRACTION of the
     hero's height it moved down with it and ate the clearance under the lens -
     2px at 3840x1080. Anything that adds height on a wide-but-short screen has
     to be capped in svh for the same reason. */
  padding-bottom: min(2vw, 3.2svh);
}

.hero-centred .hero-title-block {
  /* ONE KNOB, as in §8c, so the eyebrow, headline, outcome lines and the gaps
     between them scale together. 2.5vw is 36px at 1440, the artboard's headline
     size.

     The ceiling was 4.2rem, and that was the other half of what went wrong on a
     large screen: the type stopped growing at 1680px of viewport while the frame
     carried on to 3799, so the lockup became a small island in a very large
     hero. 7.2rem lets it keep pace. 1440 is unaffected - 2.5vw is below both
     ceilings there - so the approved composition is untouched. */
  /* min(2.5vw, 4.6svh): the width term is the composition, the HEIGHT term is a
     safety valve. On a short ultra-wide desktop (3840x1080) the width term alone
     gave --hl 72px, the lockup grew past the space between its 56svh anchor and
     the hero's own 96svh, the hero stretched to fit it, and because the object
     is a fraction of the hero's height it moved down with it and collided -
     measured 45px of overlap. Capping by svh keeps the lockup inside the space
     it is anchored in. No normal viewport is touched: at 1440x900 the width term
     is 36px against the height term's 41px, at 1920x1080 it is 48 against 50. */
  /* Directed 2026-08-27: "reduce the size of the fonts for the hero text, keep
     the ratio between the two sizes roughly the same."

     A multiplier rather than three smaller numbers inside the clamp, because
     every term in there carries its own measured reason - 2.5vw IS the
     artboard's 36px at 1440, 4.6svh is the collision valve, 7.2rem is the
     large-screen ceiling - and editing them in place would leave the comments
     above describing values that no longer exist. This keeps all of it true and
     puts the reduction in one legible place, which is also how the sibling
     block in section 8c does it.

     The ratio needs no attention: the headline is `var(--hl)` and the eyebrow is
     `calc(var(--hl) * 0.361)`, so anything done to --hl moves both by the same
     factor and 0.361 is preserved exactly. The eyebrow's own 1.1rem floor is the
     only thing that could break that, and it does not bite until --hl drops
     below 30.5px - well under the clamp's 2.6rem base.

     0.88 at 1920: headline 48px -> 42.2px, eyebrow 17.3px -> 15.2px.

     THE max() IS NOT DECORATION. The clamp's 2.6rem base is a floor somebody
     chose, and on a phone --hl already sits on it - scaling the whole expression
     pushed the mobile headline to 22.9px, under a bound that exists to stop
     exactly that. max() lets the reduction take effect only where there was
     headroom and leaves every viewport that was already at the floor exactly as
     it was tuned.

     One honest limit on "keep the ratio". It is exact from about 1385px of width
     upward. Below that the eyebrow meets its own 1.1rem readability floor before
     the headline meets its, so the gap closes a little - 0.361 becomes 0.39 at
     1280 and 0.42 on a phone. That floor is deliberate (see the eyebrow rule
     below) and readability beats proportion at that size; it was already
     happening on phones before this change, at a slightly smaller viewport. */
  --hl-scale: 0.88;
  --hl: max(2.6rem, calc(clamp(2.6rem, min(2.5vw, 4.6svh), 7.2rem) * var(--hl-scale)));

  /* align-self: START, not end. The svh anchor above does the positioning now;
     `end` would push the block back down to the hero's lower edge and undo it. */
  align-self: start;
  align-items: center;
  gap: calc(var(--hl) * 0.61);      /* 22px at --hl 36px */
  padding-block: 0;

  /* The measure has to grow with --hl or the eyebrow wraps. It is the widest
     thing in the lockup by some way - tracked caps, about 11 x --hl wide - so at
     --hl 72 it needs 792px and a fixed 64rem would break it. `max()` keeps 1440
     at exactly its approved 640px, because 14 x 36px is less than 64rem there. */
  width: 100%;
  max-width: min(92%, max(64rem, calc(var(--hl) * 14)));
  margin-inline: auto;
  text-align: center;
}

/* THE TWO SMALL TIERS, ONE DECLARATION. The lockup is three groups since
   2026-08-27 (fifth pass) — small setup line, display tier, small payoff — and
   the two small groups are .hero-eyebrow and .hero-outcome.

   THEY ARE GROUPED HERE ON PURPOSE. Before the display tier moved into the
   middle, these were two lines of one element, because as two separately-tuned
   elements they had DRIFTED: 0.361em against 0.333em of --hl and line-height 1.4
   against 1.5, which is close enough to look like a mistake rather than a
   distinction, and it was reported as one. The direction was that they be "part
   of the same text group, same font, size, colour etc. and they should animate in
   the same way." With the display tier between them they cannot be one element,
   so they are one RULE instead — the invariant is preserved by construction, not
   by hand. .hero-outcome's own values are gone; do not write them back.

   The eyebrow's numbers won, not the outcome's: 0.361em with line-height 1.4 is
   what the merged group actually shipped and what the owner has been looking at,
   and the 1.1rem floor below is a readability bound the outcome never had.

   If one of them ever genuinely needs to differ, add a single-selector override
   AFTER this rule. Do not split this back into two full declarations. */
.hero-centred .hero-eyebrow,
.hero-centred .hero-outcome {
  font-family: var(--font-ui);
  /* A floor of its own. At the clamp's bottom --hl is 26px, which would put this
     line at 9.4px - smaller than anything else on the site and not readable at
     arm's length on a phone. */
  font-size: max(1.1rem, calc(var(--hl) * 0.361));
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  text-align: center;
  color: var(--hero-grey);
  margin: 0;

  /* THIS LINE IS THE FIX FOR A REAL BUG, not tidying. §8 sets
     `align-self: start` on .hero-eyebrow for the grid it was written against.
     .hero-title-block is a FLEX COLUMN, where align-self controls the CROSS axis
     - horizontal - so `start` silently left-aligned the eyebrow inside a centred
     lockup while the headline and outcome lines stayed centred. It measured
     exactly (blockWidth - eyebrowWidth) / 2 off centre: -121.5px at 1440,
     -88.4px at 1920 and above. Setting grid-column/grid-row to auto, as this
     rule already did, does not help - align-self is not a grid property. */
  align-self: center;

  /* Belt and braces: these are inert in a flex column but were left over from
     §8's grid placement and read as if they were doing something. */
  grid-column: auto;
  grid-row: auto;
}

.hero-centred .hero-title {
  font-family: var(--font-display);
  font-size: var(--hl);
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--hero-ink);
  text-align: center;
  max-width: none;                  /* §8's 18ch would re-wrap authored lines */
  text-wrap: nowrap;
}

/* §8c's per-line indents are NOT scoped to .hero-left, even though that section
   claims to be scoped throughout, so they reach this lockup and would stagger a
   centred stack. Zeroed here rather than in §8c, which is regenerated by
   tools/build_hero_css.py and would put them straight back. */
.hero-centred .hl-1,
.hero-centred .hl-2,
.hero-centred .hl-3 { margin-left: 0; }

/* The comp's outlined ampersand, drawn by .hl-3::before from a data URI with
   fill="#111" BAKED INTO THE PATH — §8c's own comment warned it "will not
   follow --hero-ink if that changes". --hero-ink is white now, so that vector
   would paint near-black on near-black and vanish. The character is back in the
   markup; this suppresses the vector. */
.hero-centred .hl-3::before { content: none; display: none; }

/* .hero-centred .hero-outcome's declaration is NOT here. It shares the
   .hero-centred .hero-eyebrow rule above — see that rule's comment for why.
   §8's unscoped `.hero-outcome` (margin-left/margin-top staircase, 0.3333em,
   line-height 1.375, letter-spacing 0) still reaches this lockup and is
   overridden there by higher specificity. */

.hero-centred .ho-1,
.hero-centred .ho-2,
.hero-centred .ho-3 { margin-left: 0; }

/* The two small groups' authored lines. `display: block` is the whole rule for
   .he-line; .ho-line already gets it from §8, and is named here so the two
   groups' line behaviour reads in one place rather than half here and half 3500
   lines up.

   Everything else — face, size, weight, 0.18em tracking, uppercase, centring,
   --hero-grey — is inherited from the grouped .hero-eyebrow/.hero-outcome rule
   above. That is the point: no second declaration that can disagree. */
.hero-centred .he-line,
.hero-centred .ho-line { display: block; }

/* AUTHORED BREAKS STAY AUTHORED. Every line in the two small groups is a span in
   the markup, and this stops the browser adding a break of its own inside one.

   ORIGINALLY this was one directed fix on one line: "make it such that
   experiences is not on a separate line." The measured cause was not the copy —
   .hero-title-block caps at `min(92%, max(64rem, --hl * 14))`, and the merged
   setup group's 56-character line needed ~679px against a 672px cap at 1920, so
   it outgrew the block by about 1% and wrapped at 1920, 2560 and 3840, stranding
   one word.

   THAT PRESSURE IS GONE and the rule is kept anyway. Since the fifth pass the
   setup line is only "The OO-Adaptive Optics Platform enables" — 38 characters,
   about 464px at 1920 in a 640px block — and the 56-character clause it used to
   carry is in the display tier now. So this is no longer holding a line against
   its own cap; it is holding all four small lines against a re-wrap, which is
   what open item M is about: a 7px margin is the same knife edge whether it is
   the cap or a platform's font metrics that closes it.

   NOT `text-wrap: balance`, ever, on any of these — it picks two lines over one
   even when one fits, and this stylesheet has been bitten by that.

   GATED at 834 and up, and the gate is load-bearing for the ONE line that can
   still overflow: at 390 the content block is 313px. The payoff lines and the
   setup line all fit there, but nowrap on a line that does not fit pushes the
   hero into horizontal scroll, so the gate stays rather than being narrowed to
   the lines that provably need it. Below 834 they wrap freely, and the &nbsp; in
   the display tier's second line is the phone-side half of the same job. */
@media only screen and (min-width: 834px) {
  .hero-centred .he-line,
  .hero-centred .ho-line { white-space: nowrap; }
}

/* --- the scroll cue ---------------------------------------------------- */

/* §23 hides this in the whole band family and shows it only inside its own
   full-bleed query, because on the oak plate the bottom-left corner was the
   shadowed near edge of the desk and --t3 measured 1.8:1 there. That reasoning
   was about a photograph this hero no longer uses: here the cue is centred, on
   ground the vignette has taken to near-black, and it holds at every width. */
.hero-centred .hero-scroll {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.8rem;
  font-size: calc(var(--fs-mono) * 1.05);
  letter-spacing: 0.14em;
  color: var(--hero-quiet);
}

.hero-centred .hero-foot {
  grid-column: 1 / -1;
  justify-self: center;
  /* §8 zeroes .hero-inner's row-gap above 834px, and row 1 collapses because the
     eyebrow lives inside the lockup — so the foot ends up hard against the last
     outcome line with nothing between them. This is that gap, put back on the
     one row that needs it rather than on the grid, which would also push the
     lockup down. */
  padding-top: clamp(1.6rem, 2vw, 3.6rem);
}

/* NOTE ON WHAT IS NOT HERE. There used to be a
   `@media (min-width: 834px) and (min-aspect-ratio: 87/100)` block holding a
   151.4%-wide wrapper, plus a portrait branch that sized the picture by height
   and slid it with translateX(-66.86%). Both existed only to centre an
   off-centre subject, and both went when the plate was rebuilt with the lens
   centred. If either is ever reintroduced, read the top of this section first:
   the gate was also wrong (13/10 put 1280x1024 into the branch that could not
   centre it at all), and the whole approach breaks on a large screen. */

/* --- the header over a dark hero --------------------------------------- */

/* Scoped to the PAGE, not to the hero, and this is the one thing here that
   cannot be scoped to .hero-centred: the header is a fixed bar and a SIBLING
   BEFORE <main>, so no descendant or sibling selector starting at the hero can
   reach it. `.hero-photo ~ .header` is already in this file (§41) trying to do
   exactly that, and has never matched anything for the same reason.
   .has-dark-hero sits on <body> and is index-only, so about.html — whose hero is
   a pale photographic ground carrying dark ink — is untouched. */
.has-dark-hero .header:not(.is-stuck) .brand,
.has-dark-hero .header:not(.is-stuck) .brand-name,
.has-dark-hero .header:not(.is-stuck) .nav-link { color: var(--d1); }

.has-dark-hero .header:not(.is-stuck) .nav-index { color: var(--d3); }
.has-dark-hero .header:not(.is-stuck) .nav-item:hover .nav-index { color: var(--d1); }
.has-dark-hero .header:not(.is-stuck) .burger span { background: var(--d1); }

/* §41 darkens .nav-index to 0.78 ink on any header that is not stuck — tuned
   for the oak plate, and the wrong direction entirely here. The override above
   wins on SPECIFICITY (it carries the body class) rather than merely on order,
   which is worth stating because the two rules read as contradictory sitting
   300 lines apart.

   Once the header takes its own opaque white background at 40px of scroll it is
   back on a known light surface and the site's normal inks apply, which is what
   .is-stuck already does. Nothing to add for that state. */


/* --------------------------------------------------------------------------
   46. Legal pages — privacy policy, cookie policy
   --------------------------------------------------------------------------

   Added 2026-08-27 on direction: "We need to add the equivalent of pages and
   links in the footer", pointing at the current live site's two CookieYes-
   generated policies.

   These pages are deliberately the plainest thing on the site. No hero, no
   photographic ground, no pinned runway, no reveal motion beyond the shared
   fade. Everything this stylesheet does elsewhere is in service of an argument;
   a privacy policy is a reference document, and the job is to make a wall of
   text navigable and readable. So this section is mostly measure, hierarchy and
   vertical rhythm, and it introduces no new colour and no new face.

   ON THE SECTION NUMBER: this is 46 because 45 was the last one used — but note
   that §44 is used TWICE in this file, once for the Applications lock point
   (~line 3752) and once for the hero's Option B lockup (~line 3970). Both are
   referenced as "§44" in HANDOVER.md and README.md, which now resolve
   ambiguously. Not renumbered here — renumbering would invalidate citations in
   four documents — but it wants fixing deliberately.
   -------------------------------------------------------------------------- */

/* The fixed header overlays the top of the viewport, and unlike every other
   page on the site there is no hero here to absorb it. --header-h plus a
   breathing interval, in vw so it tracks the page's other top intervals. */
.legal-head {
  grid-column: 1 / -1;
  padding-top: calc(var(--header-h) + 14vw);
  padding-bottom: 6vw;
  border-bottom: 1px solid var(--rule);
  margin-bottom: 6vw;
}

@media only screen and (min-width: 834px) {
  .legal-head {
    grid-column: 2 / 6;
    padding-top: calc(var(--header-h) + 6vw);
    padding-bottom: 3.2vw;
    margin-bottom: 4vw;
  }
}

/* --fs-h2, not the h1 scale the homepage uses. A policy title is a label, not
   a claim, and the 76px display tier would be shouting a filing cabinet. */
.legal-title {
  font-size: var(--fs-h2);
  line-height: 1.06;
  letter-spacing: -0.025em;
  color: var(--t1);
  margin: 0;
}

/* Two mono lines under the title: the effective date and the last-updated date.
   These matter legally — a reader has to be able to tell which version they are
   looking at — so they get their own role rather than being folded into the
   first paragraph where they would be missed. */
.legal-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.8rem 2.4rem;
  margin-top: 2.4rem;
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--t3);
}

/* 52rem, NOT the site's .measure (62rem). The typography audit
   (.impeccable/typeset/2026-08-17…md) records .prose.measure running 77-88
   characters per line across all 19 body paragraphs and calls it a P1 finding.
   62rem is that measure. Legal prose is read in long unbroken runs by someone
   who does not want to be there, so this is capped at ~65 characters instead of
   inheriting a measure that is already flagged as too wide. */
.legal-body {
  grid-column: 1 / -1;
  max-width: 52rem;
  padding-bottom: 12vw;
}

@media only screen and (min-width: 834px) {
  .legal-body { grid-column: 2 / 6; padding-bottom: 6vw; }
}

.legal-body h2 {
  font-size: var(--fs-lead);
  line-height: 1.18;
  letter-spacing: -0.02em;
  color: var(--t1);
  margin: 6.4rem 0 0;
}

.legal-body h2:first-child { margin-top: 0; }

.legal-body h3 {
  font-size: var(--fs-body);
  line-height: 1.35;
  color: var(--t1);
  margin: 4rem 0 0;
}

/* font-weight is declared explicitly on both ranks. The UA default for h2/h3 is
   bold, and this site sets one weight everywhere — .row-title shipped bold for
   weeks for exactly this reason (§31c), so it is stated here rather than left
   to inherit. */
.legal-body h2,
.legal-body h3 { font-weight: 400; }

.legal-body p,
.legal-body ul,
.legal-body ol { color: var(--t2); margin: 1.6em 0 0; }

.legal-body h2 + p,
.legal-body h3 + p { margin-top: 1.6rem; }

.legal-body a {
  color: var(--t1);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.25em;
}

.legal-body strong { color: var(--t1); font-weight: 400; }

.legal-body ul,
.legal-body ol { padding-left: 2.4rem; }
.legal-body li + li { margin-top: 0.8rem; }

/* A definition list is the right element for "cookie name — what it does", and
   it is what a pasted CookieYes table degrades to gracefully. */
.legal-body dt { color: var(--t1); margin-top: 1.6rem; }
.legal-body dd { color: var(--t2); margin: 0.4rem 0 0; }

/* — The cookie table —
   Whether this is CookieYes-rendered or hand-pasted, it is a wide table inside
   a 52rem column, so it scrolls in its own box. The page body must never scroll
   horizontally; that is checked on every other page and holds here too. */
.legal-table-wrap {
  margin-top: 2.4rem;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.legal-body table,
.legal-table-wrap table {
  border-collapse: collapse;
  width: 100%;
  min-width: 46rem;
  font-size: var(--fs-small);
}

.legal-body th,
.legal-body td {
  text-align: left;
  vertical-align: top;
  padding: 1.2rem 1.6rem 1.2rem 0;
  border-bottom: 1px solid var(--rule);
}

.legal-body th {
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  font-weight: 400;
  color: var(--t1);
  white-space: nowrap;
}

.legal-body td { color: var(--t2); }

/* — The in-page consent control —
   `.legal-cta` dresses the "Change cookie settings" button on the cookie
   policy. It exists because this stylesheet has NO general button component:
   §7's bare `button { }` reset is all there is, so an undressed button here
   would render in the platform UI face at the platform's own size.

   It is a text button, not a filled one. The only other interactive things on
   these pages are links, and a filled button would read as the page's primary
   action when it is a secondary control on a reference document. So it borrows
   `.legal-body a`'s treatment and differs only in cursor and alignment.

   `font: inherit` first, and it is load-bearing: a UA button does NOT inherit
   font-family or size. This is the same leak that shipped `.row-title` bold for
   weeks and that `.acc-num` still carries.

   The button also carries `.cky-banner-element`, which is CookieYes's own hook
   and supplies no styling of its own — so if this rule goes, the control goes
   back to looking like a form widget on a policy page. */
.legal-cta {
  font: inherit;
  color: var(--t1);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.25em;
  transition: color var(--dur) var(--ease);
}

.legal-cta:hover { color: var(--t2); }

/* — The CookieYes insertion point —
   A container the consent script fills at runtime. Styled ONLY while :empty, so
   the dashed placeholder and its label are visible to whoever is implementing
   it and vanish by themselves the moment the script injects anything. That is
   the whole reason it is written as :empty rather than as a class someone has to
   remember to remove — the live oolabs.co.uk cookie policy currently ships a
   "Types of cookies we use" heading with nothing under it and refers to a
   "Cookie Settings" button that is not on the page, which is precisely the
   failure this avoids reproducing. */
.legal-embed:empty {
  display: block;
  margin-top: 2.4rem;
  padding: 2.4rem;
  border: 1px dashed var(--rule);
  border-radius: var(--r-sm);
}

.legal-embed:empty::before {
  content: attr(data-placeholder);
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--t3);
}

/* — Print —
   A policy is the one thing on this site somebody genuinely prints or saves to
   PDF, usually to keep a record of the version they agreed to. Drop the fixed
   header's reserved space, drop the footer chrome, and put link targets inline
   so a paper copy is still usable. */
@media print {
  .header,
  .mobile-nav,
  .skip-link,
  .footer-brand,
  .footer-legal { display: none; }

  .legal-head { padding-top: 0; }

  .legal-body,
  .legal-head { grid-column: 1 / -1; max-width: none; }

  .legal-body a::after {
    content: " (" attr(href) ")";
    font-family: var(--font-mono);
    font-size: 0.9em;
  }

  .legal-embed:empty { display: none; }
}


/* --------------------------------------------------------------------------
   47. Alternating copy alignment on About — TRIAL, 2026-08-27
   --------------------------------------------------------------------------

   Directed, to see how it looks: "left aligning the text in 'Vision without
   compromise.' to fit with the image", then "make the section below right
   aligned so that the alignment is varied through the page".

   Worth stating because the wording could mislead the next reader: the mission
   copy was ALREADY text-align: left, and every child already shared one left
   edge at 667px of 1440 — §40 exists precisely to make that true. What moved is
   the COLUMN, from the right half of the grid to the left.

   TO REVERT: drop `copy-left` from #mission and `copy-right` from #next in
   about.html, and restore #mission's `section-photo-right`. This whole section
   can then be deleted.
   -------------------------------------------------------------------------- */

/* #mission's copy moves LEFT. Removing `.section-photo-right` from the markup
   does most of it — the column goes from tracks 3/6 back to the default 2/5,
   and §30's scrim ramp flips from the 265deg copy-right shape to the 95deg
   copy-left one, which is correct and automatic: the scrim should always be
   heaviest under the type.

   But §40's `.hero-merged > .container > .col-main` also carries
   `margin-inline-start: auto`, which flushes the column right INSIDE whichever
   tracks it occupies. That was written when the copy was on the right, and left
   alone it would pin a left-hand column to the right edge of tracks 2-5. Zeroed
   here rather than edited in §40, so the trial is one class and reverts cleanly. */
.hero-merged.copy-left > .container > .col-main { margin-inline-start: 0; }

/* A generic right-hand copy column, for a section with NO photograph.
   §30's `.section-photo-right` already does the grid half of this, but it is a
   photographic modifier — it also selects the scrim's direction and the paper
   ground's token overrides — so putting it on a flat section would be a lie in
   the markup even though it happens to lay out correctly. Hence a plain
   alignment modifier that says only what it does.

   Gated at 834 like the default placement it overrides: below that everything
   is one column and there is no left or right to alternate. */
/* FIRST ATTEMPT MOVED ONLY THE HEADING, and that is why it read as broken
   rather than as right-aligned. `#next`'s container has TWO grid children, not
   one: `.col-main` holding the h2, and a `.split` holding the copy AND the
   AR-to-VR line figure. Moving `.col-main` to 3/6 shifted the heading right and
   left everything under it where it was — a heading detached from its own
   content. Measured: heading at 706, copy still at 428, at 1920.

   So a section is right-aligned only when every one of its grid children moves.
   Right-aligning a component means mirroring it, not nudging one box. */
@media only screen and (min-width: 834px) {
  /* The split spans the whole content block and cannot move — so it MIRRORS
     instead: the figure takes the left half, the copy the right. This is what
     the deleted `.split-flip` did (§24's tombstone records it used `order` for
     exactly this), rebuilt here scoped to the modifier rather than reviving a
     class that was removed on purpose. */
  .copy-right > .container > .split > .split-copy   { order: 2; }
  .copy-right > .container > .split > .split-figure { order: 1; }

  /* 4 / 6, NOT 3 / 6. The split's right-hand column begins exactly where grid
     track 4 begins — at 1920 both are 983px, because the split's two columns
     are halves of tracks 2-5 and its gutter is the grid's own. So the heading
     and the copy share one left axis. 3/6 put the heading 277px left of the
     copy it belongs to, which is the same defect §40 was written to fix on
     #mission: three different left edges inside one block.

     The cost is a narrower heading — two tracks instead of three — so it takes
     more lines. That is the trade for a shared axis, and it is the right way
     round: a heading may wrap, a heading detached from its copy is a fault. */
  .copy-right > .container > .col-main { grid-column: 4 / 6; }
  .copy-right > .container > .col-wide { grid-column: 2 / 6; }
}
