/* ═══════════════════════════════════════════════════════════════════════════
   CORVEXAL.COM — THE STYLESHEET                                          W1
   Static CSS. No build step, no preprocessor, no dependency.

   Every colour, size, tracking, duration and easing below the :root block is
   a custom property. There is no raw hex, no raw ms and no raw cubic-bezier
   past the token block, and gate.sh proves that rather than trusting it.

   THE SCALE IS MEASURED. THE FACE IS NOT.
   The proportions came out of getComputedStyle on
   references/WebsiteTemplatesFramer/08-vantara-hub.html at 1440x900.
   That template sets Cormorant Garamond. We set Fraunces. The measurement is
   of the geometry; the face carries the brand and is ours.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── THE FACES ─────────────────────────────────────────────────────────────
   Self-hosted, same origin, no third-party request of any kind. The OFL
   requires the licence to travel with the font, so fonts/OFL-*.txt ship
   alongside the woff2 and are not decoration.

   MEASURED: the Fraunces file on disk carries a 400-700 weight axis. 700 is
   the maximum; 800 and 900 render identically to 700 and are therefore never
   written. The DM Sans file carries the same 400-700 axis, which matters for
   exactly one declaration and is flagged where it lands.                   */

@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('fonts/fraunces-normal-400-700-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('fonts/fraunces-normal-400-700-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('fonts/dm-sans-normal-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('fonts/dm-sans-normal-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ═══ THE TOKENS ═══════════════════════════════════════════════════════════
   Everything the site is allowed to say, said once. Below this block the
   stylesheet may only refer; it may not invent.                            */

:root {
  /* ── GROUND AND INK ─────────────────────────────────────────────────────
     Vantara's ground is not white. MEASURED: body background-color resolved
     to rgb(247, 244, 237). Pure white is a lightbox; this is a sheet, and
     the cards placed on it are the pure white, which is what makes them read
     as PLACED ON the page rather than cut out of it.                      */
  --ground:        #F7F4ED;
  --card:          #FFFFFF;
  --ticket-shadow: rgba(69, 59, 51, 0.22);
  --ink:           #121820;   /* MEASURED, 281 text uses in the reference */
  --ink-deep:      #17251E;   /* MEASURED,  25 text uses — the green-black */
  --muted:         #5F6670;   /* MEASURED,  22 text uses */
  --bronze:        #B68A5E;   /* MEASURED,  17 text uses + every metal fill */
  --rule:          rgba(182, 138, 94, 0.30);   /* MEASURED, the hairline */

  /* MEASURED, and this is a defect in the palette as specified rather than a
     preference: --bronze as TEXT on --ground is 2.82:1. That fails AA for
     normal text (4.5) and fails even the large-text floor (3.0), so it fails
     at 13px AND at 64px. This is the same problem EMBER already solved for
     its own amber with --ember-text at foundation.css:221.

     So: --bronze stays exactly as measured and is used for FILLS, hairlines
     and anything decorative. --bronze-text is the same hue darkened until it
     passes, 5.03:1, and it is the only bronze allowed to carry a word.    */
  --bronze-text:   #8A6034;

  /* EMBER's one warm value. foundation.css:218. It is not a second accent
     and it is not interchangeable with bronze. Its budget is three uses on
     the entire site and gate.sh counts them.                              */
  --amber:         #E8A87C;
  --amber-glow:    rgba(232, 168, 124, 0.14);

  /* ── SURFACES ───────────────────────────────────────────────────────────
     MEASURED off the reference: the glass fills are 0.40 for the pill and
     the tertiary action, 0.72 for the card.                               */
  --glass:         rgba(255, 255, 255, 0.40);
  --card-glass:    rgba(255, 255, 255, 0.72);
  --edge:          rgba(18, 24, 32, 0.08);
  --ambient:       rgba(0, 0, 0, 0.15);
  --bloom:         rgba(255, 255, 255, 0.72);

  /* That inset white highlight is not decoration. It is a 2px bloom on the
     inner top edge and it is the reason the surface reads as lit glass
     rather than a grey rectangle. MEASURED on 15 elements in the reference,
     identical in every one. Do not drop it, soften it, or move it.        */
  --shadow-card:   var(--ambient) 0 24px 50px -20px,
                   var(--bloom)   0 2px  20px 0 inset;

  --blur-nav:      8px;    /* MEASURED */
  --blur-btn:      9px;    /* MEASURED */
  --blur-card:     18px;   /* MEASURED */

  /* ── THE FACES ──────────────────────────────────────────────────────────
     Fallbacks are named rather than left to the UA, because a swap to a
     default serif at 86px is a visibly different page and font-display is
     swap by design.                                                       */
  --face-display:  'Fraunces', Georgia, 'Times New Roman', serif;
  --face-ui:       'DM Sans', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;

  /* ── THE SCALE, MEASURED AT 1440x900 ────────────────────────────────────
     Tracking is a function of size, not a brand value: -2.5px at 86px is
     -0.029em, and the same -2.5px at 32px would be a disaster. That is why
     there are six tracking values here and why letter-spacing is never set
     on a parent.                                                          */
  --size-h1:       86px;   --lh-h1:      86px;     --ls-h1:      -2.5px;
  --size-h2:       70px;   --lh-h2:      73.5px;   --ls-h2:      -1.5px;
  --size-num:      64px;   --lh-num:     67.2px;   --ls-num:     -1.5px;
  --size-h3:       32px;   --lh-h3:      36.8px;   --ls-h3:      -0.5px;
  --size-h4:       28px;   --lh-h4:      32.2px;   --ls-h4:      -0.3px;
  --size-lead:     30px;   --lh-lead:    42px;
  --size-body:     20px;   --lh-body:    32px;
  --size-small:    15px;   --lh-small:   24px;
  --size-eyebrow:  13px;   --lh-eyebrow: 13px;     --ls-eyebrow:  1.6px;
  --size-label:    15px;   --lh-label:   15px;     --ls-label:    0.2px;

  --w-display:     700;    /* the axis maximum. 800 and 900 do not exist. */
  --w-mid:         600;
  --w-label:       500;
  --w-body:        400;
  --w-lead:        300;    /* see the note at .lead — the axis floor is 400 */

  /* ── MEASURE ────────────────────────────────────────────────────────────
     Column widths in ch, so they follow the face rather than the viewport. */
  --measure-body:  62ch;
  --measure-h1:    20ch;
  --measure-sub:   44ch;
  --measure-lead:  34ch;

  /* ── STRUCTURE ──────────────────────────────────────────────────────────
     64px is Vantara's measured section padding. 1280px is the measured width
     of its pill, and the content column is set to match so the two share one
     edge at every width.                                                   */
  --band-pad-y:    64px;
  --band-pad-x:    64px;
  --col-max:       1280px;
  --nav-h:         66px;
  --nav-top:       10px;
  --nav-gutter:    20px;
  --radius-pill:   80px;
  --radius-btn:    100px;
  /* THE PLATE — Vantara's scrim, measured off 08-vantara-hub.html. The
     alphas are the reference's; the cream they are mixed into is ours. */
  /* THE ACTION BAR, ported from landing.css. These five are index.html's
     measured values and they are named here so the port is checkable and
     so the two implementations cannot drift apart silently. */
  --act-open:  400ms;
  --act-row:   300ms;
  --act-swap:  200ms;
  --act-quick: 160ms;
  --act-lift:  20px;
  --act-ease:  cubic-bezier(.44, 0, .56, 1);

  --plate-92:      rgba(247, 244, 237, 0.92);
  --plate-55:      rgba(247, 244, 237, 0.55);
  --plate-0:       rgba(247, 244, 237, 0);
  --plate-none:    rgba(0, 0, 0, 0);
  --plate-pad-y:   112px;
  --plate-measure: 30rem;
  --plate-wide:    44rem;
  --radius-card:   24px;
  --radius-tile:   16px;   /* tiles under 260px wide */
  --btn-h:         46px;
  --btn-pad:       14px 28px;
  --gap-btn:       8px;
  --gap-card:      12px;
  --pad-card:      24px;
  --tap-min:       44px;
  --hair:          1px;
  --focus-w:       2px;
  --focus-off:     3px;
  --nudge:         2px;    /* the arrow's travel on hover */
  --dot:           6px;
  --glow:          380px;
  --glyph:         18px;

  /* ── MOTION, MEASURED ───────────────────────────────────────────────────
     MEASURED in the reference: cubic-bezier(0.44, 0, 0.56, 1) at 0.2s on
     colour, 9 uses; cubic-bezier(0.2, 0, 0, 1) at 0.1s on transform, 1 use;
     0.4s on opacity. Soft carries colour, background and shadow. Snap
     carries transform. There is no third easing.                          */
  --ease-soft:     cubic-bezier(0.44, 0, 0.56, 1);
  --ease-snap:     cubic-bezier(0.2, 0, 0, 1);
  --dur-quick:     100ms;
  --dur-base:      200ms;
  --dur-slow:      400ms;
  --dur-instant:   1ms;
  /* ── W3 ─────────────────────────────────────────────────────────────────
     An INK hairline, for the bands that carry amber. --rule is bronze at
     30% and may not share a viewport with amber, so the warm/cold split and
     the action band rule themselves in ink instead. Same weight, same job,
     a colour that is allowed to be there.                                */
  --rule-ink:      rgba(18, 24, 32, 0.12);
  --field-edge:    rgba(18, 24, 32, 0.18);
  --amber-wash:    rgba(232, 168, 124, 0.10);
  --orbit-r:       240px;   /* MEASURED off C07: hub 180, satellites 100 */
  --orbit-hub:     180px;
  --orbit-sat:     100px;

  /* ── THE TERMINAL ───────────────────────────────────────────────────────
     MEASURED off references/WebsiteComponents/C03-code-renderer.html at
     1440x900. This is GitHub Dark, value for value:

       panel      rgb(13,17,23)  ·  1px rgb(48,54,61)  ·  radius 10px
                  shadow rgba(0,0,0,0.18) 0 4px 32px 0
       page       rgb(12,17,23)
       type       Fira Code 12.5 / 20.625   (a 1.65 ratio)
       gutter     rgb(72,79,88) at 11 / 18.15
       lights     11px  ·  rgb(255,95,86) rgb(255,189,46) rgb(39,201,63)
       copy       47x20  ·  bg rgb(33,38,45)  ·  rgb(139,148,158)  ·  radius 5

     THE TERMINAL IS DARK ON A PAPER PAGE, DELIBERATELY. A terminal is dark
     everywhere else a developer has ever seen one, and re-skinning it to
     cream would make the one surface on this site that shows real code the
     only one that looks unfamiliar. The rest of the museum stays on the
     sheet.

     Fira Code is not on this machine and is not fetched — no third-party
     request is made from any page. The stack below is the system monospace
     on each platform, at the reference's measured size and ratio.        */
  /* ── THE PLATE ──────────────────────────────────────────────────────────
     THESE WERE GITHUB DARK. The panel was #0D1117 sitting beside the .note
     cards, which are rgba(255,255,255,0.72) + blur(18px) — two registers on
     one row, and THE-TEARDOWN 3.2 is right that the page argues with itself.

     MEASURED, references/pure-css-glassmorphism-liquid-glass-ui-kit, light
     mode, its largest .glass surface:

         background        rgba(255, 255, 255, 0.62)
         backdrop-filter   blur(18px)          --blur-md
         border            1px rgba(15, 23, 42, 0.14)
         border-radius     32px                --radius-2xl
         box-shadow        0 8px 32px rgba(0,0,0,0.1),
                           inset 0 1px 0 rgba(255,255,255,0.9)

     AND OUR .note ALREADY MEASURES blur(18px) AT radius 24px. The card family
     on this site is already this kit — 18px is the kit's --blur-md exactly,
     and 24px is its --radius-xl. So the plate is not a new treatment being
     introduced. It is the treatment already on the page, applied to the one
     element that never joined it. Where our value differs from the kit's it
     is because .note got there first and the family has to agree:

         alpha    kit 0.62   ours 0.72   .note's, so the two match on a row
         radius   kit 32px   ours 24px   --radius-card, the site's own scale
         shadow   kit's      ours        --shadow-card, .note's

     The border is the one thing the kit has that .note does not, and it is
     kept: a plate holding CODE needs its edge stated, because the content
     inside it is not prose and the eye needs to know where the object stops.
     Hue moved from the kit's slate to our --ink at the same 0.14 alpha.   */
  --term-bg:       var(--card-glass);
  --term-bar:      rgba(18, 24, 32, 0.03);
  --term-edge:     rgba(18, 24, 32, 0.14);
  --term-gutter:   #484F58;
  /* THE REFERENCE'S OWN VALUE IS NOT LEGIBLE, AND SAYING SO IS THE POINT.
     #484F58 is GitHub Dark's gutter, measured off the C03 capture, and it is
     2.28:1 on #0D1117 — a clear AA failure at 11px. It stays declared above
     because the Museum's claim is that these are the reference's real values
     and that claim has to survive inspection.
     But the two places it is RENDERED AS TYPE — the filename in the title
     bar and the line numbers — use the value below instead, at 5.07:1. A
     museum that reproduces an accessibility defect faithfully has chosen
     fidelity over the reader, and this project does not make that trade
     without writing down that it made it. */
  --term-gutter-text: #5F6670;   /* --muted   5.66:1 on the plate */
  --term-name:        #121820;   /* --ink    17.40:1 — the filename is the subject */
  --term-text:     #121820;      /* --ink    17.40:1 */
  --term-tool:     rgba(18, 24, 32, 0.06);
  --term-tooltxt:  #5F6670;      /* --muted   5.66:1 */
  --term-mark:     rgba(232, 168, 124, 0.10);  /* --amber-wash — the lit range */
  --term-shadow:   rgba(0, 0, 0, 0.18);

  /* ── THE SYNTAX PALETTE, DERIVED FROM THIS SITE ─────────────────────────
     These were GitHub Dark's seven, measured off the C03 capture. A navy
     panel wearing VS Code's colours is somebody else's brand sitting inside
     ours, and on a light plate they are also unreadable: #A5D6FF is 1.51:1
     over the glass.

     SEVEN ROLES COLLAPSE TO FOUR, and the four are separated by HUE at
     nearly equal lightness rather than by lightness. That is deliberate.
     Code set in four luminances reads as four levels of importance; code set
     in one luminance and four hues reads as one object with its parts named,
     which is what an exhibit label does. Measured spread: 31° / 163° / 215°
     and neutral — evenly around the wheel, and the warm/cool axis carrying
     keyword-vs-comment is the axis that survives red-green colour blindness.

     Every value is a site token or a stated derivation from one. Contrast is
     measured on the COMPOSITED plate — white 0.72 over the band beneath —
     not on the token, because the plate is translucent and the declared
     colour is never the rendered one:

                            source            cream    sand
       --syn-key   #8A6034  --bronze-text     5.38     5.25
       --syn-str   #3D6157  --ink-deep hue    6.72     6.56
       --syn-com   #5F6670  --muted           5.66     5.52
       --syn-id    #121820  --ink            17.40    16.96

     The two derived values keep their source's hue exactly (150°→163° and
     the ink-deep family) and raise only lightness, until each clears 4.5:1
     at 12.5px on both grounds the plate can sit on.                       */
  --syn-key:       #8A6034;   /* --bronze-text — keywords, at-rules      */
  --syn-str:       #3D6157;   /* --ink-deep's hue — strings and tags     */
  --syn-com:       #5F6670;   /* --muted — comments                      */
  --syn-id:        #121820;   /* --ink — identifiers, numbers, the code  */
  --syn-num:       #45525F;   /* --ink hue at value lightness — numbers   */
  --syn-fn:        var(--syn-str);
  --syn-tag:       var(--syn-str);

  --face-mono:     ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas,
                   'Liberation Mono', monospace;
  --size-code:     12.5px;  --lh-code:    20.625px;
  --size-gutter:   11px;    --lh-gutter:  18.15px;
  --radius-term:   10px;

  /* ── THE SWAP ───────────────────────────────────────────────────────────
     MEASURED off C21-before-after-slider.html: the mechanism is
     clip-path: inset(0 X% 0 0) on the top layer, and the handle is a ONE
     PIXEL white line with no knob at all. Its label pills are 8px at 0.96px
     tracking on rgb(245,243,236).

     The reference is pointer-only. The range input under ours is OUR
     addition, and it is not decoration: a control you can only drag is not
     a control.                                                           */
  /* TWO MEANINGS, ONE NAME — and the dark panel hid it. --swap-line was
     BOTH the box the two panes sit in AND the hairline divider between
     them. On a dark plate both wanted to be near-white and the collision
     never showed. On a light plate the ground must be pale and the divider
     must be dark, so one token cannot serve: the divider vanished. Split. */
  --swap-ground:   var(--card-glass);
  --swap-line:     rgba(18, 24, 32, 0.14);
  --size-tag:      10px;    --ls-tag:     1.2px;

  /* ── THE WEB ────────────────────────────────────────────────────────────
     The tree that cascades results. Built once here, reused for locations,
     products and contributors.                                           */
  --web-line:      rgba(18, 24, 32, 0.18);
  --web-dur:       320ms;
  --reel-hold:     4200ms;   /* each frame holds this long */
  --reel-fade:     1400ms;   /* and crosses over this slowly */
  --radius-reel:   28px;
  --dur-blink:     1s;      /* the terminal caret */



  /* ── THE PARALLAX BUDGET ────────────────────────────────────────────────
     Three depth planes per showcase band and no more, at these three speeds
     and no others. tools/parallax-budget.mjs reads these three tokens by
     name and fails the build if they are not exactly 0.15 / 0.30 / 0.50, so
     the ratio cannot drift by somebody nudging one number.

     The SPAN is a decision, not a measurement: 240px is the travel a plane
     at speed 1.0 would take across a band's whole scroll range, which puts
     the fastest plane at 120px end to end. The speeds are the specified
     ratios of it. Nothing in a data-kind="read" band gets any of this.   */
  --speed-1:       0.15;
  --speed-2:       0.30;
  --speed-3:       0.50;
  --plane-span:    240px;
  --ghost-size:    280px;

  /* ── THE LATTICE ────────────────────────────────────────────────────────
     Read from here by site.js rather than written twice. A colour that
     lives in the stylesheet and again in a script is one name with two
     meanings waiting to disagree.                                        */
  --lattice-line:  rgba(18, 24, 32, 0.12);
  --lattice-node:  rgba(18, 24, 32, 0.28);

  /* ── THE ARRIVAL CLOCK ──────────────────────────────────────────────────
     On a clock, not on scroll. These run unchanged under reduced motion:
     a reader who asked for less motion asked for less motion, not for a
     worse page and not for an instant one.                               */
  --intro-fade:    900ms;
  --intro-line-1:  800ms;
  --intro-line-2:  2000ms;
  --intro-sub:     5000ms;
  --intro-pill:    6500ms;
}

/* ═══ RESET ════════════════════════════════════════════════════════════════ */

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

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--face-ui);
  font-size: var(--size-body);
  line-height: var(--lh-body);
  font-weight: var(--w-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* The page never scrolls sideways. This is a guard, not the fix — the
     layout is built not to need it, and gate.sh measures the real thing.  */
  overflow-x: hidden;
}

h1, h2, h3, h4, p, figure, blockquote { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
img, svg { max-width: 100%; }
/* see landing.css — max-width without height:auto stretches an oversized image */
img { height: auto; }
img { height: auto; display: block; }
a { color: inherit; text-decoration: none; }

/* Never outline: none. The ring is bronze at 2px with a 3px offset, and it
   is the one place bronze may appear over an amber viewport — a transient
   keyboard affordance is not a brand surface. Stated here so the exception
   is deliberate rather than an oversight.                                 */
:focus-visible {
  outline: var(--focus-w) solid var(--bronze);
  outline-offset: var(--focus-off);
  border-radius: var(--focus-off);
}

.skip {
  position: absolute;
  left: var(--nav-gutter);
  top: calc(var(--nav-top) * -8);
  z-index: 100;
  background: var(--card);
  color: var(--ink);
  padding: var(--gap-card) var(--pad-card);
  border-radius: var(--radius-btn);
  box-shadow: var(--shadow-card);
  font-size: var(--size-small);
  transition: top var(--dur-quick) var(--ease-snap);
}
.skip:focus { top: var(--nav-top); }

.vh {
  position: absolute;
  width: var(--hair); height: var(--hair);
  padding: 0; margin: calc(var(--hair) * -1);
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ═══ TYPE ═════════════════════════════════════════════════════════════════
   font-optical-sizing: auto is REQUIRED, not preferred. Fraunces carries an
   opsz axis. MEASURED at 86px: the same string sets 1268.23px with optical
   sizing on and 1476.50px with it off — 14.1% narrower — which is the
   difference between the display cut and body type scaled up.             */

.h1, .h2, .h3, .h4, .numeral {
  font-family: var(--face-display);
  font-optical-sizing: auto;
  color: var(--ink);
  text-wrap: balance;
}

.h1 {
  font-size: var(--size-h1);
  line-height: var(--lh-h1);
  letter-spacing: var(--ls-h1);
  font-weight: var(--w-display);
  max-width: var(--measure-h1);
}
.h2 {
  font-size: var(--size-h2);
  line-height: var(--lh-h2);
  letter-spacing: var(--ls-h2);
  font-weight: var(--w-display);
}
.h3 {
  font-size: var(--size-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--ls-h3);
  font-weight: var(--w-mid);
}
.h4 {
  font-size: var(--size-h4);
  line-height: var(--lh-h4);
  letter-spacing: var(--ls-h4);
  font-weight: var(--w-mid);
}
.numeral {
  font-size: var(--size-num);
  line-height: var(--lh-num);
  letter-spacing: var(--ls-num);
  font-weight: var(--w-display);
  color: var(--bronze-text);
  font-variant-numeric: lining-nums tabular-nums;
}

.eyebrow {
  font-family: var(--face-ui);
  font-size: var(--size-eyebrow);
  line-height: var(--lh-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  font-weight: var(--w-label);
  text-transform: uppercase;
  color: var(--bronze-text);
}

/* MEASURED AND FLAGGED: the DM Sans file on disk carries a 400-700 axis, so
   this 300 resolves to 400 — the same string sets an identical 476.64px at
   both weights. The declaration is kept because the design calls for 300 and
   because it becomes correct the day a 100-1000 axis file is dropped in. It
   is recorded here so nobody later reads 300 and believes they are seeing
   it. Editing an inactive bound is a no-op that reads as a fix.           */
.lead {
  font-family: var(--face-ui);
  font-size: var(--size-lead);
  line-height: var(--lh-lead);
  font-weight: var(--w-lead);
  color: var(--ink);
  max-width: var(--measure-lead);
}

/* TWO BODY PARAGRAPHS IN A ROW HAD NO GAP AT ALL. .body carries no bottom
   margin, so a second one began on the next line and the pair read as one
   block — 25 places across nine pages. Four components had already solved
   it privately (.limit p + p, .arrival__sub p + p, and two on the landing),
   which is the tell that it belonged here and not in any of them. */
.body + .body { margin-top: 1em; }

.body {
  font-family: var(--face-ui);
  font-size: var(--size-body);
  line-height: var(--lh-body);
  font-weight: var(--w-body);
  max-width: var(--measure-body);
}
.body--muted { color: var(--muted); }

.small {
  font-family: var(--face-ui);
  font-size: var(--size-small);
  line-height: var(--lh-small);
  font-weight: var(--w-body);
  color: var(--muted);
}
.small--ink { color: var(--ink); }

/* ═══ STRUCTURE ════════════════════════════════════════════════════════════ */

.band {
  padding: var(--band-pad-y) var(--band-pad-x);
  position: relative;
}
/* SAFE CENTRING, AND THE WORD 'safe' IS THE WHOLE FIX.

   A centred flex item that is TALLER than its container overflows equally in
   both directions — so a hero whose copy grows past 100vh does not simply
   run off the bottom, it also rides up past the padding box and under the
   fixed nav. Measured on /ember-schools: content 893px in an 860px box, the
   eyebrow at y=64 with the nav occupying 10-78. Fourteen pixels of the
   eyebrow were behind the nav, on a page whose first job is to say what year
   the product arrives.

   The safe keyword centres while the content fits and falls back to start the
   moment it does not. It therefore CANNOT change any page that currently
   fits — which is every existing use of this class — and only takes effect
   in the case that was broken. That is why it is preferred here over adding
   padding, which would move heroes that are already correct. */
.band--tall {
  min-height: 100vh;
  display: flex;
  align-items: safe center;
  /* AND THE PADDING HAS TO CLEAR THE NAV, because safe centring only
     guarantees that overflowing content starts at the top of the CONTENT
     box — and the content box began 64px down while the fixed nav ends 78px
     down. Measured across five tall heroes: ember and network have content
     that fits, so they centre and clear by 76px and 93px; the three product
     pages have content 733-797px against a 732px box, so they start-align
     and clear by MINUS fourteen. The eyebrow carrying the release year sat
     under the nav on all three.

     DERIVED FROM THE NAV TOKENS, not from the 110px I measured. --nav-h and
     --nav-top are what the nav is actually built from, so this tracks it; a
     literal would be correct today and silently wrong the first time the nav
     changes height. max() so the band never gets LESS than the section
     rhythm it would otherwise have. */
  padding-top: max(var(--band-pad-y),
                   calc(var(--nav-h) + var(--nav-top) + var(--band-pad-y) / 2));
}
.col {
  max-width: var(--col-max);
  margin-inline: auto;
  width: 100%;
}
.hairline {
  border: 0;
  border-top: var(--hair) solid var(--rule);
  margin: 0;
}

/* THE FIRST BAND MUST CLEAR THE PILL.
   MEASURED at 1440x900: the pill occupies 10px to 76px, and .band opens with
   64px of top padding — so on every page whose first band is not 100vh, the
   eyebrow was rendering underneath the nav and being read as missing. The
   tall bands are flex-centred over a full viewport and were never affected,
   which is exactly why this survived the landing page and /ember.        */
main > .band:first-child:not(.band--tall) {
  padding-top: calc(var(--nav-top) + var(--nav-h) + var(--band-pad-y));
}

/* An in-page anchor must not land under the fixed pill. This is the only
   scroll courtesy on the page, and it changes where a jump LANDS — it never
   touches how fast the reader travels.                                    */
[id] { scroll-margin-top: calc(var(--nav-h) + var(--nav-top) + var(--nav-top)); }

/* ═══ THE PILL ═════════════════════════════════════════════════════════════
   ONE nav, TWO GROUNDS, IDENTICAL GEOMETRY.

   This is the detail that would otherwise have shipped broken.
   backdrop-filter blurs what is behind it, and over a flat sheet there is
   nothing behind it to blur — the glass pill in the reference floats over a
   photograph, which is the only reason its glass is visible there. Put the
   identical values over the sheet and the result is a white pill on a white
   page with a blur doing nothing, and the instinct is to "fix" CSS that is
   not broken.

   So the pill takes a solid ground over the sheet and its own shadow, and
   the reference's measured glass over a showcase section where there is
   contrast to read. ONLY THE GROUND CHANGES. The geometry — 66px tall, 80px
   radius, 10px 14px 10px 24px — is identical in both states, which is what
   keeps this one object instead of two.                                   */

.nav {
  position: fixed;
  top: var(--nav-top);
  left: var(--nav-gutter);
  right: var(--nav-gutter);
  z-index: 50;
  max-width: var(--col-max);
  height: var(--nav-h);
  margin-inline: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--pad-card);
  padding: 10px 14px 10px 24px;
  border-radius: var(--radius-pill);

  /* AT REST — nothing behind it but the sheet */
  background: var(--card);
  border: var(--hair) solid var(--edge);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: var(--shadow-card);

  /* Exactly the two properties the ground change touches. Geometry is not
     transitioned, because geometry does not change.                       */
  transition: background var(--dur-base) var(--ease-soft),
              box-shadow var(--dur-base) var(--ease-soft);
}

/* IN TRANSIT — a showcase section is under it. Set by ONE
   IntersectionObserver in site.js. There is no scroll listener on this page. */
.nav[data-ground="showcase"] {
  background: var(--glass);
  border-color: transparent;
  backdrop-filter: blur(var(--blur-nav));
  -webkit-backdrop-filter: blur(var(--blur-nav));
  box-shadow: none;
}

.nav__mark {
  font-family: var(--face-display);
  font-optical-sizing: auto;
  font-size: var(--size-small);
  line-height: var(--lh-label);
  letter-spacing: var(--ls-eyebrow);
  font-weight: var(--w-display);
  text-transform: uppercase;
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
}

.nav__links {
  display: flex;
  align-items: center;
  gap: var(--gap-btn);
}
.nav__link {
  font-size: var(--size-small);
  line-height: var(--lh-label);
  letter-spacing: var(--ls-label);
  font-weight: var(--w-label);
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding-inline: var(--gap-btn);
  transition: color var(--dur-base) var(--ease-soft);
}
.nav__link:hover,
.nav__link[aria-current="page"] { color: var(--muted); }

/* ═══ THE BUTTON FAMILY ════════════════════════════════════════════════════
   One class, three fills, and there is no fourth anywhere on the site.
   MEASURED in the reference: height 46, radius 100, padding 14/28, gap 8,
   backdrop-filter blur(9px), label 15/15 at weight 500 with 0.2px tracking,
   and an 8px arrow glyph inside an 18px box.                              */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--gap-btn);
  height: var(--btn-h);
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  padding: var(--btn-pad);
  border: 0;
  border-radius: var(--radius-btn);
  font-family: var(--face-ui);
  font-size: var(--size-label);
  line-height: var(--lh-label);
  letter-spacing: var(--ls-label);
  font-weight: var(--w-label);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--dur-base) var(--ease-soft),
              color      var(--dur-base) var(--ease-soft),
              box-shadow var(--dur-base) var(--ease-soft);
}

.btn__arrow {
  width: var(--glyph);
  height: var(--glyph);
  flex: none;
  display: block;
  /* transform only, on the snap curve, at the quick duration. Nothing else
     about the button moves.                                               */
  transition: transform var(--dur-quick) var(--ease-snap);
}
.btn:hover .btn__arrow,
.btn:focus-visible .btn__arrow { transform: translateX(var(--nudge)); }

/* MEASURED: white on --bronze is 3.09:1 and fails AA for a 15px label. Ink on
   --bronze is 5.76:1 and passes. The FILL is the measured brand value and is
   untouched; only the label colour moved, and it moved because the site
   publishes an accessibility statement and that statement has to be true. */
.btn--primary   { background: var(--bronze);  color: var(--ink); }
.btn--secondary { background: var(--ink);     color: var(--card); }
.btn--tertiary  {
  background: var(--glass);
  color: var(--ink);
  backdrop-filter: blur(var(--blur-btn));
  -webkit-backdrop-filter: blur(var(--blur-btn));
}
.btn--primary:hover,
.btn--secondary:hover { box-shadow: var(--shadow-card); }
.btn--tertiary:hover  { background: var(--card-glass); }

/* ═══ THE CARD FAMILY ══════════════════════════════════════════════════════ */

.card {
  display: flex;
  flex-direction: column;
  gap: var(--gap-card);
  background: var(--card-glass);
  backdrop-filter: blur(var(--blur-card));
  -webkit-backdrop-filter: blur(var(--blur-card));
  border-radius: var(--radius-card);
  padding: var(--pad-card);
  box-shadow: var(--shadow-card);
}
/* 16px for tiles under 260px wide. In the reference both radii occur and
   width alone does not predict which — 245px tiles take 16 and 239px tiles
   take 24 — so this is our rule applied consistently, not a measurement.  */
.card--tile { border-radius: var(--radius-tile); }

.card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap-card);
}
.card__foot { margin-top: auto; padding-top: var(--gap-card); }

/* A card that is a link gets the lift; a card that is not, does not. */
a.card {
  transition: background var(--dur-base) var(--ease-soft),
              box-shadow var(--dur-base) var(--ease-soft);
}
a.card:hover { background: var(--card); }
a.card:hover .btn__arrow { transform: translateX(var(--nudge)); }

/* ═══ THE BANDS ════════════════════════════════════════════════════════════ */

/* §1 ARRIVAL — showcase ────────────────────────────────────────────────────
   Nothing in this band but the two lines, the sub-block and the point. The
   reader completes the argument themselves, which is why the line works and
   why nothing may be added to explain it.

   THIS BAND CARRIES NO BRONZE, DELIBERATELY. The pill is fixed and therefore
   shares this viewport with the amber point, so the pill's action is ink and
   not bronze; and §2, the next band, carries no bronze either, so the two
   accents cannot meet at a scroll boundary either. The separation is
   structural, not a matter of care on the day.                            */

#arrival { overflow: hidden; }
.arrival__inner { position: relative; }
.arrival__sub {
  color: var(--muted);
  max-width: var(--measure-sub);
  margin-top: var(--pad-card);
}
.arrival__sub p + p { margin-top: var(--lh-body); }

.arrival__figure {
  position: absolute;
  left: 0;
  bottom: calc(var(--band-pad-y) * -2);
  width: var(--tap-min);
  height: var(--tap-min);
  display: grid;
  place-items: center;
  pointer-events: none;
}

/* ══════ BEGIN AMBER USE 1 OF 3 — the static point in the arrival ═════════
   6px, lower-left third, with a 380px radial behind it at 0.14 alpha. It is
   static: no pulse, no drift, no breathing. On a white field one warm point
   is not a light in the distance, it is a stamp on a document — which is
   the literal thing this page is being asked to be this week.

   gate.sh requires every amber reference on the site to sit inside a marked
   region like this one, and requires that there are never more than three
   regions. The marker is not documentation; it is the mechanism.          */
.arrival__glow {
  position: absolute;
  width: var(--glow);
  height: var(--glow);
  border-radius: 50%;
  background: radial-gradient(circle, var(--amber-glow) 0%, transparent 70%);
}
.arrival__point {
  position: relative;
  width: var(--dot);
  height: var(--dot);
  border-radius: 50%;
  background: var(--amber);
}
/* ══════ END AMBER USE 1 OF 3 ════════════════════════════════════════════ */

/* §2 THE SENTENCE — read ───────────────────────────────────────────────────
   One sentence, alone in the band, and no bronze in it. See the note in §1
   for why the emptiness of this band is load-bearing.                     */
#sentence .col {
  display: flex;
  justify-content: center;
  padding-block: var(--band-pad-y);
}
#sentence .lead {
  text-align: center;
  max-width: var(--measure-sub);
  text-wrap: balance;
}

/* §3 THROUGH §8 ────────────────────────────────────────────────────────────
   Real headings, real type, real measured columns, and no invented prose. A
   heading over an empty column is honest about what has been decided; two
   paragraphs of plausible copy would not be, and would be the first thing a
   reviewer recognises. The arguments arrive in their own sessions.        */

.band__head {
  display: flex;
  flex-direction: column;
  gap: var(--pad-card);
  max-width: var(--measure-body);
}
.band__index {
  display: flex;
  align-items: baseline;
  gap: var(--pad-card);
}

/* .band__action HAD NO RULE AT ALL. ember.html has used it since the page
   was built and /network now uses it too — a paragraph whose only job is to
   hold the section action. With no rule it took the base paragraph margin,
   so the button sat as close to the copy above it as another line of text
   would. It is a sibling of __head and __index and was simply never written.

   Found by tools/audit-unstyled.cjs, which asks which classes the markup
   uses that no stylesheet defines — the only check that can see a feature
   whose CSS half never arrived. */
.band__action {
  display: flex;
  align-items: center;
  gap: var(--pad-card);
  flex-wrap: wrap;
  margin-top: var(--band-pad-y);
}

/* The measured column: the exact width the argument will be set to, drawn
   now as a hairline and a note, so the spacing is real and the copy later
   drops into a shape that already exists rather than one invented around
   it.                                                                     */
.reserved {
  margin-top: var(--band-pad-y);
  max-width: var(--measure-body);
  border-top: var(--hair) solid var(--rule);
  padding-top: var(--gap-card);
}

.split {
  display: grid;
  grid-template-columns: 1fr var(--hair) 1fr;
  gap: var(--band-pad-x);
  margin-top: var(--band-pad-y);
}
.split__rule { background: var(--rule); }
.split__col { display: flex; flex-direction: column; gap: var(--gap-card); }

.doors {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--pad-card);
  margin-top: var(--band-pad-y);
}
.doors .card { min-height: calc(var(--glow) * 0.6); }

/* §9 FOOTER — read ─────────────────────────────────────────────────────────
   Not decoration and not a placeholder. This is the verification surface: a
   legal entity, a physical address, a real address to write to that is not
   a form, and the three policy documents. Absence of these reads as "not a
   real company", which is the exact sentence this page exists to disprove. */

/* THIS ONE LINE WAS FLATTENING THE FOOTER ON EVERY PAGE THAT LOADS site.css.
   It was written for .foot, the previous footer, and the id outlived it: the
   element carrying id="footer" is now .foot2, and #footer is an ID selector,
   so it beat footer.css's own "padding: 92px ... 40px" no matter which sheet
   loaded last.

   MEASURED, seven pages at 1280: index 954px tall with 156px of top padding,
   aria 886/92, and museum, contact, about, privacy and 404 all 794/0 — the
   footer's first line sitting hard against its own top edge, which is what
   reads as clipped. Three different footers on one site.

   Scoped to .foot so it cannot reach .foot2. .foot ships on no page today,
   and the rule stays only because deleting a rule for markup that might
   return is a different decision from stopping it hitting the wrong box. */
.foot#footer { padding-top: 0; }
.foot {
  border-top: var(--hair) solid var(--rule);
  padding-top: var(--band-pad-y);
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--band-pad-x);
  align-items: start;
}
.foot__legal { display: flex; flex-direction: column; gap: var(--gap-btn); }
/* The address is a real mailto and therefore a real touch target. MEASURED
   before this rule existed: 66.9 x 20, which is a link a thumb misses.   */
.foot__legal a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  transition: color var(--dur-base) var(--ease-soft);
}
.foot__legal a:hover { color: var(--bronze-text); }
.foot__nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--pad-card);
  align-items: center;
  justify-content: flex-end;
}
/* min-width as well as min-height: MEASURED at 1440, "Terms" came out 42px
   wide against a 44px floor. The short label is the one that fails.      */
.foot__nav a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  color: var(--ink);
  font-size: var(--size-small);
  line-height: var(--lh-small);
  transition: color var(--dur-base) var(--ease-soft);
}
.foot__nav a:hover { color: var(--bronze-text); }
.foot__base {
  margin-top: var(--band-pad-y);
  display: flex;
  flex-wrap: wrap;
  gap: var(--pad-card);
  justify-content: space-between;
  align-items: baseline;
}

/* ═══ RESPONSIVE ═══════════════════════════════════════════════════════════
   THESE ARE DECISIONS, NOT MEASUREMENTS. The reference was measured at
   1440x900 only, so every value below is ours: the desktop rules above hold
   the measured scale exactly, and these two breakpoints scale it so nothing
   overflows at 768px or 375px. Tracking is restated per size rather than
   inherited, for the same reason it is restated per size above.           */

@media (max-width: 1023px) {
  :root {
    --size-h1:  62px;  --lh-h1:  62px;    --ls-h1:  -1.8px;
    --size-h2:  50px;  --lh-h2:  54px;    --ls-h2:  -1.1px;
    --size-num: 46px;  --lh-num: 50px;    --ls-num: -1.1px;
    --size-h3:  28px;  --lh-h3:  33px;    --ls-h3:  -0.4px;
    --size-h4:  24px;  --lh-h4:  29px;    --ls-h4:  -0.25px;
    --size-lead: 26px; --lh-lead: 38px;
    --size-body: 18px; --lh-body: 30px;
    --band-pad-y: 48px; --band-pad-x: 32px;
  }
  .split { grid-template-columns: 1fr; gap: var(--band-pad-y); }
  .split__rule, .split__rule-ink { height: var(--hair); }
  .doors { grid-template-columns: repeat(2, 1fr); }
  .foot { grid-template-columns: 1fr; }
  .foot__nav { justify-content: flex-start; }

  /* The background plane appears only where the column is wide enough to
     keep it clear of the type, and MEASURED, that is not here.

       1440   ghost 1070-1353, heading text ends at 757   313px clear
        768   ghost  437-721,  heading text ends at 544   107px of OVERLAP
        375   ghost   70-355,  sits directly on the heading, unreadable

     A decorative plane that lands on the one thing the band is for is not a
     depth cue, it is a defect. The band keeps its other two planes.      */
  .ghost { display: none; }
}

@media (max-width: 640px) {
  :root {
    --size-h1:  42px;  --lh-h1:  44px;   --ls-h1:  -1.2px;
    --size-h2:  36px;  --lh-h2:  40px;   --ls-h2:  -0.8px;
    --size-num: 34px;  --lh-num: 38px;   --ls-num: -0.8px;
    --size-h3:  24px;  --lh-h3:  29px;   --ls-h3:  -0.3px;
    --size-h4:  22px;  --lh-h4:  27px;   --ls-h4:  -0.2px;
    --size-lead: 22px; --lh-lead: 32px;
    --size-body: 17px; --lh-body: 28px;
    --size-small: 14px; --lh-small: 22px;
    --band-pad-y: 32px; --band-pad-x: 20px;
    --nav-gutter: 12px;
    --pad-card: 16px;
  }
  .nav { padding: 10px 10px 10px 16px; }
  /* The links collapse rather than crowd. The wordmark and the one action
     survive, which is the whole job of the pill on a phone.

     .nav .nav__links, NOT .nav__links, AND THE EXTRA SELECTOR IS THE FIX.
     This element carries two class systems at once — it is the nav list AND
     the root of the web tree, class="nav__links web__roots". .web__roots is
     declared at line 1864 with display:flex and flex-wrap:wrap, nine hundred
     lines BELOW this rule and at the same specificity, so it won. The links
     never collapsed on any page that uses the web nav.

     Measured at 375px on corvexal-corporate: .nav__links rendered 104px wide
     and 156px TALL, starting at y = MINUS 34 — wrapped into a column that
     spilled out of the 68px pill in both directions and printed "About"
     straight through the page eyebrow underneath it. The rule was right and
     had simply never applied. */
  .nav .nav__links { display: none; }
  .doors { grid-template-columns: 1fr; }
  .arrival__figure { bottom: calc(var(--band-pad-y) * -1); }
  .arrival__glow { width: calc(var(--glow) * 0.63); height: calc(var(--glow) * 0.63); }
}

/* ═══ THE DEPTH PLANES ═════════════════════════════════════════════════════
   Bound to scroll POSITION, never to scroll SPEED, and driven by the
   browser's own view() timeline rather than by a listener. Three
   consequences, all of them the point:

     · there is no scroll listener, because nothing is listening — the
       compositor drives this off the main thread and it cannot jank
     · nothing is ever cancelled or delayed, so the reader's flick travels
       exactly as far as their muscle memory expects
     · where the timeline is not supported the planes simply do not move,
       which is a correct page rather than a broken one

   translate3d ONLY. Never top, never margin, never height. Those relayout
   or repaint on every frame of every scroll, and parallax-budget.mjs fails
   the build on any of them.                                               */

@keyframes plane {
  from { transform: translate3d(0, var(--plane-travel), 0); }
  to   { transform: translate3d(0, calc(var(--plane-travel) * -1), 0); }
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    [data-plane] {
      animation-name: plane;
      animation-duration: auto;
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
      will-change: transform;
    }
    [data-plane="1"] { --plane-travel: calc(var(--plane-span) * var(--speed-1) / 2); }
    [data-plane="2"] { --plane-travel: calc(var(--plane-span) * var(--speed-2) / 2); }
    [data-plane="3"] { --plane-travel: calc(var(--plane-span) * var(--speed-3) / 2); }
  }
}

/* Plane 1 in each showcase band: the section's own number, very large and
   very faint, sitting behind the type. Decorative and aria-hidden — the
   real numeral is in the index row and is read once, not twice.          */
.ghost {
  position: absolute;
  right: 0;
  top: 0;
  font-family: var(--face-display);
  font-optical-sizing: auto;
  font-size: var(--ghost-size);
  line-height: 1;
  font-weight: var(--w-display);
  letter-spacing: var(--ls-h1);
  color: var(--rule);
  pointer-events: none;
  user-select: none;
  z-index: 0;
}
.band--tall .col { position: relative; }
.band--tall .band__index,
.band--tall .band__head,
.band--tall .reserved { position: relative; z-index: 1; }

/* ═══ THE LATTICE ══════════════════════════════════════════════════════════
   One canvas, transparent, with the sheet showing through it. It sits at
   the back of the arrival; the type and the amber point are above it and
   are not part of it.                                                     */

.lattice {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
  pointer-events: none;
}
#arrival .col { position: relative; z-index: 1; }

/* ═══ THE ARRIVAL CLOCK ════════════════════════════════════════════════════
   0.0s  the lattice draws, no type
   0.8s  line 1 fades in over 900ms
   2.0s  line 2
   5.0s  the sub-block
   6.5s  the pill

   fill-mode: both is what holds each element at opacity 0 through its own
   delay. It also means that if this rule is ever removed without removing
   the attribute, the element stays visible rather than disappearing.     */

@keyframes intro-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

[data-line] {
  animation-name: intro-fade;
  animation-duration: var(--intro-fade);
  animation-timing-function: var(--ease-soft);
  animation-fill-mode: both;
}
[data-line="1"] { animation-delay: var(--intro-line-1); }
[data-line="2"] { animation-delay: var(--intro-line-2); }
[data-line="3"] { animation-delay: var(--intro-sub); }
[data-line="4"] { animation-delay: var(--intro-pill); }

.arrival__line { display: block; }

/* An escape hatch, and it is an accessibility fix rather than a nicety.
   The pill is at opacity 0 for the first six and a half seconds while
   remaining in the tab order, so a reader who tabs immediately would be
   moving an invisible focus ring around an invisible nav. Any real input
   before the clock finishes completes it at once.                        */
.intro-done [data-line] {
  animation: none;
  opacity: 1;
}

/* ═══ REDUCED MOTION ═══════════════════════════════════════════════════════
   Scoped deliberately, and NOT a blanket animation kill.

   The depth planes are already inside a no-preference query above, and the
   lattice checks the same query in script and draws a single static frame.
   The arrival's type timeline is deliberately NOT reset here: the brief is
   explicit that it runs unchanged, same steps, same durations. A blanket
   `animation-duration: 1ms !important` would have silently deleted it, and
   the page would have looked correct to anyone not testing with the
   preference on.                                                          */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: var(--dur-instant) !important;
    scroll-behavior: auto !important;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   W3 — THE OTHER FIVE PAGES
   ═══════════════════════════════════════════════════════════════════════════ */

/* Every page carries a static list of the six pages for readers with
   scripting off, because the pill is rendered by script. It is hidden the
   moment the stylesheet loads for everyone else — <noscript> handles the
   rest — and it sits ABOVE the fold in source order so it is the first thing
   a text browser reaches. */
.noscript-nav {
  padding: var(--nav-h) var(--band-pad-x) 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--pad-card);
}
.noscript-nav a { text-decoration: underline; }

/* ── PROSE PAGES ──────────────────────────────────────────────────────────
   /privacy, /terms and /accessibility are read end to end rather than
   scanned, so they get one column at the body measure and nothing else. */

.prose { max-width: var(--measure-body); }
.prose > * + * { margin-top: var(--pad-card); }
.prose h2 {
  font-family: var(--face-display);
  font-optical-sizing: auto;
  font-size: var(--size-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--ls-h3);
  font-weight: var(--w-mid);
  margin-top: var(--band-pad-y);
}
.prose h3 {
  font-family: var(--face-ui);
  font-size: var(--size-label);
  line-height: var(--lh-small);
  letter-spacing: var(--ls-eyebrow);
  font-weight: var(--w-label);
  text-transform: uppercase;
  color: var(--bronze-text);
  margin-top: var(--pad-card);
}
.prose p, .prose li {
  font-size: var(--size-body);
  line-height: var(--lh-body);
  color: var(--ink);
}
.prose ul { display: flex; flex-direction: column; gap: var(--gap-card); }
.prose li { padding-left: var(--pad-card); position: relative; }
.prose li::before {
  content: '';
  position: absolute;
  left: 0;
  top: calc(var(--lh-body) / 2);
  width: var(--gap-btn);
  height: var(--hair);
  background: var(--bronze);
}
.prose a { text-decoration: underline; text-underline-offset: var(--focus-off); }
.prose dl { display: grid; grid-template-columns: 14rem 1fr; gap: var(--gap-card) var(--pad-card); }
.prose dt { font-weight: var(--w-label); }
.prose dd { margin: 0; color: var(--muted); }

/* A fact that is not settled yet is marked, not guessed. Every one of these
   is listed by gate.sh with its line number. */

/* ── /ember · THE WARM/COLD SPLIT ─────────────────────────────────────────
   Two columns, one hairline, and NOTHING IN THIS SECTION MOVES WITH SCROLL.
   The reader crosses between the columns to compare them — that comparison
   IS the section — and differential scroll means one side has moved since
   they left it. Parallax here would destroy the only thing the band is for.

   THE HAIRLINE IS INK, NOT --rule. --rule is bronze at 30%, and this band
   carries amber. Bronze and amber may not share a viewport, so the divider
   takes the ink hairline instead: same weight, same job, a colour that is
   allowed to be here.                                                     */

.split--warm { grid-template-columns: 1fr var(--hair) 1fr; }
/* A SIBLING of .split__rule, not a modifier of it. The first version was
   .split--warm .split__rule, which the cascade resolved correctly and which
   tools/colour-budget.mjs flagged anyway — because the element still carried
   a class that paints bronze, and "depends where it is" is exactly how one
   name comes to carry two meanings. The checker was right.              */
.split__rule-ink { background: var(--rule-ink); }

/* The same idea for the reserved column: a SIBLING of .reserved, in ink, for
   the bands that sit against an amber one. #runs-on on /ember neighbours the
   warm/cold split, and a bronze hairline and an amber column can meet at the
   scroll boundary between two bands even when neither band contains both. */
.reserved-ink {
  margin-top: var(--band-pad-y);
  max-width: var(--measure-body);
  border-top: var(--hair) solid var(--rule-ink);
  padding-top: var(--gap-card);
}
.split__col--cold {
  padding: var(--pad-card);
  border-top: var(--focus-w) solid var(--ink);
}
.split__label {
  font-family: var(--face-ui);
  font-size: var(--size-eyebrow);
  line-height: var(--lh-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  font-weight: var(--w-label);
  text-transform: uppercase;
  color: var(--muted);
}

/* ══════ BEGIN AMBER USE 2 OF 3 — the EMBER column in the warm/cold split ══
   EMBER is the one warm thing this company makes, and this is the only
   place on the site where that is said in colour rather than in words. The
   wash is 0.10 and the top rule is the full value; the type on it stays ink,
   because amber is a ground here and never a text colour.               */
.split__col--warm {
  padding: var(--pad-card);
  background: var(--amber-wash);
  border-top: var(--focus-w) solid var(--amber);
  border-radius: var(--radius-tile);
}
/* ══════ END AMBER USE 2 OF 3 ════════════════════════════════════════════ */

/* ── /ember · WHAT IT RUNS ON ─────────────────────────────────────────────
   The least fashionable claim on the site, stated plainly because it is
   true and because it is the one a school actually needs answered.       */

.runs {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--pad-card);
  margin-top: var(--band-pad-y);
}
.runs__item {
  border-top: var(--hair) solid var(--rule-ink);
  padding-top: var(--gap-card);
  display: flex;
  flex-direction: column;
  gap: var(--gap-btn);
}

/* ── /ember · THE FILE BAND ───────────────────────────────────────────────
   SOURCE: C07-filetype-orbit-upload.html, MEASURED at 768x1024:
     hub 180px at (384, 512) · seven satellites of 100px
     left column x=144, y=392/512/632 · right column x=624, y=332/452/572/692
     radii from the hub 240 to 300px · vertical spacing 120px
     zero animations running · ground is a dark photograph, accent purple

   Re-laid on a true arc rather than two staggered columns, and re-skinned to
   bronze and ink. It is dark in the capture; it is not dark here.        */

.orbit {
  position: relative;
  width: 100%;
  max-width: calc(var(--orbit-r) * 2 + var(--orbit-sat) * 2);
  aspect-ratio: 1;
  margin-inline: auto;
  display: grid;
  place-items: center;
}
.orbit__ring {
  position: absolute;
  inset: var(--orbit-sat);
  border: var(--hair) solid var(--rule);
  border-radius: 50%;
}
.orbit__hub {
  width: var(--orbit-hub);
  height: var(--orbit-hub);
  border-radius: 50%;
  background: var(--card);
  box-shadow: var(--shadow-card);
  display: grid;
  place-items: center;
  font-family: var(--face-display);
  font-optical-sizing: auto;
  font-size: var(--size-h4);
  font-weight: var(--w-display);
  letter-spacing: var(--ls-h4);
  color: var(--bronze-text);
  z-index: 1;
}
.orbit__sat {
  position: absolute;
  width: var(--orbit-sat);
  height: var(--orbit-sat);
  border-radius: 50%;
  background: var(--card-glass);
  backdrop-filter: blur(var(--blur-card));
  -webkit-backdrop-filter: blur(var(--blur-card));
  box-shadow: var(--shadow-card);
  display: grid;
  place-items: center;
  font-family: var(--face-ui);
  font-size: var(--size-eyebrow);
  line-height: var(--lh-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  font-weight: var(--w-label);
  text-transform: uppercase;
  color: var(--ink);
  /* placed by angle, in one declaration, so adding a filetype is one line */
  transform: rotate(var(--a)) translate(var(--orbit-r)) rotate(calc(var(--a) * -1));
}


/* Seven satellites, evenly spaced from the top. The angle is a CSS custom
   property rather than an inline style attribute, because the shipped CSP is
   default-src 'self' with NO unsafe-inline — an inline style would be
   dropped by the browser and the ring would collapse into a stack.      */
.orbit__sat[data-i="1"] { --a: -90deg; }
.orbit__sat[data-i="2"] { --a: -38.57deg; }
.orbit__sat[data-i="3"] { --a: 12.86deg; }
.orbit__sat[data-i="4"] { --a: 64.29deg; }
.orbit__sat[data-i="5"] { --a: 115.71deg; }
.orbit__sat[data-i="6"] { --a: 167.14deg; }
.orbit__sat[data-i="7"] { --a: 218.57deg; }

/* ── FORMS ────────────────────────────────────────────────────────────────
   Four doors, four forms, four endpoints. One "Contact us" makes four
   different visitors write the same wrong email.

   Every control has a real <label>. A placeholder is not a label: it
   disappears the moment the reader types, which is exactly when they need
   to check what the field was asking for.                                */

.form { display: flex; flex-direction: column; gap: var(--pad-card); }
.form__set { border: 0; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--pad-card); }
.form__row { display: flex; flex-direction: column; gap: var(--gap-btn); }
.form__grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--pad-card); }

.form__label {
  font-family: var(--face-ui);
  font-size: var(--size-small);
  line-height: var(--lh-small);
  font-weight: var(--w-label);
  color: var(--ink);
}
.form__hint { color: var(--muted); font-weight: var(--w-body); }

.form__input,
.form__area,
.form__select {
  font-family: var(--face-ui);
  font-size: var(--size-body);
  line-height: var(--lh-body);
  color: var(--ink);
  background: var(--card);
  border: var(--hair) solid var(--field-edge);
  border-radius: var(--radius-tile);
  padding: var(--gap-card) var(--pad-card);
  width: 100%;
  min-height: var(--tap-min);
  transition: border-color var(--dur-base) var(--ease-soft),
              box-shadow  var(--dur-base) var(--ease-soft);
}
.form__area { min-height: calc(var(--tap-min) * 3); resize: vertical; }

/* ── THE MUSEUM: SEARCH THE RECORD ───────────────────────────────────────
   Same geometry as .form__input — this is a field on this site, and a
   second field register would be a second design. It is narrower because
   it sits in a control group rather than a form column, and it keeps the
   44px tap minimum: it measured 21px tall unstyled, which is under half of
   what a thumb needs.                                                    */
.filters__row--find { margin-bottom: var(--gap-card); }
.find {
  font-family: var(--face-ui);
  font-size: var(--size-body);
  line-height: var(--lh-body);
  color: var(--ink);
  background: var(--card);
  border: var(--hair) solid var(--field-edge);
  border-radius: var(--radius-tile);
  padding: var(--gap-btn) var(--gap-card);
  width: 100%;
  max-width: 26rem;
  min-height: var(--tap-min);
  transition: border-color var(--dur-base) var(--ease-soft);
}
.find:hover { border-color: var(--bronze); }
.find::placeholder { color: var(--muted); opacity: 1; }
/* Safari paints its own clear affordance inside a type=search and it lands
   on the border radius. The field is cleared with Escape, which is stated
   nowhere in chrome and everywhere in muscle memory. */
.find::-webkit-search-decoration,
.find::-webkit-search-cancel-button { -webkit-appearance: none; }

/* ── THE ROOM THRESHOLD ──────────────────────────────────────────────────
   A room in a museum is entered, not scrolled past. Each of the three was
   an <h3> and nothing else — the name of the room with no statement of
   what is in it or why these things are together.

   The threshold carries three facts and stops: how many imports are in the
   room, what the room is called, and one sentence saying what the things
   inside it have in common. The count is live — it follows the filter, so
   a reader who narrows to MIT sees how much of the room that leaves.    */
.room {
  display: grid;
  gap: var(--gap-btn);
  padding: var(--pad-card) 0 var(--gap-card);
  border-top: var(--hair) solid var(--rule);
  margin-bottom: var(--gap-card);
}
.room__n {
  font-family: var(--face-ui);
  font-size: var(--size-tag);
  letter-spacing: var(--ls-tag);
  font-weight: var(--w-label);
  text-transform: uppercase;
  color: var(--bronze-text);
  margin: 0;
}
.room__name { margin: 0; }
.room__note {
  margin: 0;
  max-width: 44ch;
  color: var(--muted);
}

.form__input:hover, .form__area:hover, .form__select:hover { border-color: var(--bronze); }

/* Errors are named in words, never in colour alone. There is no red on this
   site: a fifth hue for one state would cost more than it buys, and "Error:"
   in the label is what a screen reader announces either way.             */
.form__row[data-invalid] .form__input,
.form__row[data-invalid] .form__area,
.form__row[data-invalid] .form__select {
  border-color: var(--ink-deep);
  border-left-width: var(--focus-off);
}
.form__error {
  font-family: var(--face-ui);
  font-size: var(--size-small);
  line-height: var(--lh-small);
  font-weight: var(--w-label);
  color: var(--ink-deep);
}
.form__status {
  font-family: var(--face-ui);
  font-size: var(--size-small);
  line-height: var(--lh-small);
  font-weight: var(--w-label);
  color: var(--ink-deep);
  border-left: var(--focus-off) solid var(--ink-deep);
  padding-left: var(--pad-card);
}
.form__status:empty { display: none; }

/* The honeypot. Off-screen rather than display:none, because some bots
   check for that, and never in the tab order. */
.form__pot {
  position: absolute;
  left: -9999px;
  width: var(--hair);
  height: var(--hair);
  overflow: hidden;
}

.doors--forms { grid-template-columns: 1fr; gap: var(--band-pad-y); margin-top: var(--band-pad-y); }
/* ONE COLUMN, BECAUSE THERE IS NOTHING TO PUT IN THE SECOND ONE.
   Measured: the intro is a heading and two lines — its content ended 162px
   down a 663px row, leaving 501px of empty left column beside the form on
   the page an investor lands on. A stretched grid item is not filled space.

   S2 §4: a two-column band holds a figure on one side, and where there is
   no figure the band is ONE column — never two with one empty. There is no
   figure for a contact door, so the intro sits above the form and the form
   keeps a readable measure instead of being pushed into a 896px track. */
.door {
  display: block;
  padding-top: var(--band-pad-y);
  border-top: var(--hair) solid var(--rule);
}
.door__intro { max-inline-size: 46rem; margin-bottom: var(--pad-card); }
.door .form { max-inline-size: 46rem; }
.door__intro { display: flex; flex-direction: column; gap: var(--gap-card); }

/* ── /about ───────────────────────────────────────────────────────────────
   One photograph, at a real aspect ratio, with real alt text. The frame is
   measured and empty rather than filled with a stock photograph of somebody
   who is not him.                                                        */

.portrait {
  aspect-ratio: 4 / 5;
  max-width: 26rem;
  border-radius: var(--radius-card);
  background: var(--card-glass);
  border: var(--hair) solid var(--rule);
  box-shadow: var(--shadow-card);
  display: grid;
  place-items: center;
  padding: var(--pad-card);
  text-align: center;
}
.about__grid {
  display: grid;
  grid-template-columns: 26rem 1fr;
  gap: var(--band-pad-x);
  margin-top: var(--band-pad-y);
  align-items: start;
}
/* WITH THE PORTRAIT GONE THE GRID MUST NOT KEEP ITS EMPTY COLUMN.
   A two-column grid whose first child was removed does not collapse - the
   remaining child simply takes column one and the second track stays there
   as reserved emptiness, which is the defect this removal was for. */
.about__grid--text { grid-template-columns: minmax(0, 62ch); }
.about__text > * + * { margin-top: var(--pad-card); }

/* ── 404 ──────────────────────────────────────────────────────────────── */
.four04 { display: flex; flex-direction: column; gap: var(--pad-card); }
.four04__links { display: flex; flex-wrap: wrap; gap: var(--pad-card); margin-top: var(--pad-card); }

/* ══════ BEGIN AMBER USE 3 OF 3 — the primary action on /ember ════════════
   The third and last amber on the site. It is scoped to this page by the
   body's data-page, which is also how tools/colour-budget.mjs knows that on
   /ember .btn--primary is amber and NOT bronze — without that, the class
   would be one name carrying two meanings on two pages, which is the exact
   defect this project keeps paying for.

   The label is INK on amber, not white. White on #E8A87C is a contrast ratio
   of about 2:1 and fails at any size; ink on amber clears AA comfortably.
   Measured and reported with the session.                                */
[data-page="ember"] .btn--primary {
  background: var(--amber);
  color: var(--ink);
}
[data-page="ember"] .btn--primary:hover { box-shadow: var(--shadow-card); }
/* ══════ END AMBER USE 3 OF 3 ════════════════════════════════════════════ */

/* ── RESPONSIVE, W3 ─────────────────────────────────────────────────────── */

@media (max-width: 1023px) {
  .split--warm { grid-template-columns: 1fr; }
  .runs { grid-template-columns: 1fr; }
  .door { grid-template-columns: 1fr; gap: var(--pad-card); }
  .about__grid { grid-template-columns: 1fr; }
  .prose dl { grid-template-columns: 1fr; gap: var(--gap-btn); }
  .prose dd { margin-bottom: var(--gap-card); }
}

@media (max-width: 640px) {
  .form__grid { grid-template-columns: 1fr; }
  /* "Bring EMBER to your students" is 297px of nowrap label plus 56px of
     padding. On a 320px phone that is wider than the column it sits in, and
     ember.html slid sideways by 12px. A button is allowed two lines before it
     is allowed to break the page — `.btn` keeps its 44px min-height, so
     wrapping makes it taller rather than smaller and the tap target is safe. */
  .btn { white-space: normal; max-width: 100%; }
  /* THE ORBIT'S RADIUS SHRANK AND ITS SATELLITES DID NOT.
     This block already swapped the 240px ring for 38vw, which is right in
     principle. What it left behind is `--orbit-sat: 100px` — so each satellite
     still reached 50px past the ring. Measured on ember.html at a 345px
     content width: 38vw = 131 + 50 = 181 from centre against a 172 half-box,
     and the "DOC" satellite ran to 356. An 11px sideways scroll on a phone.
     A radius solved without the thing sitting ON the radius is half a fix.
     64px satellites on a 32vw ring clear at 320px and above. */
  .orbit { max-width: 100%; --orbit-sat: 64px; }
  .orbit__sat { transform: rotate(var(--a)) translate(32vw) rotate(calc(var(--a) * -1)); }
  .orbit__hub { width: var(--orbit-sat); height: var(--orbit-sat); font-size: var(--size-small); }
  .noscript-nav { padding-inline: var(--band-pad-x); }
}


/* ═══════════════════════════════════════════════════════════════════════════
   THE MUSEUM
   ═══════════════════════════════════════════════════════════════════════════ */

.tally {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--pad-card);
  margin-top: var(--band-pad-y);
  border-top: var(--hair) solid var(--rule);
  padding-top: var(--pad-card);
}
.tally__item { display: flex; flex-direction: column; gap: var(--gap-btn); }
.tally__item .numeral { font-size: var(--size-h3); line-height: var(--lh-h3); }

.station__label {
  font-family: var(--face-ui);
  font-size: var(--size-eyebrow);
  line-height: var(--lh-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  font-weight: var(--w-label);
  text-transform: uppercase;
  color: var(--muted);
}

/* ── THE FEATURED STATIONS ───────────────────────────────────────────────
   Five parts, and the fourth is the one nobody publishes.                */

.exhibit { margin-top: var(--band-pad-y); }
.exhibit + .exhibit { margin-top: calc(var(--band-pad-y) * 2); }
.exhibit__head { display: flex; flex-direction: column; gap: var(--gap-card); }
.exhibit__grid {
  display: grid;
  grid-template-columns: 1fr 22rem;
  gap: var(--band-pad-x);
  margin-top: var(--pad-card);
  align-items: start;
}
.exhibit__notes { display: flex; flex-direction: column; gap: var(--pad-card); }

.note {
  background: var(--card-glass);
  backdrop-filter: blur(var(--blur-card));
  -webkit-backdrop-filter: blur(var(--blur-card));
  border-radius: var(--radius-card);
  padding: var(--pad-card);
  box-shadow: var(--shadow-card);
  display: flex;
  flex-direction: column;
  gap: var(--gap-card);
}
/* THE REJECTION IS THE NEGATIVE OF THE PAGE.

   This was a 3px ink border down the left of a rounded glass card. Kevin:
   "that little black line on the edge of it, that is AI if AI had a name."
   He is right and the reason is worth naming: a coloured left border on a
   soft card is the single most reached-for way to mark a callout, which is
   exactly why it carries no meaning any more — every generated component
   library ships it, so it reads as a default rather than a decision.

   The replacement is not another marker. The card INVERTS: ink ground, page
   type. Everything else on this page is dark-on-light, so the one block that
   records what was thrown away is the one block that is light-on-dark. It is
   the negative of the page, it needs no border to be found, and it belongs
   to a palette this site already owns — the footer and the engine panel are
   the same ground. */
.note--reject {
  background: var(--ink);
  box-shadow: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
/* NOT BRONZE. On ink it does not need to be, and the colour budget caught
   the real cost: this card also appears in the #limit section of the three
   product pages, where the warm column beside it is amber — and bronze and
   amber may never share a viewport. A warm grey mixed from the page ground
   reads correctly on ink and belongs to no budget. */
.note--reject .station__label { color: color-mix(in srgb, var(--ground) 62%, var(--ink)); }
.note--reject .small,
.note--reject .small--ink,
.note--reject p { color: var(--ground); }
.note--reject strong { color: var(--card); }

/* ── THE TERMINAL ─────────────────────────────────────────────────────── */

.term {
  margin: 0;
  background: var(--term-bg);
  -webkit-backdrop-filter: blur(var(--blur-card));
  backdrop-filter: blur(var(--blur-card));
  border: var(--hair) solid var(--term-edge);
  border-radius: var(--radius-card);
  box-shadow: var(--term-shadow) 0 4px 32px 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  height: 100%;
}
.term__bar {
  display: flex;
  align-items: center;
  gap: var(--gap-card);
  padding: var(--gap-btn) var(--gap-card);
  border-bottom: var(--hair) solid var(--term-edge);
  background: var(--term-bar);
}
/* THE LABEL SITS IN THE BAR IT NAMES.
   It was a chip absolutely positioned against the SWAP, and the swap has
   two panes stacked in one grid area — so the chip reading AFTER was
   pinned to the swap top-right, which in the default state is the BEFORE
   pane's tool buttons. Measured 52x36px of overlap on all three stations,
   z-index 3 over the controls: the word bled through the skip button and
   named the wrong file while it did it.
   Positioned by the pane instead of by the swap, it cannot do either. The
   clip that reveals a pane now reveals its label with it, and the bar reads
   as one caption: BEFORE - style.css - lines 31-52 of 354.
   It takes the slot the three coloured dots held. Those were a macOS window
   skeuomorph carrying no information, on the panel THE-TEARDOWN 3.2 calls
   dated. The tokens for them are removed with them. */
.term__tag {
  flex: none;
  font-family: var(--face-ui);
  font-size: var(--size-tag);
  letter-spacing: var(--ls-tag);
  font-weight: var(--w-label);
  color: var(--term-text);
  background: var(--term-tool);
  padding: 2px var(--gap-btn);
  border-radius: var(--focus-off);
}

.term__name {
  font-family: var(--face-mono);
  font-size: var(--size-gutter);
  line-height: var(--lh-gutter);
  color: var(--term-gutter-text);
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* THE HIERARCHY IS CARRIED IN COLOUR, NOT IN OPACITY.
   This was opacity: 0.75 over the bar colour, which measured 3.36:1 — the
   colour had just been fixed to 5.07 and the opacity took a third of it
   straight back. Opacity on type is a colour change that no token audit can
   see, because the declared colour still passes; only a walker that
   composites the rendered pixel catches it.
   Two declared values instead: the filename brighter, the line count
   quieter, both measured on the bar's own ground. */
.term__name { color: var(--term-name); }
.term__meta { color: var(--term-gutter-text); }
.term__tools { display: inline-flex; gap: var(--gap-btn); flex: none; }
.term__btn {
  font-family: var(--face-mono);
  font-size: var(--size-gutter);
  line-height: var(--lh-gutter);
  color: var(--term-tooltxt);
  background: var(--term-tool);
  border: 0;
  border-radius: var(--focus-off);
  padding: 0 var(--gap-btn);
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  cursor: pointer;
  transition: color var(--dur-base) var(--ease-soft);
}
.term__btn:hover { color: var(--term-text); }

/* THE CAPTION BAR HAS TO WRAP ON A PHONE, OR IT EATS THE CITATION.
   `.term__name` is `flex:1; white-space:nowrap; overflow:hidden`, which is
   right on a wide bar: the filename holds the left, the tools hold the right.
   At 345px the filename takes the whole track and `.term__meta` — the
   "lines 195–216 of 686" — is pushed past the hidden boundary. Measured 0% to
   8% visible on three stations, with no ellipsis, so it does not read as
   truncated. It reads as absent.

   That matters more here than it would elsewhere. The Museum's whole claim is
   that every excerpt says where it came from, and the line range IS the
   citation. Losing it silently on a phone is losing the argument.

   One line each, tools right-aligned underneath. Above 700px nothing changes,
   so the GitHub-Dark bar this came from is untouched where it fits. */
@media (max-width: 700px) {
  .term__bar { flex-wrap: wrap; row-gap: 6px; }
  .term__name { flex: 1 1 100%; white-space: normal; overflow: visible; }
  .term__tools { margin-left: auto; }
}

.term__body { overflow: auto; flex: 1; }
.term__code {
  margin: 0;
  padding: var(--gap-card) 0;
  font-family: var(--face-mono);
  font-size: var(--size-code);
  line-height: var(--lh-code);
  color: var(--term-text);
  tab-size: 2;
}
.term__code code { display: block; }

/* The gutter is a real column, so a long line wraps against the code and
   never under the numbers. */
.term__line { display: grid; grid-template-columns: 3.5rem 1fr; }
.term__n {
  color: var(--term-gutter-text);
  font-size: var(--size-gutter);
  line-height: var(--lh-code);
  text-align: right;
  padding-right: var(--gap-card);
  user-select: none;
}
.term__t { white-space: pre-wrap; word-break: break-word; padding-right: var(--gap-card); }

/* THE MAP MATTERS MORE THAN THE PALETTE, and reading site.js is the only
   way to know it. The highlighter tags a CSS PROPERTY NAME as .tok-id — a
   word followed by a colon — and a value as .tok-num. Those two are most of
   what a CSS excerpt is made of. Folding both into the plain ink, which is
   what a straight seven-to-four collapse does, rendered station 1 in two
   colours: 25 ink tokens and 2 grey comments. Measured, then re-mapped.

   The roles are grouped by WHAT THE TOKEN IS, not by the highlighter class:

     the named thing   .tok-id  .tok-key    bronze — properties, keywords
     the literal       .tok-str .tok-tag .tok-fn  green — strings, selectors
     the quantity      .tok-num             slate  — numbers
     the aside         .tok-com             muted, italic
     everything else   the plate ink        braces, punctuation           */
.tok-key { color: var(--syn-key); }
.tok-id  { color: var(--syn-key); }
.tok-str { color: var(--syn-str); }
.tok-fn  { color: var(--syn-fn);  }
.tok-tag { color: var(--syn-tag); }
.tok-num { color: var(--syn-num); }
.tok-com { color: var(--syn-com); font-style: italic; }

.term__caret {
  display: inline-block;
  width: var(--gap-btn);
  height: var(--size-code);
  background: var(--term-text);
  vertical-align: text-bottom;
  animation: term-blink var(--dur-blink) steps(2, start) infinite;
}
@keyframes term-blink { to { visibility: hidden; } }

/* ── THE WHOLE FILE ────────────────────────────────────────────────────────
   The excerpt is what the argument needs; the whole file is what the claim
   needs. Opening one turns the window into a scroller and marks the lines
   that were quoted, so the reader can see the excerpt in its own context
   rather than taking our word for where it came from.                     */

/* The native scrollbar inside this window is the only thing telling a reader
   there is more file below, so it must stay — the page's own bar is hidden
   on <html> alone. `color-scheme` makes the platform draw it dark, which is
   one property instead of four vendor-prefixed pseudo-elements that only
   ever worked in one engine. */
/* The plate is a light surface now, so the scrollbar in .term__body and any
   form control inside it must be told — color-scheme: dark here painted a
   dark scrollbar down the edge of a cream panel. */
.term { color-scheme: light; }

.term__btn--file { color: var(--term-text); }
.term__btn--file[disabled] { opacity: .4; cursor: default; }

/* The window grows when it has a file in it. Both panes are laid on the same
   grid, so this has to be asked of the swap and not of the pane. */
/* A HEIGHT, NOT A MIN-HEIGHT. The window has to become a scroller, and a
   floor is not a ceiling: with min-height alone the body simply grew to fit
   all 937 lines and the box measured 19,735px. This is the declaration that
   makes .term__body scroll rather than expand. */
.swap:has(.term[data-full]) { block-size: min(78vh, 46rem); min-block-size: 0; }
.term[data-full] .term__body { overscroll-behavior: contain; }

/* A 937-line file is 4,489 elements, and building them all at once cost 235ms
   — a quarter-second freeze on a click. Skipping the rendering work for the
   lines that are off-screen brings it to 90ms. The estimate has to be given,
   or the scrollbar would be a guess: with it, scrollHeight moved 42px across
   the whole 19,370px file, which is a fifth of one percent. */
.term[data-full] .term__line {
  content-visibility: auto;
  contain-intrinsic-size: auto var(--lh-code);
}

/* THE QUOTED RANGE. No colour is spent here: the amber budget is three uses
   site-wide and this is not one of them, and a green band would read as a
   diff addition, which is a different claim than the one being made. The
   band is a lift of the surface and the numbers beside it come up to full
   strength — which is enough to find it while scrolling past. */
/* The lit range. White-at-5.5% was the only way to raise a line off a black
   panel; on the plate it is invisible. --amber-wash instead, which is the
   token the rest of the site already uses to mark something as the thing you
   were sent here to look at. */
.term__line--mark { background: var(--term-mark); }
.term__line--mark .term__n { color: var(--term-text); }
.term__line--mark:first-of-type { box-shadow: inset 0 var(--hair) 0 var(--term-edge); }
.term__line--mark:last-of-type  { box-shadow: inset 0 calc(var(--hair) * -1) 0 var(--term-edge); }

/* THE FOOTER. It says which of the two kinds of window this is, and the
   second kind is the point: a page arguing for licence discipline cannot
   republish an unlicensed file to prove it read one. */
.term__foot {
  margin: 0;
  padding: var(--gap-btn) var(--gap-card);
  border-top: var(--hair) solid var(--term-edge);
  background: var(--term-bar);
  font-family: var(--face-ui);
  font-size: var(--size-small);
  line-height: var(--lh-small);

  color: var(--term-tooltxt);
  flex: none;
}
.term__foot code { font-family: var(--face-mono); color: var(--term-name); }
.term__foot-k {
  display: inline-block;
  margin-right: .55em;
  padding: 1px .5em;
  border: var(--hair) solid var(--term-edge);
  border-radius: 999px;
  color: var(--term-text);
  white-space: nowrap;
}
.term__foot--shut .term__foot-k { color: var(--term-gutter-text); border-style: dashed; }

/* At phone width the footer is the first thing that should give up height,
   and the file window should not eat the whole viewport. */
@media (max-width: 40rem) {
  /* On a phone the bar wraps to three rows and the footer runs to four
     lines, so 32rem of box left 233px of actual code — less than half of it.
     The rem term was the binding one here, not the vh term, so raising the
     vh alone would have changed nothing. */
  .swap:has(.term[data-full]) { block-size: 80vh; }
  .term__foot { padding: var(--gap-btn) var(--gap-btn); }
}

.term__side {
  margin: 0;
  padding: var(--gap-btn) var(--gap-card);
  border-top: var(--hair) solid var(--term-edge);
  font-family: var(--face-ui);
  font-size: var(--size-tag);
  letter-spacing: var(--ls-tag);
  text-transform: uppercase;
  color: var(--term-tooltxt);
}

/* ── THE SWAP ─────────────────────────────────────────────────────────────
   Both panes occupy the same box. The top one is clipped from the right,
   which is the reference's mechanism exactly. */

/* C21's mechanism is clip-path: inset(0 X% 0 0) on a stacked top layer, and
   for two PHOTOGRAPHS that is exactly right — the same pixel position means
   the same thing in both, so revealing one through the other compares like
   with like.

   FOR CODE IT IS WRONG, and it was built the reference's way first to find
   out: a clip at 50% hides every line number and the start of every line,
   which is where all of the meaning in code actually is. The right-hand pane
   came out looking empty, because the right half of a short line IS empty.

   So the divider moves the SPLIT rather than the CLIP. Each pane keeps its
   own left edge and its own gutter, and the slider changes how much room
   each one gets. Same one window, same single control, and both sides stay
   readable at every position.                                            */
.swap {
  position: relative;
  isolation: isolate;
  display: grid;
  grid-template-areas: "stack";
  background: var(--swap-ground);
  -webkit-backdrop-filter: blur(var(--blur-card));
  backdrop-filter: blur(var(--blur-card));
  border: var(--hair) solid var(--term-edge);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  min-height: 20rem;
}

/* BOTH PANES ARE FULL WIDTH AND ONE OF THEM IS CLIPPED. They are not two
   columns that grow and shrink past each other. A pane squeezed to zero
   width does not go away — it reflows, and a paragraph at zero width sets
   one character per line. That is what made the hidden pane 861px tall and
   stretched the visible one to match it, which is where the dead black
   below every excerpt came from. Clipping changes what is seen without
   changing what is laid out, so the height of this box is now the height
   of the code in it. */
.swap__pane {
  grid-area: stack;
  min-width: 0;
  overflow: hidden;
}
.swap__pane--before { clip-path: inset(0 calc(100% - var(--swap-at, 50%)) 0 0); }
.swap__pane--after  { clip-path: inset(0 0 0 var(--swap-at, 50%)); }
.swap__line {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--swap-at, 50%);
  width: var(--hair);
  background: var(--swap-line);
  z-index: 2;
  pointer-events: none;
}
/* Inside a swap the SWAP is the plate — the panes are stacked in one grid
   area and clipped, so a border on each pane would draw two edges inside one
   object and the clip would slice them mid-stroke. */
.swap .term {
  border-radius: 0;
  border: 0;
  box-shadow: none;
  background: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* @property IS WHAT MAKES THE WIPE POSSIBLE.
   --swap-at drives grid-template-columns. An unregistered custom property has
   no type as far as the browser is concerned, so it can only ever jump from
   one value to the next; registering it as a <percentage> makes it a number
   the engine can interpolate, and the whole animation is then one transition
   on one declaration. Where @property is unsupported the split still switches,
   it simply does not travel — the control works either way. */
@property --swap-at {
  syntax: '<percentage>';
  inherits: true;
  initial-value: 50%;
}

.swap {
  transition: --swap-at var(--dur-slow) var(--ease-snap);
}
.swap[data-showing="before"] { --swap-at: 100%; }
.swap[data-showing="after"]  { --swap-at: 0%; }

/* The pane that is clipped away takes its own label with it, so there is
   nothing left to dim: the bar you can read is the bar that is showing. */

.swap__control { margin: var(--gap-card) 0 0; }

@media (prefers-reduced-motion: reduce) {
  .swap { transition: none; }
}

/* ── THE STATIONS ─────────────────────────────────────────────────────── */

.filters { display: flex; flex-direction: column; gap: var(--gap-card); margin-top: var(--band-pad-y); }
.filters__row { display: flex; flex-wrap: wrap; gap: var(--gap-btn); }
.chip {
  font-family: var(--face-ui);
  font-size: var(--size-small);
  line-height: var(--lh-small);
  font-weight: var(--w-label);
  color: var(--ink);
  background: var(--card-glass);
  border: var(--hair) solid var(--rule);
  border-radius: var(--radius-btn);
  padding: var(--gap-btn) var(--pad-card);
  min-height: var(--tap-min);
  cursor: pointer;
  transition: background var(--dur-base) var(--ease-soft),
              color      var(--dur-base) var(--ease-soft);
}
.chip:hover { background: var(--card); }
.chip[aria-pressed="true"] { background: var(--ink); color: var(--card); border-color: transparent; }

.group { margin-top: var(--band-pad-y); }
.group > .h3 { margin-bottom: var(--pad-card); }
/* TWO COLUMNS THAT REFUSED TO NARROW, AND THEN REFUSED TO COLLAPSE.
   `1fr` is `minmax(auto, 1fr)`, so a track never goes below its content's
   min-content width. A station holds a monospace filename, a licence line and
   a badge, and its min-content came out at 349px — inside a 305px container.
   The card was WIDER THAN ITS OWN PARENT and the museum scrolled sideways by
   24px. Same failure the landing's card grid had; `minmax(0,1fr)` is the same
   one-word fix, and it is worth knowing this is a class of bug rather than an
   incident.
   And two columns is wrong on a phone regardless: 145px of monospace code is
   not a reading width. One column below 760. */
/* ══ THE RAIL — Evasion's math, as a view-timeline ════════════════════════

   references/evasion-e-commerce-template/.../gallery-section.tsx pins a
   section and runs its content sideways as you scroll down. The arithmetic:

     sectionHeight = viewportHeight + (contentWidth - viewportWidth)
     progress      = -rect.top / (contentWidth - viewportWidth)
     translateX    = progress * -(contentWidth - viewportWidth)

   The height is DERIVED from the content width — that is the part worth
   stealing. The section is exactly as tall as the distance it has to travel,
   so it pins, travels, and releases with nothing left over.

   IT IS NOT COPIED LINE FOR LINE. Evasion drives it from a scroll listener
   and setState; gate check 6 forbids scroll listeners in this project, and a
   rail is the worst possible place to keep one because it moves on every
   frame of a long scroll. The same mapping is a view-timeline instead: the
   browser divides on the compositor and no handler ever runs.

   RANGE IS contain, NOT cover. Cover begins the moment the element's edge
   enters the viewport, which would start the track moving while the heading
   was still being read. Contain runs from "fully inside" to "about to leave"
   — precisely the span during which the section is pinned.                */

.rail {
  /* the formula, in CSS */
  /* --rail-card, NOT --card. Naming this --card shadowed the global
     --card: #FFFFFF for every descendant of .rail, so every badge inside the
     rail resolved color: var(--card) to 26rem — an invalid colour, so the
     whole declaration was dropped and the badge fell back to ink text on an
     ink ground. Ratio 1.00. They rendered as solid black rectangles and I had
     looked straight at them in a screenshot without registering it.

     A custom property is a scoped global. Shadowing one by accident is the
     same one-name-two-meanings failure this project keeps paying for, and it
     is worse than a colliding class because it silently DELETES declarations
     rather than adding wrong ones. */
  --rail-card:  26rem;
  --pitch: calc(var(--rail-card) + var(--pad-card));
  /* THE GAIN. Evasion maps one pixel of scroll to one pixel of travel, which
     is right for eight photographs and wrong for twenty-nine cards: at 1:1
     this section alone was four and a half thousand pixels of scrolling and
     Kevin's complaint was that the page is too long. The gain shortens the
     SCROLL without shortening the TRAVEL, so the same rail runs in 58% of the
     distance and the cards simply move faster. Below about 0.45 it starts to
     feel flung rather than driven. */
  --rail-gain: .58;
  height: calc(100vh + ((var(--n) * var(--pitch)) - 100vw) * var(--rail-gain));
  view-timeline: --rail block;
  position: relative;
}
/* A HAND-WRITTEN COUNT DRIFTED AND TOOK FIVE IMPORTS WITH IT.
   --n sets the rail height, and the rail height IS the scroll runway that
   carries the horizontal track past the viewport. Interface said 11 and
   held 16, so the runway ran out five cards early and book-store-ui,
   evasion-e-commerce-template, book-storewebsite-design,
   solid-command-palette-main and pressure-sensitive-canvas-master could
   not be reached at ANY scroll position. Measured by walking the whole
   runway in 120px steps: 11 of 16 cards ever entered the viewport.

   On a page whose argument is that every import is credited and read line
   by line, five credits that cannot be reached is the argument failing.

   This ladder stays for the no-JS path, and site.js now sets --n from the
   live count so the two can never disagree again. tools/rail-n.cjs gates
   the attribute against the markup. */
.rail[data-n="3"]  { --n: 3;  }
.rail[data-n="11"] { --n: 11; }
.rail[data-n="15"] { --n: 15; }
.rail[data-n="16"] { --n: 16; }

.rail__stick {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  align-items: center;
  /* clip, not hidden: hidden on an ancestor kills position:sticky, and this
     element IS the sticky one. clip does the same visual job without
     establishing a scroll container. */
  overflow: clip;
}

.stations {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--pad-card);
}

.rail__track {
  display: flex;
  grid-template-columns: none;
  gap: var(--pad-card);
  padding-inline: var(--band-pad-x);
  will-change: transform;
  animation: rail-run linear both;
  animation-timeline: --rail;
  animation-range: contain 0% contain 100%;
}
/* A CARD IN A PINNED RAIL CANNOT BE TALLER THAN THE PIN. The stations carry
   very different amounts of text — one is four lines, one is twenty — and in
   a flex row they all stretched to the tallest, then ran off the bottom of
   the 100vh stick with no way to reach the rest. Capped, and the long ones
   scroll inside themselves.

   overscroll-behavior: contain is the important half: without it, reaching
   the end of a card hands the gesture back to the page, which is pinned, so
   the rail lurches. With it the card simply stops. */
.rail__track > .station {
  flex: 0 0 var(--rail-card);
  max-width: var(--rail-card);
  max-height: calc(100vh - 10rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  align-self: center;
}

@keyframes rail-run {
  to { transform: translate3d(calc(100vw - (var(--n) * var(--pitch)) - var(--band-pad-x) * 2), 0, 0); }
}

/* NO TIMELINE, NO RAIL. Where view-timeline is unsupported the track must not
   sit in a 100vh sticky box with its content running off the side — it goes
   back to being the two-column grid it was, and the rail height collapses to
   the content. Everything below is the fallback, and the @supports flips it. */
.rail { height: auto; }
.rail__stick { position: static; height: auto; overflow: visible; }
.rail__track { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
               animation: none; padding-inline: 0; }
.rail__track > .station { flex: none; max-width: none; }

@supports (animation-timeline: view()) {
  .rail { height: calc(100vh + ((var(--n) * var(--pitch)) - 100vw) * var(--rail-gain)); }
  .rail__stick { position: sticky; top: 0; height: 100vh; overflow: clip; }
  .rail__track {
    display: flex; grid-template-columns: none;
    padding-inline: var(--band-pad-x);
    animation: rail-run linear both;
    /* AFTER the shorthand, never before. The animation shorthand resets
       animation-timeline to auto, so declaring the timeline in the base rule
       and the shorthand in here silently put the track back on the document
       timeline — it reported DocumentTimeline and ran once on load instead of
       tracking scroll. This is the second time that reset has bitten this
       project; the scrollbar had it too. */
    animation-timeline: --rail;
    animation-range: contain 0% contain 100%;
  }
  .rail__track > .station { flex: 0 0 var(--rail-card); max-width: var(--rail-card); }
}

/* ── THE SWIPE TRAP, AND WHY THE BREAKPOINT WAS NOT ENOUGH ────────────────
   Kevin: "on mobile you don’t have to scroll through the entire section of
   the Every Import in order to scroll downwards. You can swipe right on that
   section. But if you’re just swiping down the page, it shouldn’t make you
   scroll through the entire thing."

   The rail already turned back into a column below 760px. That was not the
   trap. The trap was the CARD: it is capped at the stick height and scrolls
   its own text, with overscroll-behavior: contain so the gesture never
   chains. On a desktop that is a wheel over a box. On a phone it means a
   downward swipe that begins anywhere on a card scrolls THAT CARD to its end
   and then stops dead, and the page does not move until you lift your thumb
   and start again. MEASURED AT 375px: 19 of the 37 cards did this.

   The cap exists for the pinned rail, and where there is no pinned rail
   there is nothing to cap. Below the breakpoint the card is simply as tall
   as it is and the page scrolls the way a page scrolls.

   POINTER: COARSE IS IN THE QUERY, NOT JUST A WIDTH. A tablet at 900px is
   wide enough for the rail and is still a thumb: same 37 cards, same trap. A
   touchscreen laptop is not caught by this, because a coarse PRIMARY pointer
   is what the query asks about and such a machine reports fine. */
@media (max-width: 760px), (pointer: coarse) {
  /* THE RAIL STOPS BEING SCROLL-DRIVEN AND BECOMES A REAL ONE.

     Stacking the cards into a column fixed the trap and made /museum 44,602px
     tall on a phone — thirty-seven full-height cards, measured. Kevin asked
     for the other thing, in as many words: "you can swipe right on that
     section, but if you’re just swiping down the page, it shouldn’t make you
     scroll through the entire thing."

     That is overflow-x, not an animation. A browser already separates the two
     axes on a horizontal scroller: a sideways drag moves the track, a
     downward drag moves the page, and it decides which from the gesture
     rather than from a timeline. No JavaScript, and nothing to get wrong.

     overscroll-behavior-x: contain keeps a sideways fling from turning into a
     browser back-swipe at the end of the track. The Y axis is left alone —
     containing THAT is the bug this block exists to remove. */
  .rail, .rail__stick { height: auto; position: static; overflow: visible; }
  .rail__track {
    display: flex;
    grid-template-columns: none;
    animation: none;
    overflow-x: auto;
    overflow-y: visible;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    /* NO INLINE PADDING ON A PHONE. The track sits inside a column that is
       already inset, so its own 20px each side came off a box that had none
       to give. */
    scroll-padding-inline: 0;
    padding-inline: 0;
    -webkit-overflow-scrolling: touch;
  }
  /* ── THE CARD IS SIZED OFF THE TRACK, NOT OFF THE WINDOW ───────────
     85vw looked like the obvious width and was wrong by 56px: at 375 the
     column insets the track to 303 and its padding cut that to 263, so a
     318.75px card could never be fully on screen. MEASURED: the first card
     ran x=56 to 375 inside a track ending at 339 — every line of the record
     clipped mid-word, on the first card of the section.

     A percentage resolves against the flex container, which is the thing the
     card actually has to fit in. 94% leaves a sliver of the next card
     showing, which is what tells a thumb the row moves; and it keeps the
     measure at about 36 characters, above the readability floor this very
     page cites when it explains what it rejected. */
  .rail__track > .station {
    flex: 0 0 min(94%, 22rem);
    max-width: min(94%, 22rem);
    scroll-snap-align: start;
    /* the card is as tall as it is. Nothing here scrolls but the track. */
    max-height: none;
    overflow: visible;
    overscroll-behavior: auto;
    /* EACH CARD IS ITS OWN HEIGHT. stretch made every card in a room as tall
       as the longest one in it — measured 2,178px of mostly empty glass behind
       a four-line record. The track is still as tall as its tallest card,
       because that is what a row is, but a short record now looks short. */
    align-self: start;
  }
  .rail__track > .station > .station__body {
    overflow: visible;
    overscroll-behavior: auto;
    min-height: 0;
    padding-right: 0;
    margin-right: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .rail, .rail__stick { height: auto; position: static; overflow: visible; }
  .rail__track { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
                 animation: none; padding-inline: 0; }
  .rail__track > .station { flex: none; max-width: none; }
}

.station {
  background: var(--card-glass);
  backdrop-filter: blur(var(--blur-card));
  -webkit-backdrop-filter: blur(var(--blur-card));
  border-radius: var(--radius-card);
  padding: var(--pad-card);
  box-shadow: var(--shadow-card);
  display: flex;
  flex-direction: column;
  gap: var(--gap-card);
  transition: background var(--dur-base) var(--ease-soft),
              opacity    var(--dur-base) var(--ease-soft);
}
.station[hidden] { display: none; }
.station__head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--gap-card); }
.station__name {
  font-family: var(--face-mono);
  font-size: var(--size-small);
  line-height: var(--lh-small);
  font-weight: var(--w-label);
  color: var(--ink);
  margin: 0;
  word-break: break-word;
}
.station__lic { font-family: var(--face-ui); font-size: var(--size-small); line-height: var(--lh-small); color: var(--muted); margin: 0; }
.station__body { display: flex; flex-direction: column; gap: var(--gap-btn); }
/* A WEB ADDRESS IS ONE WORD AND SOME OF THEM ARE 318px LONG.
   These snippets quote what was taken, and several of them are script tags
   naming a CDN — a single unbreakable token with no space to wrap at. With
   the grid fixed the card came down to 305px and the code inside it still ran
   to 354. Breaking such a token across lines is normal in a citation and far
   better than a page that slides sideways; `anywhere` breaks only when it
   must, so nothing on desktop moves.

   (This comment deliberately does NOT spell out the address. Gate check 5
   greps every stylesheet for external requests and cannot tell prose from
   code — the first draft of this note failed the build by describing the
   problem too literally. Fourth time a comment has tripped the check that
   enforces the rule it describes.) */
.station code {
  font-family: var(--face-mono);
  font-size: var(--size-gutter);
  background: var(--edge);
  border-radius: var(--focus-off);
  padding: 0 var(--focus-off);
  overflow-wrap: anywhere;
}

.badge {
  font-family: var(--face-ui);
  font-size: var(--size-tag);
  letter-spacing: var(--ls-tag);
  font-weight: var(--w-label);
  text-transform: uppercase;
  padding: 2px var(--gap-btn);
  border-radius: var(--focus-off);
  flex: none;
  background: var(--edge);
  color: var(--ink);
}
.badge--verbatim { background: var(--ink); color: var(--card); }
.badge--superseded, .badge--defect { background: var(--ink-deep); color: var(--card); }

/* THE METHOD BADGE IS A LADDER, NOT A SET OF LABELS. It says how far a station
   sits from the thing it came from, and the border says how sure we are:
   filled means the code was taken as written, a solid outline means some of it
   was, a dashed outline means none of it was and the behaviour was rebuilt
   from reading, and a dotted outline means the licence never told us which of
   those we were allowed to do. Left unstyled, PARTIAL, DERIVED and UNSTATED
   rendered as the same grey pill, which flattened the one distinction the
   page exists to draw. No colour is spent: the amber budget is three uses
   site-wide and none of them is here. */
.badge--partial {
  background: none;
  color: var(--ink);
  border: var(--hair) solid var(--ink);
  padding: 1px calc(var(--gap-btn) - 1px);
}
.badge--derived {
  background: none;
  color: var(--ink);
  border: var(--hair) dashed var(--ink);
  padding: 1px calc(var(--gap-btn) - 1px);
}
.badge--unstated {
  background: none;
  color: var(--muted);
  border: var(--hair) dotted var(--muted);
  padding: 1px calc(var(--gap-btn) - 1px);
}

/* The column that holds a code window has to be allowed to be narrower than
   its contents, or a 937-line file with a long line in it pushes the grid
   wider than the page. A grid child's default min-width is auto, which is
   the one value that would let that happen. */
.exhibit__panes { min-width: 0; }

/* The name is what is being chosen; the count beside it is an aside. Without
   this they set at the same weight and the row read as two equal columns. */
.web__name { color: var(--ink); }


/* ── THE WEB ──────────────────────────────────────────────────────────────
   The tree that cascades. It opens on hover AND on click, and it does not
   close when the pointer leaves the trigger — only when the pointer leaves
   the whole branch. A menu that vanishes while you are travelling to it is
   not a menu.                                                            */

.web { margin-top: var(--band-pad-y); }
.web__roots { display: flex; flex-wrap: wrap; gap: var(--gap-card); align-items: flex-start; }
.web__root { position: relative; }

.web__node {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-btn);
  font-family: var(--face-ui);
  font-size: var(--size-small);
  line-height: var(--lh-small);
  font-weight: var(--w-label);
  color: var(--ink);
  background: var(--card-glass);
  border: var(--hair) solid var(--rule);
  border-radius: var(--radius-btn);
  padding: var(--gap-btn) var(--pad-card);
  min-height: var(--tap-min);
  cursor: pointer;
  transition: background var(--dur-base) var(--ease-soft),
              transform  var(--dur-base) var(--ease-snap);
}
.web__node:hover, .web__node[aria-expanded="true"] { background: var(--card); }
.web__node[aria-expanded="true"] { transform: translateY(calc(var(--nudge) * -1)); }
.web__count {
  font-family: var(--face-mono);
  font-size: var(--size-gutter);
  color: var(--muted);
}

.web__branch {
  position: absolute;
  top: calc(100% + var(--gap-btn));
  left: 0;
  z-index: 4;
  min-width: 15rem;
  display: flex;
  flex-direction: column;
  gap: var(--focus-off);
  background: var(--card);
  border: var(--hair) solid var(--rule);
  border-radius: var(--radius-card);
  padding: var(--gap-card);
  box-shadow: var(--shadow-card);
}
.web__branch[hidden] { display: none; }
.web__leaf {
  position: relative;
  padding-left: var(--pad-card);
  font-family: var(--face-mono);
  font-size: var(--size-gutter);
  line-height: var(--lh-small);
  color: var(--ink);
}
/* the branch line — this is the tree, drawn rather than described */
.web__leaf::before {
  content: '';
  position: absolute;
  left: var(--gap-btn);
  top: 0;
  bottom: 50%;
  width: var(--hair);
  background: var(--web-line);
}
.web__leaf::after {
  content: '';
  position: absolute;
  left: var(--gap-btn);
  top: 50%;
  width: var(--gap-btn);
  height: var(--hair);
  background: var(--web-line);
}
.web__leaf:first-child::before { top: calc(var(--lh-small) * -0.5); }
.web__leaf--meta { color: var(--muted); }

/* THE PULL. Every open branch dims the page behind it a little, so the eye
   goes to the branch and not to the twenty other names beside it. */
.web[data-open] .web__node:not([aria-expanded="true"]) { opacity: 0.45; }
.web__node { transition: background var(--dur-base) var(--ease-soft),
                         opacity    var(--dur-base) var(--ease-soft),
                         transform  var(--dur-base) var(--ease-snap); }

/* ── LEGAL ────────────────────────────────────────────────────────────── */

.legal { border-top: var(--hair) solid var(--rule); padding-top: var(--gap-card); }
.legal summary {
  font-family: var(--face-ui);
  font-size: var(--size-small);
  line-height: var(--lh-small);
  font-weight: var(--w-label);
  color: var(--ink);
  cursor: pointer;
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
}
.legal__text {
  font-family: var(--face-mono);
  font-size: var(--size-gutter);
  line-height: var(--lh-small);
  color: var(--muted);
  white-space: pre-wrap;
  overflow-x: auto;
  margin: var(--gap-card) 0 0;
}

/* ── RESPONSIVE ───────────────────────────────────────────────────────── */

@media (max-width: 1023px) {
  .tally { grid-template-columns: repeat(2, 1fr); }
  .exhibit__grid { grid-template-columns: 1fr; }
  .stations { grid-template-columns: minmax(0, 1fr); }
  .swap { min-height: 18rem; }
}

@media (max-width: 640px) {
  .tally { grid-template-columns: 1fr; }
  .term__line { grid-template-columns: 2.5rem 1fr; }
  .swap { min-height: 16rem; }
  .web__branch { position: static; min-width: 0; }
}

/* The nav's web hangs below the pill. It needs a little more offset than the
   Museum's does, because the pill has a shadow the branch must clear.     */
.nav .web__branch {
  top: calc(100% + var(--pad-card));
  left: 50%;
  transform: translateX(-50%);
  min-width: 15rem;
  white-space: nowrap;
}
.nav .web__leaf { padding-left: var(--pad-card); }
.nav .web__leafname {
  display: block;
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  font-family: var(--face-ui);
  font-size: var(--size-small);
  color: var(--ink);
  transition: color var(--dur-base) var(--ease-soft);
}
.nav .web__leafname:hover { color: var(--muted); }
/* the pill dims its other links while a branch is open, same as the Museum */
.nav__links[data-open] .nav__link:not([aria-expanded="true"]) { opacity: 0.45; }

/* ── THE REEL ─────────────────────────────────────────────────────────────
   Rounded, revolving, and it crossfades rather than slides — a slide moves
   the eye, and the eye is meant to be on the sentence. It sits behind the
   type at low opacity and holds the right-hand third, which is the part of
   the arrival the lattice leaves emptiest.                               */

.reel {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 38%;
  max-width: 30rem;
  aspect-ratio: 4 / 5;
  margin: 0;
  border-radius: var(--radius-reel);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  z-index: 0;
  pointer-events: none;
}
.reel__frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--reel-fade) var(--ease-soft);
}
.reel__frame.is-on { opacity: 1; }

/* The type must win. The reel is a ground, not a subject. */
#arrival .arrival__inner > .h1,
#arrival .arrival__sub { position: relative; z-index: 1; }

@media (max-width: 1023px) {
  /* Below this width the reel would sit under the headline rather than
     beside it, so it goes under the text block instead of behind it. */
  .reel {
    position: relative;
    top: auto; right: auto;
    transform: none;
    width: 100%;
    max-width: none;
    aspect-ratio: 16 / 10;
    margin-top: var(--pad-card);
  }
}

/* A reader who asked for less motion gets the first frame and no crossfade.
   The reel is decorative, so unlike the arrival's type timeline there is
   nothing here they would be missing. */
@media (prefers-reduced-motion: reduce) {
  .reel__frame { transition: none; }
}

/* ══ THE MENU ON A PHONE ══════════════════════════════════════════════════
   Twenty-four pages had no mobile navigation at all. The links are hidden
   below 640px — correctly, they do not fit — and nothing replaced them, so a
   phone could reach the home page and one contact button and nothing else.
   index.html has had a burger since it was built; these pages never did.

   Built by site.js from the same LINKS and ACTION arrays the pill is built
   from, so the destinations cannot drift apart.

   THE BURGER IS 44px, WHICH IS THE POINT. WCAG 2.5.8 asks for 24px and every
   platform guideline asks for 44; the two bars inside it are 18px of drawing
   inside a 44px target, which is the same relationship the landing's burger
   already uses.                                                            */

.nav__burger {
  display: none;                 /* desktop shows the links instead */
  width: var(--tap-min); height: var(--tap-min);
  /* flex:none, or the pill squeezes it. Measured 36px wide before this,
     against the 44px the target is declared as. */
  flex: none;
  align-items: center; justify-content: center;
  flex-direction: column; gap: 5px;
  margin-left: auto;
  background: none; border: 0; padding: 0; cursor: pointer;
  border-radius: 50%;
}
.nav__burger span {
  display: block; width: 18px; height: 1.5px;
  background: var(--ink); border-radius: 2px;
  transition: transform var(--dur-quick) var(--ease-soft),
              opacity var(--dur-quick) var(--ease-soft);
}
.nav__burger[aria-expanded="true"] span:first-child { transform: translateY(3.25px) rotate(45deg); }
.nav__burger[aria-expanded="true"] span:last-child  { transform: translateY(-3.25px) rotate(-45deg); }

.nav__menu {
  position: absolute;
  top: calc(100% + var(--gap-btn));
  left: 0; right: 0;
  background: var(--card);
  border: var(--hair) solid var(--rule);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  padding: var(--pad-card);
  max-height: calc(100vh - var(--nav-h) - var(--nav-top) * 4);
  overflow-y: auto;
  overscroll-behavior: contain;   /* the page must not scroll behind it */
}
.nav__menu-l { list-style: none; margin: 0; padding: 0; }
.nav__menu-a {
  display: block;
  padding: 12px 8px;
  font-family: var(--face-display);
  font-size: var(--size-h4); line-height: var(--lh-h4);
  color: var(--ink); text-decoration: none;
}
.nav__menu-a[aria-current="page"] { color: var(--muted); }
.nav__menu-s { list-style: none; margin: 0 0 var(--gap-btn); padding: 0 0 0 8px; }
.nav__menu-b {
  display: block;
  padding: 9px 8px;
  min-height: var(--tap-min);
  font-size: var(--size-small); line-height: var(--lh-small);
  color: var(--muted); text-decoration: none;
}
.nav__menu-cta { width: 100%; justify-content: center; margin-top: var(--gap-btn); }

@media (max-width: 640px) {
  .nav__burger { display: flex; }
  /* The pill has to be able to hold the panel it opens. */
  .nav { position: fixed; }

  /* THE ACTION MUST NOT WRAP. With the burger added, the pill holds the
     wordmark, the action and the burger on one 351px row, and a flex item
     with no nowrap is the one that gives — "Get in touch" broke to two
     lines and pushed the pill to 68px of stacked text. Measured at 375: the
     three together need about 277px, so they fit comfortably once the
     action is allowed to keep its own width. */
  .nav .btn { white-space: nowrap; flex: none; }
  .nav .btn__arrow { flex: none; }
}
@media (min-width: 641px) {
  /* Belt and braces: if the panel were ever left open by a resize the script
     did not see, it still cannot appear on a desktop. */
  .nav__menu { display: none; }
}

@media (max-width: 640px) {
  /* The page does not scroll behind an open menu. The panel has its own
     max-height and overscroll-behavior:contain, so it scrolls internally;
     without this the page scrolls too and the reader loses their place
     behind a sheet they cannot see past.

     Caught by tools/audit-unstyled.cjs: site.js was setting .nav-open on
     <html> and no rule had ever read it — the JS half of a feature with no
     CSS half, which is the same shape as three other defects this week. */
  html.nav-open { overflow: hidden; }
}

/* ══ THE NAV PANEL IS THE LANDING'S NAV PANEL ═════════════════════════════
   Kevin: "when you hover over the specific things at the top, it's very low
   quality, and it's not the one that's rendered on the first landing site."

   Both halves of that were true. The interior panel was a plain white box
   with a file-tree connector beside every row — a vertical hairline and a
   horizontal stub. That is the right language for the Museum's dependency
   tree, which is literally a tree, and it reads as debris on a menu. It also
   had no group header, no icons, no year pills and no entry motion, all four
   of which the landing's panel has and all four of which are most of why the
   landing's reads as considered.

   SCOPED TO .nav, AND THAT IS THE WHOLE CARE HERE. .web__branch, .web__leaf
   and .web__leafname are shared with museum.html's import ledger, where the
   connectors are correct and wanted. Restyling the bare classes would have
   redrawn the Museum as a side effect of fixing the nav — the same one
   name, two meanings trap this project keeps paying for. Every selector
   below starts at .nav.                                                    */

.nav .web__branch {
  min-width: 17rem;
  padding: 14px;
  border-radius: 22px;
  border: 0;
  background: color-mix(in srgb, var(--card) 94%, transparent);
  box-shadow: rgba(0,0,0,.20) 0 30px 60px -28px,
              rgba(255,255,255,.60) 0 1px 0 0 inset;
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  gap: 0;
}

/* 10.5px, WHICH IS THE LANDING'S VALUE AND NOT THIS SHEET'S --size-tag.
   --size-tag is 8px. At 8px an uppercase group header stops being a label
   and becomes texture — it is a large part of what reads as low quality in
   a panel this size, and the landing does not make that mistake: .l-web__h
   and .l-web__yr are both 10.5px. The instruction here was to BE the
   landing's panel, so the number comes from the landing.

   --size-tag has since been raised from 8px to 10px, with the tracking
   moved from 0.96px to 1.2px to hold the same ratio. What settled it was the
   badge ladder: PARTIAL, DERIVED and UNSTATED are told apart by border-style,
   and at 8px with a 1px border dashed and dotted are the same pill — the one
   distinction the Museum exists to draw was being rendered invisible. */
.nav .web__head {
  font-family: var(--face-ui);
  font-size: 10.5px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 3px 12px 11px;
}

/* The connectors come off. They are drawn by ::before and ::after on the
   leaf, and inside the nav there is no tree for them to describe. */
.nav .web__leaf { padding-left: 0; }
.nav .web__leaf::before,
.nav .web__leaf::after { content: none; }

.nav .web__leafname {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 10px 12px;
  min-height: var(--tap-min);
  border-radius: 13px;
  font-family: var(--face-ui);
  font-size: var(--size-small);
  line-height: var(--lh-small);
  letter-spacing: normal;
  color: var(--ink);
  text-decoration: none;
  opacity: 0;
  transform: translate3d(0, -5px, 0);
  /* --ease-snap, NOT --ease-out. The first version named --ease-out, which
     this site does not declare — and an undefined var() inside the animation
     shorthand does not fall back, it deletes the whole declaration. The rows
     kept their opacity:0 starting state and the panel opened completely
     empty: a header, a blur, and six invisible links. animationName computed
     to 'none'. The two easings that exist here are --ease-soft and
     --ease-snap; a menu row arriving wants the snappier one. */
  animation: web-in var(--dur-base) var(--ease-snap) forwards;
  transition: background var(--dur-quick) var(--ease-soft);
}
.nav .web__leafname:hover { background: var(--ground); color: var(--ink); }
.nav .web__leafname .ico {
  width: 16px; height: 16px; flex: none;
  opacity: .6; color: var(--bronze);
}
.nav .web__yr {
  margin-left: auto;
  font-family: var(--face-mono);
  font-size: 10.5px;   /* the landing's .l-web__yr, same reason */
  padding: 2px 7px;
  border-radius: var(--radius-btn);
  background: var(--ground);
  color: var(--muted);
}
@keyframes web-in { to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  /* Present, not animating. An element left at opacity 0 because its entry
     never ran is the commonest way a reduced-motion menu ends up empty. */
  .nav .web__leafname { animation: none; opacity: 1; transform: none; }
}

/* A rejection note that follows a split has to clear it. Both blocks reset
   their own margins — the site's rhythm comes from containers, not from
   element margins — so with nothing between them the note's card sat flush
   against the bottom of the warm column, measured gap 0. The two read as one
   broken box. */
.split + .note { margin-top: var(--band-pad-y); }

/* ══ THE MUSEUM'S VESSEL ══════════════════════════════════════════════════
   A hero object beside the hero copy. The jar is raymarched by vessel.js; all
   this rule does is give it a box to live in and decide how that box behaves
   when the column collapses.

   THE FIGURE IS THE MOUNT AND THE CANVAS IS ITS ONLY CHILD, appended by the
   script. If WebGL is unavailable the script returns before appending
   anything, so the figure collapses to its caption and the section still
   reads — which is why the caption is markup and not drawn into the canvas.

   ASPECT-RATIO, NOT A HEIGHT. The shader is framed for a portrait-ish box; a
   fixed pixel height would crop the jar's foot at one width and float it in
   space at another. 4/5 keeps the framing the shader was tuned against at
   every column width.                                                      */

.museum-hero {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  gap: var(--band-pad-x);
  align-items: center;
}
.museum-hero__copy { min-width: 0; }

.museum-hero__vessel {
  margin: 0;
  position: relative;
  min-width: 0;
}
/* THE BOX IS ONLY RESERVED IF THERE IS SOMETHING IN IT. vessel.js appends the
   canvas and returns early when WebGL is unavailable, so without :has() a
   browser with no WebGL would lay out a 4:5 hole containing one line of
   caption. :has(canvas) means the aspect exists exactly when the object does. */
.museum-hero__vessel:has(canvas) { aspect-ratio: 4 / 5; }
.museum-hero__vessel:not(:has(canvas)) figcaption { display: none; }
/* THE CANVAS HAS NO EDGE.

   Writing alpha 0 for background rays removed the painted backdrop, but the
   FLOOR is opaque and it still ran to the boundary of the box — so the jar
   stood on a visible rectangle of plaster that stopped dead at three sides.
   Kevin: "I do not like the hard edges, it looks like an image."

   An elliptical mask dissolves the ground before it reaches any edge. It is
   one layer rather than the usual four-sided intersection, because a single
   ellipse cannot disagree with itself at the corners the way four linear
   gradients composited together do. The object sits well inside the opaque
   centre of it, so nothing of the jar is ever touched — only the ground it
   is standing on fades out into the page.

   rgba(0,0,0,1) rather than #000 because gate check 1 refuses a raw hex below
   the token block, and it is right to even here: only the ALPHA of a mask
   stop matters, so the colour notation is free and the check should not have
   to learn an exception to stay useful. */
.museum-hero__vessel canvas {
  display: block;
  width: 100%;
  height: 100%;
  -webkit-mask-image: radial-gradient(ellipse 70% 66% at 50% 48%,
                        rgba(0,0,0,1) 42%, rgba(0,0,0,.60) 72%, transparent 99%);
          mask-image: radial-gradient(ellipse 70% 66% at 50% 48%,
                        rgba(0,0,0,1) 42%, rgba(0,0,0,.60) 72%, transparent 99%);
  /* The jar stands on the page, so the canvas must not announce itself with
     an edge. No border, no radius, no shadow — the object's own cast shadow
     is the only thing that says where it is. */
  cursor: grab;
  touch-action: pan-y;      /* the page still scrolls under a vertical drag */
}
.museum-hero__vessel canvas:active { cursor: grabbing; }

/* NO CAPTION. "Drag to turn it" is a label explaining an affordance, and a
   label explaining an affordance is an admission that the affordance is not
   legible. cursor: grab says it to anyone who can act on it. */
.museum-hero__vessel figcaption { display: none; }

@media (max-width: 900px) {
  /* One column. The jar goes first, because an object is a better invitation
     into a page about credit than a paragraph is. */
  .museum-hero { grid-template-columns: minmax(0, 1fr); gap: var(--band-pad-y); }
  /* The SAME 4:5 box on a phone, not a square. The shader normalises its
     view by height, so changing the aspect changes how much you see to the
     sides but not above — a square box therefore keeps the vertical framing
     and simply crops the air the composition was tuned with. One aspect
     everywhere means one framing to get right. */
  .museum-hero__vessel { order: -1; }
}


/* ══ THE SCHOOLS HERO ═════════════════════════════════════════════════════
   Two devices from DOCS/"ChatGPT Prompt to Ox Alpha v5.html" (NIGHT DEPOT),
   which is the toys piece in that set. Kevin: "K through 8th grade needs to
   be very, and I mean very artistic and bubbly."

   THE PAIRING is a light italic line in the display face with a much larger
   heavy line under it — two weights, two styles, one sentence broken across
   them. It is the whole reason that hero reads as a picture book rather than
   a landing page, and it costs two rules.

   FRAUNCES STOPS AT 700, not the 900 the original asks for. That is measured,
   not assumed — the variable axis on the file this site ships genuinely ends
   there. 700 against a 300-weight italic is still a two-step jump and still
   reads; asking for 900 would silently render 700 anyway and leave a number
   in the stylesheet that lies about what is on screen.

   WHAT DID NOT COME WITH IT. Night Depot is a nocturne — plum, near-black, a
   train at bedtime. This page is for schools, so the ground stays cream. The
   devices are the loan; the mood is not.                                   */

.pitch {
  font-family: var(--face-display);
  font-optical-sizing: auto;
  margin: 0;
  max-width: 18ch;
}
.pitch__script {
  display: block;
  font-weight: 300;
  font-style: italic;
  font-size: clamp(26px, 3.4vw, 46px);
  line-height: 1.02;
  letter-spacing: -0.01em;
  color: var(--muted);
}
.pitch__script--tail { text-align: right; }
.pitch__caps {
  display: block;
  font-style: normal;
  font-weight: 700;
  font-size: clamp(54px, 9vw, 124px);
  line-height: .88;
  letter-spacing: -0.02em;
  color: var(--ink);
}

/* ══ THE TICKET ═══════════════════════════════════════════════════════════
   A real perforated ticket. The notches are the whole trick: two radial
   gradients used as a MASK, one anchored to each end, each covering 51% of
   the width so they overlap in the middle and never leave a seam. The
   transparent stop at 98% rather than 100% keeps the arc edge from
   shimmering as the element resizes.

   The shadow is offset and hard — 3px 4px with no blur — because a blurred
   shadow says "floating UI card" and a hard one says "printed object sitting
   on paper". That distinction is most of why this does not read as generated.

   NO border-radius ANYWHERE. A ticket has square corners and two bites out of
   its sides; rounding it would turn it back into the pill it is replacing. */

.ticket {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--pad-card);
  padding: 16px 26px 16px 20px;
  min-height: var(--tap-min);
  background: var(--card);
  border: 1.5px solid var(--ink);
  color: var(--ink);
  text-decoration: none;
  font-family: var(--face-ui);
  font-size: var(--size-small);
  font-weight: var(--w-label);
  letter-spacing: .22em;
  text-transform: uppercase;
  box-shadow: 3px 4px 0 var(--ticket-shadow);
  transition: transform var(--dur-base) var(--ease-snap),
              box-shadow var(--dur-base) var(--ease-snap);
  -webkit-mask:
    radial-gradient(circle 9px at 0 50%, transparent 98%, rgba(0,0,0,1)) left/51% 100% no-repeat,
    radial-gradient(circle 9px at 100% 50%, transparent 98%, rgba(0,0,0,1)) right/51% 100% no-repeat;
          mask:
    radial-gradient(circle 9px at 0 50%, transparent 98%, rgba(0,0,0,1)) left/51% 100% no-repeat,
    radial-gradient(circle 9px at 100% 50%, transparent 98%, rgba(0,0,0,1)) right/51% 100% no-repeat;
}
.ticket:hover {
  transform: translateY(-4px) rotate(-1deg);
  box-shadow: 5px 8px 0 var(--ticket-shadow);
}
.ticket:active { transform: scale(.97); }
.ticket:focus-visible { outline: 2px dashed var(--bronze); outline-offset: 4px; }

.ticket__no {
  font-family: var(--face-display);
  font-style: italic;
  font-weight: var(--w-mid);
  font-size: var(--size-h4);
  letter-spacing: 0;
  text-transform: none;
  padding-right: var(--pad-card);
  border-right: 1.5px dashed var(--rule);
  flex: none;
}
.ticket__go { display: inline-flex; align-items: center; gap: 10px; }
.ticket__go svg {
  width: 18px; height: 18px; flex: none;
  transition: transform var(--dur-base) var(--ease-snap);
}
.ticket:hover .ticket__go svg { transform: translateX(5px); }

@media (prefers-reduced-motion: reduce) {
  .ticket, .ticket__go svg { transition: none; }
  .ticket:hover { transform: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE PLATE — VANTARA'S GROUND, MEASURED OFF 08-vantara-hub.html

   The reference does not put photographs behind text and then dim the whole
   thing, which is what a flat 40% black overlay does and why it always looks
   cheap. It lays a HORIZONTAL cream scrim across the picture so the left of
   the band is solid ground the copy sits on, the middle is a wash, and the
   right is the photograph at full strength. The eye reads the sentence and
   then finds the room it is about.

       linear-gradient(90deg,
         GROUND        0%,
         GROUND / 0.92 35%,
         GROUND / 0.55 65%,
         transparent  100%)

   THE STOPS ARE VERBATIM. The colour is not, and could not be: the reference
   mixes this against its own ground of #F5F1EB and ours is #F7F4ED, so
   copying the literal would leave a two-value seam on every edge where the
   plate meets the page. The stops and the alphas are the design; the cream
   is the substrate it is mixed into.

   The second gradient is the reference's too — a bottom-up fade over the
   last 23% so the picture dissolves into the page instead of ending on a
   ruled line.

   WHY THE COPY IS CAPPED: measured, the longest line ends at 39.7% across
   the band, where the scrim is 0.862 opaque. See the note on the two text
   tokens below for what that costs and what it buys.                     */

.band--plate {
  position: relative;
  overflow: hidden;
  background: var(--ground);
  padding-block: var(--plate-pad-y);
}

/* TWO TOKENS STEP DOWN INSIDE A PLATE, AND THIS IS ARITHMETIC, NOT TASTE.
   The scrim is 0.862 opaque where the longest line ends (39.7% across).
     Composite that over the worst photograph anybody could put here — one
     that is black at that point — and the ground under the words is
     rgb(213,210,204). Against it --ink is 11.82:1 and fine, but --muted is
     3.84:1 and --bronze-text is 3.66:1. Both fail AA, and both fail it for
     body copy, which is where --muted does all of its work.

     Darkening them here fixes it for every plate at once, because all 15
     declarations that read either token read it as a colour and nothing
     else — no border, no fill — so the whole cascade moves with them.
     4.62:1 and 4.64:1 against that worst case; 6.34:1 and 6.37:1 on plain
     ground, where --ink is 16.24:1, so the three still read as three.

     These are values for THIS scrim. Change a stop and they are wrong:
     tools/plate-contrast.cjs recomputes them and the gate runs it.

     It is its own rule because the checker that blanks token blocks only
     recognises a block whose declarations are ALL custom properties. */
.band--plate {
  --muted:       #535A64;
  --bronze-text: #7A5024;
}

/* the photograph */
.band--plate::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: var(--plate-img, none);
  background-size: var(--plate-size, cover);
  background-position: var(--plate-pos, 50% 50%);
  background-repeat: no-repeat;
}

/* Vantara's two gradients, in the order the reference stacks them: the
   bottom fade paints over the horizontal scrim, not under it. */
.band--plate::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    linear-gradient(0deg,
      var(--ground) 0%,
      var(--plate-0) 23%,
      var(--plate-none) 100%),
    linear-gradient(90deg,
      var(--ground) 0%,
      var(--plate-92) 35%,
      var(--plate-55) 65%,
      var(--plate-none) 100%);
}

/* the content rides above both */
.band--plate > * {
  position: relative;
  z-index: 2;
}
/* THE COLUMN KEEPS ITS PLACE; ONLY THE MEASURE NARROWS. Capping .col itself
   would work against a centred column — it would pull the text inward from
   both sides and land it in the middle of the band, which is the one part
   of the scrim that is neither solid nor clear. Capping the CHILDREN keeps
   their left edge exactly where every other band on the site puts it, and
   ends them inside the opaque zone.  Vantara's own copy column is narrow
   for this reason and not by taste. */
.band--plate .col > * { max-inline-size: var(--plate-measure); }
/* The tally is three columns, not a paragraph. Held to the prose measure it
   wraps every label onto two lines and reads as a squeezed table. It gets
   the wider bound and still ends inside the scrim. */
.band--plate .col > .tally { max-inline-size: var(--plate-wide); }

/* ── THE PICTURES ─────────────────────────────────────────────────────────
   Named here rather than on the element, because the policy on this site
   forbids a style attribute and a custom property has nowhere else to go.
   Each is positioned by hand: `cover` decides the scale, and which part of
   the frame survives is a judgement about the photograph.

   THREE OF THE FIVE PHOTOGRAPHS IN media/ ARE NOT HERE, and each was cut
   by looking at it at this size rather than at a thumbnail:

     atrium.webp    a legible MALL OF BERLIN sign and two H&M storefronts.
                    Every crop that keeps the glass vault leads the eye down
                    the arcade to the sign; cropping the sign loses the vault.
     notebook.webp  a real published book, title and author both readable.
     mist.webp      a pine forest, which is nowhere anybody studies.

   A reader who can name the mall, or the book, has been told something
   about this company that is not true. Two survive. Until there are more,
   plates go on pages far enough apart that the same picture is never one
   click from itself.                                                    */
[data-plate="light"] { --plate-img: url(media/light.webp); --plate-pos: 64% 46%; }
[data-plate="desk"]  { --plate-img: url(media/desk.webp);  --plate-pos: 72% 38%; }

/* ── NARROW ───────────────────────────────────────────────────────────────
   A horizontal scrim needs horizontal room. Under 52rem there is none, so
   the picture goes behind the copy entirely and the scrim turns vertical —
   solid at the top where the words are, clearing at the bottom. Same
   device, rotated, rather than a photograph nobody can see next to text
   nobody can read.                                                       */
@media (max-width: 52rem) {
  .band--plate .col > * { max-inline-size: none; }
  .band--plate::after {
    background:
      linear-gradient(0deg,
        var(--ground) 0%,
        var(--plate-0) 30%,
        var(--plate-none) 100%),
      linear-gradient(180deg,
        var(--ground) 0%,
        var(--plate-92) 42%,
        var(--plate-55) 72%,
        var(--plate-none) 100%);
  }
}

/* ── THE DOORS ────────────────────────────────────────────────────────────
   Six forms, one page, one visible at a time. There is no navigation between
   them and no script behind them: the chips are ordinary links to fragments
   and :target does the switching. So the state is in the URL, which means
   /contact#door-press is shareable, bookmarkable, and is exactly what the
   retired contact-press page now redirects to.

   ALL SIX FORMS STAY IN THE MARKUP. display:none is a rendering decision;
   the host registers a form by parsing the deployed HTML, so a hidden form
   is still a registered form — where a form assembled by a script is not one
   the host has ever seen.

   WHAT THIS REPLACED. contact.html carried four forms and four separate
   pages carried four more, with institutions and press in both — the same
   host-side form name over two different field sets, which merges two shapes
   of submission into one bucket. And contact-general, the door for somebody
   who simply has a question, was three clicks from the front door: the
   hardest thing on the site to reach, and the one that should have been the
   easiest.                                                                */

.doortabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-btn);
  margin: var(--pad-card) 0 var(--band-pad-y);
}

.doortab {
  font-family: var(--face-ui);
  font-size: var(--size-small);
  line-height: var(--lh-small);
  font-weight: var(--w-label);
  color: var(--muted);
  background: var(--glass);
  border: var(--hair) solid var(--edge);
  border-radius: var(--radius-btn);
  padding: 10px var(--pad-card);
  min-height: var(--tap-min);
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  transition: background var(--dur-base) var(--ease-soft),
              color      var(--dur-base) var(--ease-soft),
              border-color var(--dur-base) var(--ease-soft);
}
.doortab:hover { color: var(--ink); background: var(--card); }

/* THE OPEN DOOR'S TAB, MARKED BY site.js AND NOT BY :has().
   The panels below use plain :target and are correct on every hop. The
   highlight cannot: :has() invalidation runs a step behind a :target
   change in this engine, so the lit tab was always the door you left
   rather than the one you opened — measured across three hops, wrong on
   all three. aria-current is the right attribute for it anyway, since
   these are navigation links and not toggle buttons. */
.doortab[aria-current="true"] {
  background: var(--ink);
  color: var(--ground);
  border-color: var(--ink);
}

/* .door IS ALREADY display:grid ABOVE — a two-column layout, intro left and
   form right. Hiding it and revealing it as  threw that away and
   stacked every open door into one column. Whatever hides it, what comes
   back has to be the grid it was. */
.door {
  display: none;
  scroll-margin-top: calc(var(--nav-top) + var(--nav-h) + var(--pad-card));
}
.door:target { display: block; }

/* the one you land on, before anything has been picked */
.doors--forms:not(:has(.door:target)) #door-institutions { display: block; }

/* With stylesheets off all six show, which is the right degradation for a
   stack of forms — nothing is lost, it is only longer. */



/* ── THE KEY/VALUE SPEC ───────────────────────────────────────────────────
   A definition list, not a table, because these are eight terms and their
   definitions — there is no second axis. blocks.css already has a `.spec`
   table for aria's sheet, and this deliberately does not reuse that name:
   ember.html does not load blocks.css, so a third `.spec` would be a class
   that is styled on one page and bare on another.                       */
.kv {
  margin: var(--band-pad-y) 0 0;
  border-top: var(--hair) solid var(--rule);
}
.kv__row {
  display: grid;
  grid-template-columns: 13rem 1fr;
  gap: var(--gap-card) var(--band-pad-x);
  padding: var(--pad-card) 0;
  border-bottom: var(--hair) solid var(--rule);
}
.kv__k {
  margin: 0;
  font-family: var(--face-ui);
  font-size: var(--size-small);
  line-height: var(--lh-small);
  font-weight: var(--w-label);
  color: var(--ink);
}
.kv__v {
  margin: 0;
  font-family: var(--face-ui);
  font-size: var(--size-small);
  line-height: var(--lh-body);
  color: var(--muted);
  max-inline-size: 62ch;
}
.kv__key {
  font-family: var(--face-mono);
  font-size: 0.9em;
  padding: 1px 6px;
  border: var(--hair) solid var(--rule);
  border-radius: 4px;
}

/* ── THE PARAGRAPH THAT GETS ITS OWN GROUND ───────────────────────────────
   The Bible marks the accommodations row as "the one that matters" and says
   it deserves its own paragraph. A rule down the side rather than a tinted
   panel: the point is emphasis, and a panel would make it look like an
   aside, which is the opposite of what it is.                           */
.claim {
  margin: var(--band-pad-y) 0 0;
  padding-left: var(--pad-card);
  border-left: 2px solid var(--bronze);
  max-inline-size: 62ch;
}
.claim__last { margin-bottom: 0; font-weight: var(--w-label); color: var(--ink); }

/* ── THE PELL LADDER ──────────────────────────────────────────────────────
   A real table: two columns with a header, which is a second axis and is
   what separates this from the list above. The free rows are their own
   tbody so the rule between them can say "and these are different" without
   a class on every row.                                                 */
.ladder {
  inline-size: 100%;
  max-inline-size: 46rem;
  margin: var(--band-pad-y) 0 var(--pad-card);
  border-collapse: collapse;
  font-family: var(--face-ui);
}
.ladder caption { text-align: left; }
.ladder th, .ladder td {
  text-align: left;
  padding: var(--gap-card) var(--gap-card) var(--gap-card) 0;
  border-bottom: var(--hair) solid var(--rule);
  font-size: var(--size-small);
  line-height: var(--lh-small);
}
.ladder thead th {
  font-family: var(--face-ui);
  font-size: var(--size-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  font-weight: var(--w-label);
  color: var(--muted);
  border-bottom-width: 2px;
}
.ladder tbody th {
  font-weight: var(--w-body);
  color: var(--ink);
  inline-size: 60%;
}
.ladder td { color: var(--muted); }
.ladder__n {
  font-family: var(--face-display);
  font-size: var(--size-lead);
  line-height: 1;
  font-weight: var(--w-display);
  color: var(--bronze-text);
}
/* the three free rows are a different kind of row and read as one group */
.ladder__free { border-top: 2px solid var(--rule); }
.ladder__free th, .ladder__free td { color: var(--ink); }

@media (max-width: 52rem) {
  .kv__row { grid-template-columns: 1fr; gap: var(--gap-btn); }
  .ladder tbody th { inline-size: auto; }
  .ladder__n { font-size: var(--size-body); }
}

/* ═══ /founder — 01-hasani-editorial ═══════════════════════════════════════
   A different template on purpose. Vantara is the site's system — cream,
   24px radii, glass, the 100px pill — and it is the wrong register for one
   person's account of themselves. Hasani is editorial: a warmer greige
   ground, near-black ink, a 4px radius where the rest of the site uses 24,
   one large photograph, and a lot of air.

   MEASURED OFF THE TEMPLATE, not taken from the build plan: ground #E9E1D6,
   ink #242424, muted #4A4A4A, accent #A38767. The build plan lists the
   accent as #A7D8FF, a pale blue; that colour does not appear in the file
   at all. The tan does, and it belongs to the same warm family as the rest
   of this site, so it is used and the discrepancy is recorded here.

   THE WHOLE PALETTE TRAVELS TOGETHER, and it has to. Put this ground under
   the site's own text tokens and --muted measures 4.48:1 and --bronze-text
   4.26:1 — both under AA, because the ground is warmer and lighter than the
   cream they were chosen against. Hasani's own ink and muted measure 11.98
   and 6.84. Mixing one template's ground with another's type is how a page
   fails contrast while every colour in it is "approved".

   The accent is 2.61:1 and CANNOT CARRY TEXT. It is rules and fills only.
   Where the eyebrow needs the same hue in words, it uses #7A5E3E — the same
   tan walked down until it clears AA at 4.63:1.                          */

[data-page="founder"] {
  /* THE GROUND OVERRIDE IS GONE. Kevin, 2026-08-24: four grounds across the
     site is the inconsistency, and every click between pages was a colour
     change a visitor feels and cannot name. This page keeps Hasani's TYPE,
     which is what L13 protects — measured on the site cream, its ink is
     14.13:1, its muted 8.07:1 and its accent 5.47:1, all clear of AA. What
     it no longer keeps is a third background. */
  --ink:          #242424;
  --muted:        #4A4A4A;
  --rule:         rgba(163, 135, 103, 0.34);
  --bronze:       #A38767;
  --bronze-text:  #7A5E3E;
  --radius-card:  4px;
  --radius-tile:  4px;
}

.fnd { max-inline-size: 62rem; }

.fnd__name {
  margin: 0 0 var(--band-pad-y);
  letter-spacing: -1.5px;
}

/* the photograph and the opening, side by side, then the text runs on alone */
.fnd__lead {
  display: grid;
  grid-template-columns: 20rem 1fr;
  gap: var(--band-pad-x);
  align-items: start;
}

.fnd__shot { margin: 0; position: sticky; top: calc(var(--nav-top) + var(--nav-h) + var(--pad-card)); }

/* 4:5, and empty rather than filled with somebody who is not him — the same
   decision /about already made and for the same reason */
.fnd__frame {
  aspect-ratio: 4 / 5;
  inline-size: 100%;
  border: var(--hair) solid var(--rule);
  border-radius: var(--radius-card);
  background:
    repeating-linear-gradient(45deg,
      rgba(163, 135, 103, 0.07) 0 10px,
      rgba(163, 135, 103, 0) 10px 20px);
  margin-bottom: var(--gap-card);
}

.fnd__text { min-inline-size: 0; }

/* the first line is the thesis and is set larger than what follows */
.fnd__open {
  font-size: var(--size-lead);
  line-height: var(--lh-lead);
  color: var(--ink);
  margin-bottom: 1.1em;
}

/* ── THE PARAGRAPH THAT MAKES IT NOT A FOUNDER PAGE ───────────────────────
   Bible §5.2 calls it the single most on-brand thing the company could
   publish. It is set apart by a rule and its own ground rather than by a
   heavier type: the point is that it is a caveat, and a caveat that shouts
   is a boast.                                                            */
.fnd__limits {
  margin-top: calc(var(--band-pad-y) * 1.5);
  padding: var(--band-pad-x);
  border: var(--hair) solid var(--rule);
  border-radius: var(--radius-card);
  background: rgba(255, 255, 255, 0.42);
  max-inline-size: 60ch;
}
.fnd__limits-h {
  margin: 0 0 var(--pad-card);
  font-family: var(--face-ui);
  font-size: var(--size-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  font-weight: var(--w-label);
  color: var(--bronze-text);
}
.fnd__limits .body:last-child { margin-bottom: 0; }

@media (max-width: 52rem) {
  .fnd__lead { grid-template-columns: 1fr; gap: var(--pad-card); }
  .fnd__shot { position: static; max-inline-size: 18rem; }
  .fnd__limits { padding: var(--pad-card); }
}

/* ═══ THE TONES — ported from index.html, S1 §2 ════════════════════════════
   THE DIAGNOSIS THIS FIXES, in one measurement: index.html carries eight
   toned sections out of fifteen — three sheet, three sand, two ink. Every
   other page on the site measured `tones: none`. That is the whole of
   "the landing page was composed, the others were filled in": a reader
   walking a page with one ground has no way to tell where a section ended,
   so fifteen correct sections read as one long document.

   NOTHING HERE IS INVENTED. The four grounds, their values and the doubled
   selector are lifted from landing.css, which is the page Kevin approved.
   This is the extraction S1 §2 asks for — naming what is already there.

   THE SELECTOR IS DOUBLED ON PURPOSE, and the reason is landing.css's, kept
   verbatim because it is still true here: [data-tone] and .band are both
   specificity 0,1,0, so whichever is declared later wins on source order.
   Repeating the attribute takes it to 0,2,0 without changing what it
   matches, so a tone beats the band wherever it is written and whatever is
   appended between them later.                                          */

:root {
  --tone-sheet:     #FFFFFF;
  --tone-sand:      #F1EADD;
  --tone-ink:       #121820;
  --tone-sand-card: #FAF6EE;
  --tone-sand-line: rgba(182, 138, 94, 0.22);
  --tone-on-ink:    #F4F1EA;
  --tone-on-ink-mut: rgba(244, 241, 234, 0.72);
  --tone-ink-card:  rgba(255, 255, 255, 0.06);
  --tone-ink-line:  rgba(244, 241, 234, 0.18);
}

[data-tone="sheet"][data-tone="sheet"] { background: var(--tone-sheet); }

[data-tone="sand"][data-tone="sand"] {
  /* between the cream and the white — warm enough to read as a different
     room, close enough that the join is a fold rather than a border */
  background: var(--tone-sand);
  --card: var(--tone-sand-card);
  --rule: var(--tone-sand-line);
}

[data-tone="ink"][data-tone="ink"] {
  /* var(--tone-ink), NOT var(--ink). A custom property referenced inside the
     block that REDEFINES it resolves to the NEW value — landing.css hit this
     and painted a band in its own text colour at 1.00:1. Separate token
     names make that mistake unavailable here. */
  background: var(--tone-ink);
  color: var(--tone-on-ink);
  --ink:   var(--tone-on-ink);
  --muted: var(--tone-on-ink-mut);
  --card:  var(--tone-ink-card);
  --rule:  var(--tone-ink-line);
  --edge:  var(--tone-ink-line);
  /* THE BRIGHT bronze, not the text one: measured on ink, #8A6034 is 3.23:1
     and #B68A5E is 5.76:1. The token flips with the ground, which is the
     entire reason it is a token. */
  --bronze-text: var(--bronze);
}

/* THE BUTTONS INVERT WITH THE GROUND, and this is the trap landing.css
   documents, hit again from the other side. .btn--secondary is
   "background: var(--ink); color: var(--card)". On an ink tone --ink IS
   the cream text token, so the fill became cream on a cream label and the
   walker measured it at 1.01:1 — an invisible button on the one section
   chosen to stop a reader.

   Redefining a token cannot fix a rule that reads two tokens which both
   moved. The variants are restated for this ground instead. */
[data-tone="ink"][data-tone="ink"] .btn--secondary {
  background: var(--tone-on-ink);
  color: var(--tone-ink);
}
[data-tone="ink"][data-tone="ink"] .btn--tertiary {
  background: rgba(244, 241, 234, 0.10);
  color: var(--tone-on-ink);
}
[data-tone="ink"][data-tone="ink"] .btn--primary {
  /* bronze fill stays; its LABEL needs the dark ink, not the flipped one */
  background: var(--bronze);
  color: var(--tone-ink);
}

/* A toned band carries its own ground, so it must not also inherit the
   plate's photograph — a tone and a plate are two answers to the same
   question and a section may only give one. */
[data-tone].band--plate::before,
[data-tone].band--plate::after { display: none; }

/* ═══ /investors ══════════════════════════════════════════════════════════
   The conversion surface. Every rule here implements a mechanism named in
   THE-RUN §1 and the mechanism is stated beside it.                     */

/* ── ③ THE SPECIFICITY STRIP ──────────────────────────────────────────────
   There are no logos and no customers. The substitute that works is a
   number somebody can go and check, so every figure carries the page that
   proves it as a live link rather than as a footnote. */
.proof {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--band-pad-x);
  margin: var(--band-pad-y) 0 0;
  padding: 0;
  list-style: none;
}
.proof__i {
  padding-top: var(--pad-card);
  border-top: 2px solid var(--rule);
}
.proof__i .numeral { display: block; font-size: var(--size-num); line-height: 1; }
.proof__k {
  margin: var(--gap-card) 0 var(--gap-btn);
  font-family: var(--face-ui);
  font-size: var(--size-body);
  line-height: var(--lh-small);
  font-weight: var(--w-label);
  color: var(--ink);
}
.proof__s {
  margin: 0;
  font-family: var(--face-ui);
  font-size: var(--size-small);
  line-height: var(--lh-small);
  color: var(--muted);
}
.proof__s a { color: var(--bronze-text); text-decoration: underline; text-underline-offset: 2px; }
.proof__s a:hover { color: var(--ink); }

/* ── ⑧ THE FORM ─────────────────────────────────────────────────────────── */
.inv { max-inline-size: 46rem; margin-top: var(--band-pad-y); }

/* ── L5 · THE OPENERS CASCADE, THEY DO NOT DROP ───────────────────────────
   grid-template-rows 0fr → 1fr is the whole trick, and it is index.html's:
   the list grows out of the control's own body instead of appearing under
   it as a tray. The four durations are landing.css's, unchanged. */
.inv__act { margin-bottom: var(--gap-card); }

.inv__act-t {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-btn);
  min-height: var(--tap-min);
  padding: 8px var(--pad-card);
  font-family: var(--face-ui);
  font-size: var(--size-small);
  font-weight: var(--w-label);
  color: var(--muted);
  background: var(--glass);
  border: var(--hair) solid var(--edge);
  border-radius: var(--radius-btn);
  cursor: pointer;
  transition: color var(--dur-base) var(--ease-soft),
              background var(--dur-base) var(--ease-soft);
}
.inv__act-t:hover { color: var(--ink); background: var(--card); }
.inv__act-t svg { inline-size: 15px; block-size: 15px; transition: transform var(--act-row) var(--act-ease); }
.inv__act[data-on] .inv__act-t { color: var(--ink); background: var(--card); }
.inv__act[data-on] .inv__act-t svg { transform: rotate(180deg); }

.inv__act-l {
  display: grid;
  grid-template-rows: 0fr;                 /* index.html's mechanism */
  opacity: 0;
  transition: grid-template-rows var(--act-open) var(--act-ease),
              opacity var(--act-swap) var(--act-ease);
}
.inv__act[data-on] .inv__act-l { grid-template-rows: 1fr; opacity: 1; }

/* the row that is animating out of a 0fr track still needs somewhere to be
   clipped, and min-height:0 is what lets a grid item shrink below content */
.inv__act-l > * { min-height: 0; }
.inv__act-l { overflow: hidden; }

.inv__act-r {
  display: block;
  inline-size: 100%;
  text-align: left;
  min-height: var(--tap-min);
  margin-top: 6px;
  padding: 9px 12px;
  border: 0;
  border-radius: 9px;
  background: none;
  font-family: var(--face-ui);
  font-size: var(--size-small);
  font-weight: var(--w-label);
  color: var(--ink);
  cursor: pointer;
  opacity: 0;
  transform: translate3d(0, var(--act-lift), 0);
  transition: opacity var(--act-row) var(--act-ease),
              transform var(--act-row) var(--act-ease),
              background var(--act-quick) var(--act-ease);
}
.inv__act[data-on] .inv__act-r { opacity: 1; transform: translate3d(0, 0, 0); }
.inv__act-r:hover { background: rgba(18, 24, 32, .06); }

/* ── ⑧ optional detail is collapsed, never a gate ─────────────────────── */
.inv__more { margin: var(--pad-card) 0; }
.inv__more summary {
  font-family: var(--face-ui);
  font-size: var(--size-small);
  font-weight: var(--w-label);
  color: var(--muted);
  cursor: pointer;
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
}
.inv__more summary:hover { color: var(--ink); }

/* ── ⑨ FITTS'S LAW ────────────────────────────────────────────────────────
   The primary action is the largest interactive target on the page and the
   highest-contrast one. Ink fill, not bronze on cream — the brief measured
   the old Send button as the LOWEST-contrast control on its page. 56px is
   its floor, and the label says what happens rather than "Send". */
.inv__send {
  min-height: 56px;
  padding-inline: var(--band-pad-x);
  font-size: var(--size-body);
}

@media (max-width: 52rem) {
  .proof { gap: var(--pad-card); }
  .inv__send { inline-size: 100%; justify-content: center; }
}

/* the single collapsing track — see the note in investors.js */
.inv__act-in { min-height: 0; }

/* the "optional" mark smartfill.js adds to labels that are not required. It
   is in the LABEL rather than a placeholder, because a placeholder vanishes
   the moment somebody types and the reassurance is needed most while they
   are deciding whether to skip the field. */
.form__opt {
  margin-left: var(--gap-btn);
  font-size: var(--size-tag);
  letter-spacing: var(--ls-tag);
  text-transform: uppercase;
  font-weight: var(--w-label);
  color: var(--muted);
  opacity: .8;
}

/* ── A KEY/VALUE LINK IS THE WHOLE VALUE, NOT A WORD IN A SENTENCE ─────────
   A link inside running prose is deliberately left alone — padding it breaks
   the line box, and the guidance exempts it for that reason. A link that IS
   the value of a row is not that: on /investors, "/ember" and "Who that is"
   are the entire contents of their cell and measured 18px tall. */
@media (pointer: coarse) {
  .kv__v a {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap-min);
  }
}

/* ── THE MICRO LABELS GET ONE STEP UP ON A PHONE ───────────────────────────
   10px uppercase is a fine chip on a 27-inch display and is at the edge of
   legible held at arm's length. These are labels, not reading text, so the
   step is one pixel rather than a rewrite of the scale — enough to clear the
   floor the audit checks, not enough to change the composition. */
@media (pointer: coarse) {
  .badge, .term__tag { font-size: 11px; }
  /* "optional" is a word in a label, not a chip — it gets the reading floor */
  .form__opt { font-size: 12px; }
}

/* "lines 31–52 of 354" is the citation that tells a reader WHICH part of
   somebody else's file they are looking at. It is a sentence, not a chip. */
@media (pointer: coarse) {
  .term__meta { font-size: 12px; }
}

/* the two terminal buttons are lowercase words in a 44px box; the side note
   under a code panel is a tracked caps label that was one pixel under even
   the label floor */
@media (pointer: coarse) {
  .term__btn { font-size: 12px; }
  .term__side { font-size: 11px; }
}

/* ── THE LAST THREE, ALL ONE PIXEL UNDER ───────────────────────────────────
   .station code is the inline hex and selector inside a record — "#F7F4ED",
   "nth-last-child(1)". Monospace runs optically smaller than the prose around
   it, so a shared value that reads level on a desktop reads under on a phone.

   .room__n is "18 imports" — the count that tells a reader how big the room
   they just walked into is.

   .l-num is the running figure on /aria. A number nobody can read is not a
   number. */
@media (pointer: coarse) {
  .station code { font-size: 12px; }
  .room__n { font-size: 11px; }
  .l-num, .num-live { font-size: 12px; }
}

/* A licence is text somebody may actually have to read. .legal__text is a
   <pre>, so it already scrolls rather than reflows, and a pixel of size costs
   nothing but the horizontal distance it was going to travel anyway. */
@media (pointer: coarse) {
  .legal__text { font-size: 12px; }
}

/* ── ONE SENTENCE PER INPUT ────────────────────────────────────────────────
   Instructions that name a key are wrong on a phone and instructions that
   name a swipe are wrong at a desk. Both ship; the device picks. Not
   display:none on the losing one alone — that would leave BOTH in the
   accessibility tree for a screen reader to read out twice. */
.only-coarse { display: none; }
@media (pointer: coarse) {
  .only-fine { display: none; }
  .only-coarse { display: inline; }
}
