/* HERMOD SYSTEMS — house style.
   Palette: Steep's role structure, hue-rotated to HERMOD violet (OKLCH H 284).
   Every value solved against a WCAG target; see docs/company-context/.
   SITE-BLUEPRINT §五: dark theme required, tokens defined twice. */

:root {
  --canvas:#FAFAFE; --raised:#FFFFFF; --mist:#F1F2F7;
  --ink:#18181F; --muted:#65656E; --faint:#71727B;
  --wash:#E3E4FF; --wash-ink:#373463;
  --fill:#573EC9; --on-fill:#FFFFFF;
  --rule:#DDDDE5; --rule-strong:#C6C6CF;
  /* --edge: any border a user is meant to perceive as an affordance.
     SC 1.4.11 wants >=3:1 for non-text UI. --rule-strong is decorative and
     deliberately below that; do not swap one for the other. */
  --edge:#8B8B90;
  --red:#B3403C;
  --ring:rgba(24,24,31,.09);

  --serif:"Source Serif 4",Tiempos Headline,Georgia,ui-serif,serif;
  --sans:"Public Sans","Helvetica Neue",Arial,sans-serif;

  --gap:80px;          /* section rhythm */
  --measure:66ch;

  /* ── Type scale: 9 faces, measured off steep.app (DESIGN-PROVENANCE §七).
     Display holds line-height 1.30 at EVERY size and tracks in two tiers.
     Body tracks at 0 and never goes below 14px. Adding a tenth size is a
     decision, not a convenience -- the previous system had 24. ── */
  --d1:clamp(40px,5.4vw,68px);  /* hero            serif 400 / 1.30 / -.025em */
  --d2:clamp(30px,3.2vw,44px);  /* section head    serif 400 / 1.30 / -.015em */
  --d3:30px;                    /* card head       serif 400 / 1.30 / -.015em */
  --t1:20px;                    /* lede, large body        / 1.50 / 0 */
  --t2:18px;                    /* control label, emphasis / 1.50 / 0 */
  --t3:17px;                    /* long-form prose         / 1.60 / 0 */
  --t4:16px;                    /* body default            / 1.50 / 0 */
  --t5:15px;                    /* small UI                / 1.50 / 0 */
  --t6:14px;                    /* meta floor              / 1.45 / tracked when caps */
  --r-card:24px; --r-small:16px; --r-img:12px; --r-pill:9999px;

  /* ── Motion: four durations, three curves, one distance. ──────────────
     These deliberately get NO dark-mode counterpart. The house rule is
     "tokens are defined twice" (SITE-BLUEPRINT §五), but a duration and an
     easing are theme-independent -- only colour is defined twice. That
     exception is written here so the next reader does not "fix" it.
     --e-out has both y control points inside 0..1, so it is mathematically
     incapable of overshoot. That is what keeps this out of springy-startup
     territory, and it is checkable by reading the four numbers. */
  --m-press:70ms;   /* press and release. Under ~100ms a change still reads
                       as caused by your own hand; above it the page reads as
                       thinking. Shortest in the system, and the only
                       duration allowed on a transform. */
  --m-state:150ms;  /* hover, focus, and every colour or border change. Also
                       the duration that sits on RESTING declarations -- see
                       the structural note above `a` below. */
  --m-move:260ms;   /* anything that changes position or length: a hairline
                       wiping across a row, the transit veil arriving. */
  --m-cine:420ms;   /* the flight stage's own tempo. NOT a new number: this
                       is the .42s that was hardcoded on #copycard. The stage
                       may be slower than the chrome because it is film. */
  --e-out:cubic-bezier(.2,0,0,1); /* entering, or changing state. Fast
                       departure, long settle. The default. */
  --e-in:cubic-bezier(.4,0,1,1);  /* exits only, accelerating away. Used on
                       the resting state of the two things that leave: the
                       transit veil and the suppressed copy card. */
  --e-flat:linear;  /* progress and scrub ONLY (#flightbar). An eased
                       progress indicator misreports the rate of the thing it
                       measures; the bar is a measurement, not an animation.
                       The only place `linear` is permitted. */
  --press-shift:1px;/* the single displacement in the system, and it goes
                       INTO the page. Nothing lifts: cards are box-shadow:0
                       by house rule, and a lift with no shadow reads as a
                       glitch. A token, not a literal, so reduced motion can
                       zero it in one line. */
}

@media (prefers-color-scheme: dark) {
  :root {
    --canvas:#0F0F19; --raised:#1D1D27; --mist:#171722;
    --ink:#EAEAF2; --muted:#9A9AA8; --faint:#85858A;
    --wash:#2B2A48; --wash-ink:#C6C4F5;
    --fill:#9A96FE; --on-fill:#0F0F19;
    --rule:#262634; --rule-strong:#333344;
    --edge:#68686C;
    --red:#E8837E;
    --ring:rgba(234,234,242,.11);
  }
}

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

html { scroll-behavior:smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior:auto; }
  *,*::before,*::after { animation:none !important; transition:none !important; }
  /* The kill switch removes the TRANSITION, not the STATE. Every :hover,
     :focus-visible and :active in this file is a plain static property
     change, so in here the press still lands -- instantly, with no tween.
     That is deliberate: reduced motion must never mean reduced feedback.
     The check on anything added later is that no state may be expressed
     ONLY as a transition or a keyframe, because such a state disappears
     entirely under this query.
     The one thing worth removing is the displacement. Every press also
     carries a colour change -- the ring thickens, the fill deepens, the
     hairline doubles -- so press stays fully legible at zero offset. */
  :root { --press-shift:0px; }
}

/* ── WHAT MUST NOT MOVE ────────────────────────────────────────────────
   Naming the exclusions is half of a motion system.
   · .skip:focus snaps instantly. The skip link never animates.
   · .ph-block and .ph never animate and never answer hover. A placeholder
     must read as unfinished, not as decorated.
   · Labels are not controls: .badge, .tag, .eyebrow, .rule-no, .row-meta
     and .work-kind get nothing.
   · Non-interactive .card blocks (the two service cards) get no hover.
     They are not clickable and must not pretend to be.
   · Figures are static. No draw-on-scroll of the inline SVGs.
   · #screen is never CSS-animated. It is scrubbed by scroll and by nothing
     else.
   · No section reveal, ever. The site's entire scroll budget is spent on
     the flight, and a second scroll-driven effect would compete with it.
   This file declares no key-frame animation and must not start: every
   transition below is caused by a pointer, a key, or scroll position, so
   nothing here runs unattended. Anything that moves on its own is out. */

body {
  margin:0; background:var(--canvas); color:var(--ink);
  font-family:var(--sans); font-weight:400; font-size:var(--t4); line-height:1.50;
  -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
}

img { max-width:100%; height:auto; display:block; }

.skip {
  position:absolute; left:-9999px; top:0; background:var(--fill); color:var(--on-fill);
  padding:10px 18px; border-radius:0 0 var(--r-small) 0; z-index:20;
}
.skip:focus { left:0; }

/* THE ONE STRUCTURAL RULE: there is no `* { transition }` in this file and
   there must not be. Every component declares its own transition, on named
   properties only, on its RESTING selector. Two things follow. A property
   not named in a list cannot silently animate -- the file stays honest about
   what it costs. And because the resting declaration governs the LEAVING
   direction, exits are automatically at or faster than entrances without an
   exit-duration token: a state that wants a slower entrance overrides the
   duration on itself. No compositor hints are declared anywhere either --
   opacity and transform are already composited without being asked. */
a {
  color:var(--wash-ink); text-decoration:none; border-bottom:1px solid var(--edge);
  transition:border-bottom-color var(--m-state) var(--e-out),
             background-color var(--m-press) var(--e-in),
             color var(--m-state) var(--e-out);
}
a:hover, a:focus-visible { border-bottom-color:currentColor; }
/* Press marks the link like a highlighted passage, using the --wash pair the
   badge already carries (9.17:1 light, 8.26:1 dark). An inline background
   changes no geometry, so nothing reflows under the finger.
   :not(.btn) is load-bearing -- on the home and method pages the CTA buttons
   sit inside <p class="cta-row">, so `p a` would otherwise catch them.
   A bare `li a` is deliberately NOT in this list: .entry-list rows are block
   anchors with 30px of padding, and washing a whole row would drop the
   --faint timestamp inside it below AA. Those rows get their hairline. */
.prose a:not(.btn):active, p a:not(.btn):active, .mailto:active {
  background-color:var(--wash); color:var(--wash-ink); border-bottom-color:currentColor;
}
/* No border-radius in here. It used to be 6px, and `a:focus-visible` at
   specificity (0,1,1) beats `.btn` at (0,1,0) -- so keyboard-focusing a pill
   button changed its computed radius from 9999px to 6px and the button
   visibly squared off next to its unfocused neighbour. The outline follows
   each element's own radius by itself in every current browser.
   The ring is on plain tokens, never inside a color-mix: an unsupported
   color-mix drops the declaration, and a focus indicator may never be the
   thing that fails to paint. */
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible {
  outline:2px solid var(--fill); outline-offset:3px;
}

/* ---- type: serif display, always weight 400 ---- */
h1,h2,h3 { font-family:var(--serif); font-weight:400; margin:0; text-wrap:balance; }
h1 { font-size:var(--d1); line-height:1.30; letter-spacing:-.025em; }
h2 { font-size:var(--d2); line-height:1.30; letter-spacing:-.015em; }
h3 { font-size:var(--d3); line-height:1.30; letter-spacing:-.015em; }
p { margin:0 0 18px; }

.eyebrow {
  font-family:var(--sans); font-size:var(--t6); font-weight:500; letter-spacing:.10em;
  text-transform:uppercase; color:var(--muted); margin:0 0 14px;
}
.lede {
  font-family:var(--serif); font-weight:400; font-size:var(--t1);
  line-height:1.50; letter-spacing:0; color:var(--muted); max-width:54ch;
}

/* ---- placeholders must look unfinished on purpose (BRIEF §五) ---- */
.ph-block, .ph {
  font-family:var(--sans); font-size:var(--t6); line-height:1.45; color:var(--red);
  border:1px dashed var(--red); border-radius:var(--r-small);
  padding:10px 14px; margin:14px 0; background:transparent;
}
.ph { display:inline-block; padding:3px 10px; font-size:var(--t6); margin-left:8px; border-radius:var(--r-pill); }

/* ---- shell ---- */
/* The shell. Every direct child of <main> gets it, so a new block cannot
   silently ship full-bleed the way .contact-grid did. */
.masthead, main > section, main > article, main > ol, main > div, .site-footer {
  max-width:1200px; margin-inline:auto; padding-inline:clamp(20px,5vw,40px);
}
.masthead {
  display:flex; align-items:center; justify-content:space-between; gap:24px;
  padding-block:26px; flex-wrap:wrap;
}
.lockup { display:flex; align-items:center; gap:11px; border:0; transition:transform var(--m-press) var(--e-out); }
.lockup img { width:30px; height:32px; }
.wordmark { font-family:var(--sans); font-weight:500; font-size:var(--t5); letter-spacing:.05em; color:var(--ink); }
.wordmark span { font-weight:400; color:var(--muted); transition:color var(--m-state) var(--e-out); }
/* The wordmark ships as HERMOD in --ink plus SYSTEMS in --muted. On hover the
   name completes itself: one colour change, no layout, and it is the brand
   doing the talking rather than an imported affordance. */
.lockup:hover .wordmark span, .lockup:focus-visible .wordmark span { color:var(--ink); }
.lockup:active { transform:translateY(var(--press-shift)); }
.masthead nav { display:flex; gap:22px; }
.masthead nav a {
  font-size:var(--t5); font-weight:450; color:var(--muted); border:0;
  border-bottom:1px solid transparent; padding-bottom:2px;
  position:relative;
  transition:color var(--m-state) var(--e-out), transform var(--m-press) var(--e-out);
}
/* The underline is a scaled pseudo-element rather than a border colour, so it
   wipes left to right in reading direction instead of appearing all at once.
   The transparent border and the 2px padding stay for layout stability.
   The current page's link is simply already at the terminal state, which is
   honest rather than a special case. */
.masthead nav a::after {
  content:''; position:absolute; left:0; right:0; bottom:0; height:1px;
  background:var(--fill); transform:scaleX(0); transform-origin:left;
  transition:transform var(--m-move) var(--e-out);
}
.masthead nav a:hover, .masthead nav a:focus-visible, .masthead nav a[aria-current] { color:var(--ink); }
.masthead nav a:hover::after, .masthead nav a:focus-visible::after, .masthead nav a[aria-current]::after { transform:scaleX(1); }
.masthead nav a:active { color:var(--ink); transform:translateY(var(--press-shift)); }

main > section, main > article { padding-block:var(--gap) 0; }
main > ol { padding-block:0; }

/* ---- buttons: Steep pills ---- */
.btn {
  display:inline-flex; align-items:center; font-family:var(--sans);
  font-size:var(--t2); font-weight:450; line-height:1;
  height:44px; padding:0 20px;           /* 44px is the touch-target floor, WCAG 2.5.5 */
  border-radius:var(--r-pill); border:0;
  background:var(--fill); color:var(--on-fill); cursor:pointer;
  transition:background-color var(--m-state) var(--e-out),
             transform var(--m-press) var(--e-out);
}
/* filter:brightness is gone. A filter cannot know which way contrast is
   moving. Mixing toward --ink is self-correcting instead: in light theme ink
   is dark so the fill darkens, in dark theme ink is light so the same
   declaration brightens, and in BOTH directions the fill moves away from
   --on-fill, so contrast only ever rises.
   Measured: base 7.15:1 light / 7.41:1 dark, hover 7.76 / 7.93,
   press 8.47 / 8.48. */
.btn:hover:not(:disabled), .btn:focus-visible {
  background:color-mix(in oklab, var(--fill) 92%, var(--ink)); border-bottom-color:transparent;
}
.btn:active:not(:disabled) {
  background:color-mix(in oklab, var(--fill) 84%, var(--ink));
  transform:translateY(var(--press-shift));
}
/* Secondary is a 5% tint of the ink, not a stroke. A stroke would owe SC 1.4.11
   its 3:1 while the hairline token owes the opposite -- the tint has no such debt. */
.btn.ghost { background:color-mix(in srgb, var(--ink) 6%, transparent); color:var(--ink); border:0; }
.btn.ghost:hover:not(:disabled), .btn.ghost:focus-visible { background:color-mix(in srgb, var(--ink) 10%, transparent); }
.btn.ghost:active:not(:disabled) {
  background:color-mix(in srgb, var(--ink) 14%, transparent);
  transform:translateY(var(--press-shift));
}
/* The dead state, drawn. Until now there was no :disabled rule in this file
   at all, so the contact page's unwired submit computed a full --fill
   background with cursor:pointer -- it looked exactly like a live primary
   button, and the only thing saying otherwise was the red note above it.
   --muted, not --faint: on this ground --faint measures 4.08:1 light and
   3.71:1 dark. WCAG exempts inactive controls; the house rule does not, and
   the house rule is stricter, so the house rule wins. --muted measures
   4.83:1 light and 4.82:1 dark. Declared last so it beats .btn.ghost. */
.btn:disabled {
  background:color-mix(in srgb, var(--ink) 8%, transparent); color:var(--muted);
  cursor:not-allowed; transform:none;
}
.cta-row { display:flex; gap:12px; flex-wrap:wrap; align-items:center; margin-top:26px; }

.card {
  background:var(--raised); border-radius:var(--r-card); padding:28px 30px;
  box-shadow:inset 0 0 0 1px var(--ring);
}

/* the frame is a light clay photograph in both themes, so overlaid text is fixed dark */
/* below 900px the copy stacks, so the frame's reserved negative space is dead
   weight -- crop it away and keep the subject. */
@media (max-width:899px) {
  .hero-frame img { aspect-ratio:4/3; object-fit:cover; object-position:22% center; }
}

@media (min-width:900px) {
  .hero-stage .hero-copy {
    position:absolute; top:50%; right:4%; transform:translateY(-50%);
    width:38%; max-width:44ch; color:#18181F;
  }
  .hero-stage .hero-copy .eyebrow,
  .hero-stage .hero-copy .hero-sub,
  .hero-stage .hero-copy .hero-plain { color:#54545E; }
  .hero-stage .hero-copy .btn.ghost { color:#18181F; border-color:rgba(24,24,31,.28); }
}
.services { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-top:30px; }
/* h3 because the section now has its own h2 above it. Base h3 is already --d3,
   so the old font-size declaration was redundant the moment the level was right. */
.services h3 { margin-bottom:8px; }
@media (max-width:760px) { .services { grid-template-columns:1fr; } }

/* Deleted here: an unwired fade-up-on-scroll on .gap-line / .rule. Neither
   class appears in any built page and nothing in the codebase adds .on to
   them -- the only classList.add("on") is flight.js:222, on the copy card.
   Deleted rather than revived: a fade-up reveal is precisely the motion the
   brief rules out, and as written it would have parked live content at
   opacity .42 the moment the markup grew the class. */
.rule-no {
  display:block; font-family:var(--sans); font-size:var(--t6); font-weight:500;
  letter-spacing:.10em; text-transform:uppercase; color:var(--fill); margin-bottom:12px;
}
/* ---- after the flight: services, work, about, diaries ----
   The rule is the film-to-document transition, so it belongs on whatever
   section comes first out of the cinema -- that is #services now. */
#services { border-top:1px solid var(--rule); margin-top:var(--gap); }

.work-list { list-style:none; margin:34px 0 0; padding:0; display:grid; gap:34px; }
/* --muted sits on the ITEM, not on `.work-item p`. As `.work-item p` it was
   (0,1,1) and beat every label class inside it at (0,1,0), so the .ph-block in
   the client entry painted muted grey at body size inside a red dashed
   border -- a placeholder that has stopped reading as one. Inherited colour
   loses to any class that states its own, so now .ph-block stays red, .eyebrow
   stays a label and .work-kind stays --faint, with no :not() chain holding it
   together. font-size and line-height came out with it: both were restating
   the body defaults. */
.work-item { border-top:1px solid var(--rule); padding-top:26px; color:var(--muted); }
.work-item h3 { color:var(--ink); margin-bottom:6px; }
.work-item p { max-width:var(--measure); margin:0 0 12px; }
.work-item p:last-child { margin-bottom:0; }
.work-item .work-kind { color:var(--faint); font-size:var(--t5); margin-bottom:14px; }
/* "What can be checked", on every item. One label, asked of all four, so an
   entry with nothing to show answers the same question as the ones that do
   instead of quietly changing the subject to a different kind of claim. */
.work-item .eyebrow { margin:18px 0 6px; }

/* Covers. Screenshots of each project's own public site, captured headlessly and
   cropped here -- so they are evidence of the same kind as the "what can be
   checked" line under them, not decoration. Held to the text measure rather than
   run full-bleed: a wide banner would outrank the sentence that qualifies it.
   aspect-ratio + object-fit means a re-shot cover at a different window size
   drops in without touching this file or reflowing the page. */
/* No forced aspect-ratio. Each cover is cropped to the ONE thing on that site
   worth reading -- an outcome line, a headline -- and a shared ratio would crop
   it a second time. Uneven heights are the cost of every cover being legible at
   this column width, and legibility is the whole point: a screenshot nobody can
   read is decoration, and this section is called "what can be checked". */
.work-cover {
  width:100%; max-width:var(--measure); height:auto;
  border-radius:var(--r-img); box-shadow:inset 0 0 0 1px var(--ring);
  margin:0 0 18px;
}
/* The fourth entry has no site to photograph. It gets the same box, saying so,
   rather than a stand-in image -- an entry with nothing to show should look
   different from one that has something. */
.work-cover-none { max-width:var(--measure); margin-bottom:18px; }

.about-body { color:var(--muted); max-width:var(--measure); }
/* The portrait is white line on a black ground, baked in. On the light canvas
   that makes it the darkest thing on the page, which is where the eye should
   go: the whole site argues that a human sits at the top of the structure. Not
   inverted and not cut out -- it is the artwork as the author uses it. */
.about-portrait {
  width:180px; height:180px; border-radius:50%;
  margin:0 0 26px; box-shadow:inset 0 0 0 1px var(--ring);
}
/* Both placeholders in #about are full paragraphs of dashed red. Held to the
   same measure as the prose they interrupt they read as a gap in the text; left
   at shell width they read as a banner. */
.about .ph-block { max-width:var(--measure); }

.entry-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:16px; margin-top:30px; }
.entry-card {
  background:var(--raised); border-radius:var(--r-card); padding:26px 28px; border:0;
  box-shadow:inset 0 0 0 1px var(--ring); color:var(--ink); display:block;
  transition:box-shadow var(--m-state) var(--e-out), transform var(--m-press) var(--e-out);
}
.entry-card:hover, .entry-card:focus-visible { box-shadow:inset 0 0 0 1px var(--fill); }
/* Press thickens the ring the card already grows on hover. No background tint
   on press -- rejected on measurement: a 4% --fill wash drops --faint to
   4.33:1 in dark, and the same treatment on .adj (which does carry --faint
   text) would fail AA outright. */
.entry-card:active { box-shadow:inset 0 0 0 2px var(--fill); transform:translateY(var(--press-shift)); }
.entry-card h3 { margin-bottom:10px; }
.entry-card p { color:var(--muted); font-size:var(--t4); line-height:1.50; margin-bottom:14px; }
/* inline-block is required: a transform does nothing on an inline box. It is
   the only box-type change in this work. */
.entry-card .more {
  font-size:var(--t5); font-weight:450; color:var(--wash-ink);
  display:inline-block; transition:transform var(--m-move) var(--e-out);
}
/* One axis, 3px, no return bounce. The label already ends in an arrow, so the
   motion is describing the link's own promise. The only directional cue in
   the system. */
.entry-card:hover .more, .entry-card:focus-visible .more { transform:translateX(3px); }

/* ---- writing index ---- */
/* Keep the shell box so the head lines up with everything under it; narrow the
   TEXT, not the container. Narrowing the container makes it centre itself and
   the H1 drifts right of the content below (measured 296px at 1440). */
.page-head h1, .page-head .lede, .page-head > p { max-width:62ch; }
.entry-list { list-style:none; margin-block:48px 0; }
/* Two different lines, on two different tokens, and they must not be confused.
   The RESTING hairline is structure: it separates one row from the next and
   nothing more, so it is --rule, the decorative token that is deliberately
   under 3:1 (see the palette note at the top of this file). What identifies
   the control is the row's own heading text. The INTERACTION is the second
   line drawn over it in --fill by ::before, which is the only part that
   responds to hover, focus and press.
   Saying it the other way round -- "the hairline IS the interaction" -- would
   make this an affordance border, and an affordance border has to be --edge.
   It is not one, so it is not.
   The rule moves from the li to the anchor. That is layout-identical only
   because .entry-list li has no padding -- if a li ever gains padding, the
   hairline shortens and stops meeting the column edges. */
.entry-list a {
  display:block; padding:30px 0; border:0; border-top:1px solid var(--rule);
  color:var(--ink); position:relative;
}
.entry-list a::before {
  content:''; position:absolute; left:0; top:-1px; width:100%; height:1px;
  background:var(--fill); transform:scaleX(0); transform-origin:left top;
  transition:transform var(--m-move) var(--e-out);
}
.entry-list a:hover::before, .entry-list a:focus-visible::before { transform:scaleX(1); }
/* Press thickens the lit line to 2px instead of displacing the row, so the
   hairline never separates from its text. Rejected on measurement: a
   background tint on the row, because canvas plus 3% ink drops the --faint
   time in .row-meta to 4.28:1 in light. */
.entry-list a:active::before { transform:scaleX(1) scaleY(2); }
.entry-list a:hover h2, .entry-list a:focus-visible h2 { color:var(--wash-ink); }
.row-meta { display:flex; gap:12px; align-items:center; font-size:var(--t6); color:var(--faint); margin-bottom:10px; }
.badge {
  font-size:var(--t6); font-weight:500; letter-spacing:.10em; text-transform:uppercase;
  background:var(--wash); color:var(--wash-ink); padding:3px 11px; border-radius:var(--r-pill);
}
.entry-list h2 { font-size:var(--d2); margin-bottom:10px; transition:color var(--m-state) var(--e-out); }
.entry-list p { color:var(--muted); max-width:var(--measure); margin:0; font-size:var(--t4); line-height:1.50; }

/* ---- article ---- */
.entry { max-width:760px; }
.entry-head { margin-bottom:36px; }
.entry-date { color:var(--faint); font-size:var(--t6); margin:14px 0 0; }
.prose { max-width:var(--measure); font-size:var(--t3); line-height:1.60; }
.prose h2 { font-size:var(--d2); margin:48px 0 18px; }
.prose ol, .prose ul { padding-left:1.2em; }
.prose li { margin-bottom:10px; }
.prose hr { border:0; border-top:1px solid var(--rule); margin:44px 0; }
.prose em { color:var(--muted); }
.prose strong { font-weight:600; }

/* ---- figures ------------------------------------------------------------
   Drawn as inline SVG (build.py inline_figures) so the theme tokens reach
   inside them. The load-bearing strokes -- the seat frames, the record bar,
   the baselines the tallies stand on, the dashed line the drift departs from
   -- are --edge, not --rule-strong: they are the drawing's non-text content,
   so SC 1.4.11 owes them 3:1 and --rule-strong measures 1.63:1 light / 1.54:1
   dark on --canvas. --rule stays on the connective hairlines (the stems in
   two-seats), which is the decorative half of the same contract at the top of
   this file. The rest is --muted / --faint / --ink, and each figure spends the
   violet exactly once, on the thing it is about. */
.fig { margin:44px 0; }
.figbox { position:relative; }
.fig svg { display:block; width:100%; height:auto; overflow:visible; }
.fig figcaption {
  font-size:var(--t5); line-height:1.50; color:var(--faint);
  margin-top:16px; padding-top:12px; border-top:1px solid var(--rule);
  max-width:var(--measure);
}

/* ---- figure labels ----
   The type is NOT in the SVG, and this is the reason. SVG text is measured in
   user units, so it scales with the column: the same label was measured at
   24.08px in a 680px column and 18.15px in a 335px one, and neither number is
   in the nine-face scale. Compensating in the SVG made it worse -- the
   breakpoint that did it moved a label from 24.22px to 37.05px across one pixel
   of viewport, at a width where the column had not changed at all. Nothing in
   this file can hold the scale, because CSS cannot divide by the rendered
   width. So the labels came out of the drawing entirely: ordinary HTML in a
   layer over it, on the same tokens and in the same font as the prose around
   it, at the same size in every column.

   Positions are the drawing's own coordinates, converted once. Every figure
   uses viewBox "40 0 480 H", so left = (x - 40) / 480 and top = y / H, both as
   percentages -- which is what keeps the layer registered to the drawing while
   the column resizes. `top` is the label's BOTTOM edge (translateY(-100%)):
   SVG text sits on a baseline and an HTML box does not, and a label is placed
   against the thing beneath it. The drawings were given the vertical room to
   hold real type at the narrowest column; that is why the seat boxes and the
   context-pack box are taller than they were.

   The layer is aria-hidden: each figure is one role="img" with a <desc> that
   already reads every label out, so exposing these would say it all twice. */
.figlabels { position:absolute; inset:0; pointer-events:none; }
.figlabels span {
  position:absolute; transform:translateY(-100%); white-space:nowrap;
  font-family:var(--sans); font-weight:400; font-size:var(--t6); line-height:1.45;
  color:var(--muted);
}
.figlabels .mid { transform:translate(-50%,-100%); }
.figlabels .end { transform:translate(-100%,-100%); }
.figlabels .seat { font-size:var(--t2); font-weight:500; line-height:1.50; color:var(--ink); }
.figlabels .num { font-family:var(--serif); font-size:var(--d3); line-height:1.30; color:var(--ink); }
/* one accent per figure, on the thing the figure is about */
.figlabels .attn { color:var(--fill); }

/* rounds — viewBox 40 0 480 230; four counts standing under the baseline */
.fig-rounds .n1 { left:12.5%; top:97.4%; }
.fig-rounds .n2 { left:37.5%; top:97.4%; }
.fig-rounds .n3 { left:62.5%; top:97.4%; }
.fig-rounds .n4 { left:87.5%; top:97.4%; }

/* the-gap — viewBox 40 0 480 210 */
.fig-gap .count { left:2.5%; top:56.2%; }
.fig-gap .from { left:0; top:98.1%; }
.fig-gap .to { left:100%; top:98.1%; }

/* two-seats — viewBox 40 0 480 290 */
.fig-seats .builder { left:19.8%; top:33.1%; }
.fig-seats .builder-sub { left:19.8%; top:44.8%; }
.fig-seats .reviewer { left:80.2%; top:33.1%; }
.fig-seats .reviewer-sub { left:80.2%; top:44.8%; }
.fig-seats .work { left:49.2%; top:26.2%; }
.fig-seats .attacks { left:50%; top:75.9%; }
.fig-seats .record { left:50%; top:91.7%; }

/* drift — viewBox 40 0 480 244 */
.fig-drift .truth { left:0; top:23.8%; }
/* both sit lower than the point they name: at the narrowest column the label
   is wide relative to the drawing, and the drift curve runs through the space
   a tighter placement would want */
.fig-drift .drifting { left:5%; top:61.5%; }
.fig-drift .killed { left:38.75%; top:77.9%; }
.fig-drift .successor { left:81.25%; top:23.8%; }
.fig-drift .pack { left:71%; top:91%; }

.entry-cta { margin:52px 0 0; }
.entry-cta h2 { font-size:var(--d3); margin-bottom:10px; }
.entry-cta p { color:var(--muted); margin-bottom:16px; }
.entry-share { margin-top:34px; font-size:var(--t5); color:var(--muted); }
.entry-share .share-off { color:var(--faint); }
.entry-share .ph-block { margin-top:10px; }
.entry-adjacent { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin:34px 0 0; }
.adj {
  border:0; box-shadow:inset 0 0 0 1px var(--ring); border-radius:var(--r-small);
  padding:18px 20px; display:block; color:var(--ink);
  transition:box-shadow var(--m-state) var(--e-out), transform var(--m-press) var(--e-out);
}
/* Same ladder as .entry-card. This block had no feedback of any kind before. */
.adj:hover, .adj:focus-visible { box-shadow:inset 0 0 0 1px var(--fill); }
.adj:active { box-shadow:inset 0 0 0 2px var(--fill); transform:translateY(var(--press-shift)); }
.adj span { display:block; font-size:var(--t6); font-weight:500; letter-spacing:.10em; text-transform:uppercase; color:var(--faint); margin-bottom:8px; }
.adj strong { font-family:var(--serif); font-weight:400; font-size:var(--t1); line-height:1.30; }
.adj.prev { text-align:right; grid-column:2; }
@media (max-width:640px) { .entry-adjacent { grid-template-columns:1fr; } .adj.prev { grid-column:1; text-align:left; } }

/* ---- method ---- */
.method-rules { list-style:none; margin-block:52px 0; display:grid; gap:56px; }
.method-rules > li { border-top:1px solid var(--rule); padding-top:34px; }
.method-rules h2 { margin:0 0 14px; }
.method-rules p { max-width:var(--measure); color:var(--muted); font-size:var(--t4); line-height:1.50; }
.pair { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-top:26px; }
@media (max-width:760px) { .pair { grid-template-columns:1fr; } }
.pair figure { margin:0; }
.pair img { border-radius:var(--r-img); box-shadow:inset 0 0 0 1px var(--ring); }
.pair figcaption { font-size:var(--t6); color:var(--faint); margin-top:10px; }
.honest { margin-top:var(--gap); }
/* A .card must never sit directly on a shell element: its padding shorthand
   overrides the shell's padding-inline and the block goes edge to edge. */
main > .card { padding-inline:clamp(20px,5vw,40px); }
.honest h2 { font-size:var(--d3); margin-bottom:12px; }
.honest p { max-width:var(--measure); margin:0; color:var(--muted); }
.method-cta { border-top:1px solid var(--rule); margin-top:var(--gap); }

/* ---- contact ---- */
.contact-grid { display:grid; grid-template-columns:1.4fr 1fr; gap:24px; margin-top:44px; align-items:start; }
@media (max-width:840px) { .contact-grid { grid-template-columns:1fr; } }
.contact-form { display:grid; gap:18px; }
.contact-form label { display:grid; gap:8px; font-size:var(--t5); font-weight:450; color:var(--muted); }
.contact-form input, .contact-form textarea {
  font-family:var(--sans); font-size:var(--t4); color:var(--ink); background:var(--canvas);
  border:1px solid var(--edge); border-radius:var(--r-small); padding:12px 14px; width:100%;
  transition:border-color var(--m-state) var(--e-out), background-color var(--m-state) var(--e-out);
}
/* Measured 7.22:1 light, 7.32:1 dark. No :active here: mousedown grants
   focus, so focus already covers the press on a text field. */
.contact-form input:hover:enabled, .contact-form textarea:hover:enabled {
  border-color:color-mix(in oklab, var(--edge) 55%, var(--ink));
}
.contact-form input:focus-visible, .contact-form textarea:focus-visible { border-color:var(--fill); }
/* The dashed border is a deliberate quiet cousin of the .ph-block placeholder
   language: the field says it is not wired without needing the red note to
   say it for it. Dropping the border from --edge to --rule-strong is the
   --edge/--rule contract applied exactly as written -- the border stops being
   an affordance at the moment the control stops being interactive, so it is
   no longer owed 3:1. Text stays --muted (5.28:1 light, 5.38:1 dark). */
.contact-form input:disabled, .contact-form textarea:disabled {
  background-color:color-mix(in srgb, var(--ink) 4%, transparent);
  border-color:var(--rule-strong); border-style:dashed;
  color:var(--muted); cursor:not-allowed;
}
.contact-form textarea { resize:vertical; }
.contact-form .btn { justify-self:start; }
.contact-side h2 { font-size:var(--d3); margin-bottom:10px; }
.mailto { font-family:var(--serif); font-size:var(--t1); line-height:1.30; }
/* .foot-dead's rule, at contact size: a destination nobody has confirmed ships
   as words, not as an anchor. --faint measures 4.59:1 on --canvas and 5.73:1 in
   dark, so it stays readable body text, and the .ph-block directly under it
   names what is unconfirmed -- the state is never carried by colour alone. */
.mailto-off { color:var(--faint); }
.privacy { font-size:var(--t6); color:var(--faint); margin-top:22px; }

/* ---- footer ---- */
.site-footer { margin-top:calc(var(--gap) * 1.4); padding-block:32px 72px; border-top:1px solid var(--rule); }
.foot-line { font-size:var(--t5); color:var(--muted); margin-bottom:10px; }
/* A destination that does not exist yet is a word, not a link. --faint against
   --muted is the same drop the row meta uses, and it has to be visible without
   colour alone: these three sit next to the .ph note that names them. */
.foot-dead { color:var(--faint); }
.foot-meta { font-size:var(--t6); color:var(--faint); margin:0; }

/* ═══════════════════════════════════════════════════════════════════
   FLIGHT — scroll-scrub stage (Home only)
   #flight is made tall by the engine (T x pxPerSecond); the stage sticks
   to the viewport inside it, so scrolling the tall element scrubs time.
   ═══════════════════════════════════════════════════════════════════ */
#flight { position:relative; margin:0; padding:0; max-width:none; }
/* The stage's chrome flips light and dark with the STORY (body.night), not
   with the OS theme, so the page-level tokens are wrong inside it in two of
   the four combinations. These seven are byte-identical to the literals that
   used to sit inline on the pills, the rail and the bar: lifting them changes
   nothing on screen, and it is what lets every state rule below contain no
   raw hex. This block is the one documented exception zone for literal
   colour in the file. */
.flight-stage {
  position:sticky; top:0; height:100vh; height:100svh;
  overflow:hidden; background:var(--canvas);
  --chrome-ground:rgba(255,255,255,.88);
  --chrome-ink:#54545E;
  /* .55, not .42. This border is the pill's affordance edge, so --edge's floor
     applies: >=3:1 against what sits next to it. At .42 it composited to
     roughly #909093 over the pill's own fill and measured 2.57 to 2.65:1 on
     every one of the nine shipped day frames -- worst on H1-hero.webp, the
     first thing anyone sees, where the fill is also only 1.41:1 against the
     clay behind it, so the control had no perceivable boundary at all. This is
     the ring's problem one element over, and it takes the ring's answer: the
     frame is not knowable from CSS, so the boundary stops depending on it. The
     border is measured against the pill's OWN fill, which composites over the
     same unknown pixels and therefore moves with them. Re-measured over the
     top strip of all nine frames plus white, black and mid grey: 3.63:1 worst,
     3.93:1 best. Where a dark frame swallows the border instead, the pale fill
     is what separates -- 15.91:1 over black -- so one of the two edges is
     always visible, which is the same guarantee --chrome-halo gives the ring. */
  --chrome-edge:rgba(24,24,31,.55);
  --chrome-sel:#573EC9;
  --chrome-on-sel:#FFFFFF;
  --chrome-dot:rgba(255,255,255,.75);
  --chrome-dot-edge:rgba(24,24,31,.55);
  /* The eighth fixes a measured failure. --fill as a focus ring measures
     2.66:1 on a night segment in light theme and 2.52:1 on a day segment in
     dark theme -- both under the 3:1 floor SC 1.4.11 sets for a focus
     indicator, because the stage flips with the story while --fill flips with
     the OS. --chrome-focus is also the stage-local ink that the pill and the
     CTA mix toward, for the same self-correcting reason .btn mixes toward
     --ink.
     The ninth is what makes the ring honest. --chrome-focus was justified at
     17.34:1 "on the day chrome" -- but the ring is drawn OUTSIDE the pill, on
     the film, and body.night is set from segment metadata rather than from
     frame luminance. Measured against the actual pixels under the ring, a
     dark-clay video stretch put it at 1.02:1, 1.15:1, 1.41:1 and 2.39:1 --
     roughly 2,900px of a 9,499px flight below the floor. The frame is not
     knowable from CSS, so the ring stops depending on it: --chrome-halo is
     always the opposite tone, painted either side of the ring, and the pair
     measures 17.66:1 (day) and 15.92:1 (night) against EACH OTHER. Whatever
     the film is doing behind them, one of the two edges is visible. */
  --chrome-focus:#18181F;
  --chrome-halo:#FFFFFF;
  --target:0%;
}
body.night .flight-stage {
  --chrome-ground:rgba(15,15,25,.82);
  --chrome-ink:#C9C9D2;
  --chrome-edge:rgba(234,234,242,.55);  /* night passed at .42 (3.19:1 worst);
     moved with the day value so one alpha describes the edge, and the same
     sweep now measures 4.08:1 worst. */
  --chrome-sel:#9A96FE;
  --chrome-on-sel:#0F0F19;
  --chrome-dot:rgba(15,15,25,.55);
  --chrome-dot-edge:rgba(234,234,242,.62);
  --chrome-focus:#EAEAF2;
  --chrome-halo:#0F0F19;
}
/* 2px ring at 2px offset, inside a 6px halo: the halo fills 0-6px out, the
   outline paints over 2-4px of it (outlines paint last), leaving halo, ring,
   halo. Offset is 2px rather than the page's 3px so the whole indicator stays
   inside 6px and cannot reach the neighbouring pill. */
.flight-stage :focus-visible {
  outline-color:var(--chrome-focus); outline-offset:2px;
  box-shadow:0 0 0 6px var(--chrome-halo);
}

/* ── The missing state: "clicked, and the page is now travelling" ──────
   Measured on the live page at an 843px viewport: a THESIS -> FIELD pill
   click scrolls 7,298px, runs for just under 1.5 seconds, fires 90 scroll
   events and flashes 8 different copy-card headlines through the slot, 6 of
   them inside one 216ms window.
   The z-order is the whole idea. The veil sits at z1 -- above the canvas,
   below the copy card (z2) and the chrome (z3). The world goes quiet, the
   controls stay sharp, the bar keeps reporting real scroll position, and a
   1px tick shows where the trip ends. Nothing here is a fake progress
   animation: every number on screen is the real one. It is also not an
   optimisation -- the canvas still repaints 78 to 90 times during a long
   trip; the only work saved is suppressing the card rewrites.
   flight.js sets the class in goChapter and clears it when the scroll settles
   (measured after wiring: the same click still rewrites the card 8 times, but
   none of those rewrites is on screen). The two halves fail safe in either
   order -- CSS alone leaves every rule below unmatched, JS alone sets a class
   nothing styles. Under reduced motion the class is never set, because the
   jump is instantaneous and the veil would flash within a single frame. */
.flight-stage::after {
  content:''; position:absolute; inset:0; z-index:1; pointer-events:none;
  background:var(--chrome-ground); opacity:0;
  transition:opacity var(--m-state) var(--e-in);
}
body.in-transit .flight-stage::after { opacity:.72; transition:opacity var(--m-move) var(--e-out); }
.flight-stage::before {
  content:''; position:absolute; z-index:3; bottom:0; left:var(--target);
  width:1px; height:10px; background:var(--chrome-sel); opacity:0;
  transition:opacity var(--m-state) var(--e-out);
}
body.in-transit .flight-stage::before { opacity:1; }
body.in-transit #flightbar { height:3px; }
/* (1,1,1) beats #copycard.on at (1,1,0), so this needs no !important. */
body.in-transit #copycard { opacity:0; transition:opacity var(--m-state) var(--e-in); }
#screen { display:block; width:100%; height:100%; }

/* copy card — sits over the frame's reserved negative space */
/* The card must stay legible over ANY frame. The hero reserves pale negative
   space, but the macro close-ups fill the frame edge to edge, so the card
   carries its own ground rather than trusting what is behind it. */
#copycard {
  position:absolute; z-index:2; right:4%; top:50%; transform:translate(0,-46%);
  width:min(40%,46ch); color:#18181F;
  background:rgba(250,250,254,.90); backdrop-filter:blur(10px) saturate(1.1);
  border-radius:var(--r-card); padding:26px 28px;
  /* Cards carry a 1px inset ring and no elevation -- house rule, and the same
     rule --press-shift is justified by twenty lines up. This panel used to
     break it with a two-layer drop shadow, which is also the only thing in the
     file that painted a soft edge outside a radius. currentColor rather than a
     token because the card flips with the STORY: its own colour is already
     #18181F or #EAEAF2 by the time this resolves, so the ring follows without
     a second rule and without restating a literal. 18%, not --ring's 9%: it is
     doing separation work over film, where --canvas is not behind it. */
  box-shadow:inset 0 0 0 1px color-mix(in oklab, currentColor 18%, transparent);
  opacity:0; pointer-events:none;
  /* was the same literal duration written twice, with `ease`. Identical
     timing, now named: the stage's tempo is a decision, and --m-cine makes
     it legible as one. */
  transition:opacity var(--m-cine) var(--e-out), transform var(--m-cine) var(--e-out);
}
#copycard.on { opacity:1; transform:translate(0,-50%); pointer-events:auto; }
#copycard.mid {            /* text-card beats: centred, no frame behind */
  right:auto; left:50%; width:min(90%,30ch); text-align:center;
  transform:translate(-50%,-46%);
}
#copycard.mid.on { transform:translate(-50%,-50%); }
#copycard.night, #copycard.night h2 { color:#EAEAF2; }
#copycard.night { background:rgba(15,15,25,.86); }
#copycard.mid { background:transparent; backdrop-filter:none; box-shadow:none; }
#copycard h2 {
  font-family:var(--serif); font-weight:400; font-size:var(--d2);
  line-height:1.30; letter-spacing:-.015em; margin:0 0 12px;
}
/* #copycard is a fixed light panel in both themes -- theme tokens do not apply. */
#copycard .counter {
  font-family:var(--sans); font-size:var(--t6); font-weight:500; letter-spacing:.10em;
  color:#4A31B8; margin:0 0 10px; font-variant-numeric:tabular-nums;
}
#copycard .eyebrow { color:#54545E; margin-bottom:10px; }
#copycard.night .eyebrow, #copycard.night .sub, #copycard.night .note { color:#9A9AA8; }
#copycard .sub { font-size:var(--t4); line-height:1.50; color:#54545E; margin:0 0 12px; }
#copycard .note { font-size:var(--t5); line-height:1.50; color:#54545E; margin:0; }
#copycard .tags { display:flex; gap:8px; flex-wrap:wrap; margin:14px 0 0; }
#copycard .tag {
  font-size:var(--t6); font-weight:450; letter-spacing:0; padding:5px 12px;
  border-radius:var(--r-pill); background:rgba(87,62,201,.10); color:#3B2F8C;
}
#copycard.night .tag { background:rgba(154,150,254,.16); color:#C6C4F5; }
#copycard .cardcta { margin:18px 0 0; }

/* ---- the lead: the page's <h1>, in every code path ----
   Static markup, first child of the stage. In flight it floats over the film
   exactly where the copy card does, so it carries the card's fixed colours for
   the same reason (site.css:369 -- it sits on clay, not on --canvas). With JS
   off or reduced motion on it is an ordinary block at the top of the document,
   which is why the base rule below is the flat one and the overlay is the
   override. It is never display:none: the h1 stays in the DOM and in the
   accessibility tree for the whole page. */
.flight-lead {
  max-width:1200px; margin-inline:auto;
  padding:var(--gap) clamp(20px,5vw,40px) 0;
}
/* --d2, not --d1: this is the same line that used to render as the copy card's
   h2 and it must not change size -- a semantics fix, not a resize. */
.flight-lead h1 {
  font-size:var(--d2); line-height:1.30; letter-spacing:-.015em; margin:0 0 12px;
}
.flight-lead .lead-sub { font-size:var(--t4); line-height:1.50; color:var(--muted); max-width:var(--measure); margin:0 0 12px; }
.flight-lead .lead-note { font-size:var(--t5); line-height:1.50; color:var(--muted); max-width:var(--measure); margin:0; }

.js:not(.reduced) .flight-lead {
  position:absolute; z-index:2; right:4%; top:50%; transform:translate(0,-50%);
  width:min(40%,46ch); max-width:none; margin:0; padding:26px 28px;
  /* --chrome-focus / --chrome-ink, not the literals they resolve to: this
     element is a child of .flight-stage, so both are already in scope here.
     The chrome block above is the file's one exception zone for literal
     colour, and that only works if nothing below it restates the literals. */
  color:var(--chrome-focus);
  background:rgba(250,250,254,.90); backdrop-filter:blur(10px) saturate(1.1);
  border-radius:var(--r-card);
  /* Same ring as #copycard, for the same reason: this is the same panel in a
     different code path, and cards do not lift. */
  box-shadow:inset 0 0 0 1px color-mix(in oklab, currentColor 18%, transparent);
  transition:opacity var(--m-cine) var(--e-out), transform var(--m-cine) var(--e-out);
}
.js:not(.reduced) .flight-lead .eyebrow,
.js:not(.reduced) .flight-lead .lead-sub,
.js:not(.reduced) .flight-lead .lead-note { color:var(--chrome-ink); }
.js:not(.reduced) .flight-lead.off { opacity:0; transform:translate(0,-46%); pointer-events:none; }

/* chrome row: the chapter rail, plus the one control that leaves the page.
   The chrome carries the positioning; the nav centres inside it, the link sits
   at the end. They are different KINDS of control on purpose -- buttons that
   move the playhead, one <a> that navigates -- because the fifth pill used to
   say CONTACT and was neither reliably. */
.flight-chrome {
  position:absolute; z-index:3; left:0; right:0; top:0;
  display:flex; align-items:flex-start; gap:12px; padding:16px 12px;
}
.flight-cta {
  flex:0 0 auto; white-space:nowrap;
  font-family:var(--sans); font-size:var(--t6); font-weight:500;
  padding:7px 15px; border-radius:var(--r-pill); border:1px solid transparent;
  background:var(--chrome-sel); color:var(--chrome-on-sel);
  transition:background-color var(--m-state) var(--e-out),
             transform var(--m-press) var(--e-out);
}
/* The body.night override is gone: --chrome-sel and --chrome-on-sel already
   carry both stories, so the day and night values live in one place.
   Same self-correcting mix as .btn, anchored on the stage's ink rather than
   the page's, because this control flips with the story and not with the OS.
   border-bottom-color is load-bearing here in a way it is not on .btn: this
   element really does carry a 1px border, so a:hover would otherwise paint
   its bottom edge with currentColor. */
.flight-cta:hover, .flight-cta:focus-visible {
  background:color-mix(in oklab, var(--chrome-sel) 92%, var(--chrome-focus));
  border-bottom-color:transparent;
}
.flight-cta:active {
  background:color-mix(in oklab, var(--chrome-sel) 84%, var(--chrome-focus));
  border-bottom-color:transparent; transform:translateY(var(--press-shift));
}

/* chapter rail + progress */
#flightnav {
  flex:1 1 auto; display:flex; gap:6px; justify-content:center; flex-wrap:wrap;
}
/* This pill sits on the stage chrome, not on the page ground, which is why it
   spends --chrome-* and not the page tokens. The four body.night overrides
   that used to live here are gone: the tokens carry both stories. */
#flightnav .navbtn {
  font-family:var(--sans); font-size:var(--t6); font-weight:500; letter-spacing:.10em;
  text-transform:uppercase; padding:7px 15px; border-radius:var(--r-pill);
  border:1px solid var(--chrome-edge); background:var(--chrome-ground);
  color:var(--chrome-ink); cursor:pointer; backdrop-filter:blur(6px);
  transition:background-color var(--m-state) var(--e-out),
             border-color var(--m-state) var(--e-out),
             color var(--m-state) var(--e-out),
             transform var(--m-press) var(--e-out);
}
#flightnav .navbtn.on { background:var(--chrome-sel); border-color:transparent; color:var(--chrome-on-sel); }
/* The :where() wrappers are load-bearing, twice over. They keep the guard out
   of the specificity sum, so :active below stays the last word instead of
   losing to a simultaneous :hover. And splitting on .on is a contrast fix,
   not a style choice: --chrome-focus text on a selected pill puts the stage
   ink straight onto the selected fill, which measures 2.47:1. The selected
   pill deepens instead, by the same mix .btn uses. */
#flightnav .navbtn:where(:not(.on)):hover, #flightnav .navbtn:where(:not(.on)):focus-visible {
  border-color:var(--chrome-focus); color:var(--chrome-focus);
}
#flightnav .navbtn:where(.on):hover, #flightnav .navbtn:where(.on):focus-visible {
  background:color-mix(in oklab, var(--chrome-sel) 92%, var(--chrome-focus));
}
/* Press paints the selected look immediately, so the pill under your finger is
   already showing the state the engine is about to confirm -- the handover to
   .on is continuous rather than a second event. */
#flightnav .navbtn:active {
  background:var(--chrome-sel); border-color:transparent; color:var(--chrome-on-sel);
  transform:translateY(var(--press-shift));
}

#flightrail {
  position:absolute; z-index:3; right:14px; top:50%; transform:translateY(-50%);
  display:flex; flex-direction:column; gap:0;
}
/* The dot is a MARK, not a control. It used to be a <button> with an
   aria-label, four of them, inside #flightrail's aria-hidden -- keyboard
   reachable, invisible to the accessibility tree they were describing
   themselves to (SC 4.1.2), and four extra tab stops for the four chapters the
   pills already expose. Growing it to a 24px target made that worse, not
   better: a bigger target for a control that no screen reader could see.
   So it went the other way. The rail reports position and does not accept
   input; the pills above it are the control, they are real buttons, and they
   are in the tree. WCAG 2.2 SC 2.5.8 does not apply to something that is not a
   target, and there is no interactive state to draw because there is no
   interaction. The 24px box stays: it is the pitch that spaces the marks. */
#flightrail .dot {
  width:24px; height:24px; display:grid; place-items:center;
}
#flightrail .dot::before {
  content:''; width:8px; height:8px; border-radius:50%;
  border:1px solid var(--chrome-dot-edge); background:var(--chrome-dot);
  transition:background-color var(--m-state) var(--e-out),
             border-color var(--m-state) var(--e-out);
}
#flightrail .dot.on::before { background:var(--chrome-sel); border-color:var(--chrome-sel); }

/* --e-flat, and never on width: the width IS scroll position and must stay
   exactly synchronous with the scrollbar. The token swap from --fill is a fix
   rather than a refactor -- --fill measures 2.66:1 against a night segment in
   light theme, so the bar was effectively disappearing for part of the
   story. */
#flightbar {
  position:absolute; z-index:3; left:0; bottom:0; height:2px;
  background:var(--chrome-sel); width:0;
  transition:height var(--m-state) var(--e-flat), opacity var(--m-state) var(--e-flat);
}

/* The flight prose ships in the HTML and is the default view. Only once the
   inline head script has confirmed JS (html.js) do we swap to the canvas.
   Without this, the homepage's entire crawlable body is the word PLACEHOLDER.

   These rules used to hide #flight whole in both degraded paths, which is
   exactly why the page had no h1: there was no element that survived every
   path. So they now hide only the moving parts, and the stage lies flat with
   the lead still in it. */
html:not(.js) #screen, html:not(.js) #copycard, html:not(.js) .flight-chrome,
html:not(.js) #flightrail, html:not(.js) #flightbar,
.js.reduced #screen, .js.reduced #copycard, .js.reduced .flight-chrome,
.js.reduced #flightrail, .js.reduced #flightbar { display:none; }
html:not(.js) .flight-stage, .js.reduced .flight-stage { position:static; height:auto; }
/* This used to be `display:none`, which deleted the ten story beats from the
   rendered document for every reader running our JavaScript. The film replaced
   them on screen, but a screen reader got the h1, one canvas alt sentence, an
   empty div and then the section after the flight: 10 of the page's 22
   headings never reached it, and the copy card holds one beat at a time and is
   empty on load. Clipped instead of removed, the prose stays in the document
   and in the accessibility tree while the canvas plays it visually -- the film
   is the presentation, this is the text.
   flight.js takes the two links inside it out of the tab order on this path,
   because a sighted keyboard user must not focus something they cannot see;
   browse-mode readers still reach them. */
.js:not(.reduced) #flightfallback {
  position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0;
}

/* JS-off / reduced-motion: the script as a plain readable document */
#flightfallback { padding-block:var(--gap) 0; }
#flightfallback .fb-seg { border-top:1px solid var(--rule); padding:34px 0; max-width:var(--measure); }
#flightfallback .tags { display:flex; gap:8px; flex-wrap:wrap; }
#flightfallback .tag { font-size:var(--t6); font-weight:450; padding:5px 12px; border-radius:var(--r-pill); background:var(--wash); color:var(--wash-ink); }
#flightfallback h2 { font-size:var(--d2); margin:0 0 12px; }
#flightfallback p { color:var(--muted); }
#flightfallback .fb-note { font-size:var(--t5); }

@media (max-width:820px) {
  #copycard, #copycard.mid {
    right:auto; left:50%; width:min(88%,36ch); text-align:left;
    top:auto; bottom:6%; transform:translate(-50%,0);
    padding:20px 22px;
  }
  #copycard.on, #copycard.mid.on { transform:translate(-50%,0); }
  #copycard.night { background:rgba(15,15,25,.9); }
  .js:not(.reduced) .flight-lead {
    right:auto; left:50%; width:min(88%,36ch);
    top:auto; bottom:6%; transform:translate(-50%,0);
    padding:20px 22px;
  }
  .js:not(.reduced) .flight-lead.off { transform:translate(-50%,0); }
  #flightrail { display:none; }
  /* Four pills measure 425px, so at 375px they scroll. Sharing one row with the
     link left them 187px and cut the second pill through the middle, which reads
     as broken rather than as scrollable. So the link takes the first row and the
     rail gets the full width of the second -- and the way off this page is still
     the part that cannot scroll out of reach. */
  .flight-chrome { gap:8px; padding:10px 8px; flex-wrap:wrap; }
  .flight-cta { order:1; margin-left:auto; }
  #flightnav { order:2; flex:1 0 100%; gap:4px; padding:0; overflow-x:auto; justify-content:flex-start; flex-wrap:nowrap; min-width:0; }
}

/* Pull quotes. Every one is the founder's own published sentence, verbatim, with
   the essay linked -- so the claim beside it is not the studio's say-so but
   something written down before there was anything to sell. That is the whole
   value, and it is why the rule here is: quote or do not, never paraphrase into
   quotation marks. Condition 01 carries no quote on purpose. Nothing in the
   corpus supports it well, and a weak quote next to a strong claim reads as a
   claim its author could not support. */
.pullquote {
  margin:26px 0; padding:0 0 0 20px;
  border-left:2px solid var(--edge);   /* informational, so --edge not --rule */
  max-width:var(--measure);
}
.pullquote p {
  font-family:var(--serif); font-weight:400; font-size:var(--t1);
  line-height:1.50; color:var(--ink); margin:0 0 10px;
}
.pullquote cite {
  font-family:var(--sans); font-style:normal; font-size:var(--t6);
  letter-spacing:.02em; color:var(--faint);
}
.pullquote cite a { color:inherit; }

/* The form honeypot. Hidden from everyone, including screen readers: a field a
   sighted user cannot see but a screen-reader user CAN is a trap that catches
   the wrong people. display:none removes it from the accessibility tree as
   well, which is the whole point -- the only thing left that fills it is a bot
   parsing markup. Never animated, never focusable. */
.hp { position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden; }
.hp input { display:none; }
