/* GridReady landing page styles.
 *
 * These live in a file rather than in a <style> block for one reason: the
 * apex ships a Content-Security-Policy of style-src 'self', and an inline
 * block would need 'unsafe-inline', which buys close to nothing against
 * injection while reading as protection that is not there. The app's four
 * public pages made the same move. See deploy/RUNBOOK.md section 6 in the
 * prototype repository.
 *
 * Extracted verbatim from index.html on 28 July 2026. No rule was changed.
 */
@font-face{
  font-family:'Space Grotesk';
  font-style:normal;
  font-weight:400 700;
  font-display:swap;
  src:url("fonts/space-grotesk-latin.woff2") format("woff2");
}
@font-face{
  font-family:'Public Sans';
  font-style:normal;
  font-weight:400 700;
  font-display:swap;
  src:url("fonts/public-sans-latin.woff2") format("woff2");
}
@font-face{
  font-family:'Public Sans';
  font-style:italic;
  font-weight:400 700;
  font-display:swap;
  src:url("fonts/public-sans-italic-latin.woff2") format("woff2");
}
@font-face{
  font-family:'JetBrains Mono';
  font-style:normal;
  font-weight:400 600;
  font-display:swap;
  src:url("fonts/jetbrains-mono-latin.woff2") format("woff2");
}
/* Tokens are a subset of the app's canonical ui/shared/tokens.css in the
 * prototype repo, values verbatim. Every COLOUR token must appear in all
 * four blocks below (:root, the prefers-dark media block, and the two manual
 * data-mode overrides) or the modes drift apart; the mode-independent tokens
 * (spacing, type, radius, fonts) live in :root only. */
:root{
  color-scheme:light dark;
  --s1:4px;--s2:8px;--s3:12px;--s4:16px;--s5:24px;--s6:32px;--s7:48px;--s8:64px;
  --fs-xs:0.75rem;--fs-sm:0.875rem;--fs-base:1rem;--fs-md:1.25rem;--fs-lg:1.563rem;--fs-xl:1.953rem;--fs-2xl:2.441rem;--fs-3xl:3.052rem;
  --font-display:'Space Grotesk',system-ui,sans-serif;
  --font-body:'Public Sans',system-ui,sans-serif;
  --font-mono:'JetBrains Mono',ui-monospace,monospace;
  --r-sm:4px;--r-md:8px;--r-lg:12px;--r-pill:100px;
  /* MOTION · Direction B "Energise", adopted 10 Aug 2026. One ease, two beats;
     mode-independent, so it lives here rather than in any of the four colour
     blocks below. Current flows, things brighten into place; nothing loops at
     rest. Identical values to ui/shared/tokens.css in the prototype repository,
     kept equal on purpose: one motion vocabulary across both surfaces. */
  --ease-flow:cubic-bezier(.22,.61,.1,1);--m-rise:700ms;--m-sweep:1100ms;
  /* Header height, declared once because the sticky header's own offset and
     every in-page anchor's scroll-margin derive from it. .topbar padding
     16+16, plus the tallest child (a 44px nav link), plus the 1px bottom
     border. Measured, not guessed: re-measure if the bar gains a taller
     control. TRUE ONLY ON ONE ROW: .topbar is flex-wrap:wrap, so below about
     900px the bar grows and this constant stops describing it. Nothing derives
     from it there, because the media query at the foot of this file un-sticks
     the header instead.

     The wrapped heights are smaller than they were. Measured at 375px on
     6 August 2026: 218px before the nav stopped wrapping and the tagline was
     hidden, and the header was STICKY at that size until the same day (F138).
     Both fixed, and the bar there is static now, so what it costs is scroll
     height once rather than a third of the viewport on every scroll.

     This said "TRUE ONLY ON index.html" until 5 August 2026: 404.html had no
     .topnav, so its tallest child was the 40px mode control and its bar
     measured 73px, and the note closed by saying that if that page ever gained
     an in-page anchor, the fix was to give it the nav or stop deriving its
     offset from here. It has the nav now. That was done for navigation rather
     than for this constant, but the constant holds on both pages for the one
     reason either way: a 44px nav link is the tallest child of both bars. */
  --head-h:77px;
  /* The phone bar's two halves below 900px, measured at 375x812 on 6 August
     2026 and identical to ui/client.css in the prototype repository.
       --headhide-h   .topbar's top padding plus the brand/mode row (8 + 40).
                      The header's sticky `top` is the negative of this, so
                      exactly this much scrolls out of sight and no more.
       --headstrip-h  everything below that line, and therefore everything that
                      stays pinned: the 8px row gap, the 44px nav row, the 8px
                      bottom padding, the 1px border. 61px, 9% of a 667px
                      viewport, against the 125px this bar measures today.
     48 + 61 = 109, the whole bar. If either changes, both do.
     The names may not begin `--head-h-`: the prototype's
     test_the_page_anchors_clear_the_sticky_bar matches that prefix by
     substring, and a token containing it would satisfy the guard on its own.
     Kept spelled the same here so the two files still diff clean. */
  --headhide-h:48px;
  --headstrip-h:61px;
  --bg:#EAE4D7;--surface:#F6F2E9;--text:#1A1B1E;--muted:#62625A;
  --border:#DAD3C3;--border-strong:#C8BFAD;--border-control:#8C8069;--on-accent:#F6F2E9;
  --error:#B23A2E;
  --shadow:0 1px 2px rgba(26,27,30,.05),0 12px 30px -16px rgba(26,27,30,.20);
  --accent:#1F5D4C;--accent-deep:#164539;--accent-tint:#D8E7E0;--accent-subtle:#B4D2C6;
  /* --live's oklch redeclaration lives behind @supports below, NOT as a second
     bare declaration here: a browser that cannot parse oklch() does not "keep"
     an earlier plain declaration once a later one has won the cascade, because
     the property is reassigned unconditionally and an unsupported value then
     makes that reassignment invalid rather than falling back. @supports gates
     on parse support, which is the real mechanism. Identical values and the
     same reasoning as ui/shared/tokens.css in the prototype repository. */
  --live:#008767;
  --seq1:#D8E7E0;--seq2:#9BC3B4;--seq3:#4E9078;--seq4:#1F5D4C;--seq5:#123D31;
}
@supports (color: oklch(0 0 0)) {
  :root { --live:oklch(.55 .12 170); }
}
@media(prefers-color-scheme:dark){
  :root{
    color-scheme:dark;
    --bg:#0D1626;--surface:#16233A;--text:#E7ECF5;--muted:#8C99AE;
    --border:#243449;--border-strong:#34475F;--border-control:#5A7290;--on-accent:#06101F;
    --error:#E0796A;
    --shadow:0 2px 6px rgba(0,0,0,.55),0 20px 48px -20px rgba(0,0,0,.8);
    --accent:#4FC0A0;--accent-deep:#7FD9BF;--accent-tint:#143A30;--accent-subtle:#2A5F50;
    --live:#40E3B1;
    --seq1:#1E5445;--seq2:#2C7A64;--seq3:#3FA184;--seq4:#4FC0A0;--seq5:#8FE5CE;
  }
  @supports (color: oklch(0 0 0)) {
    :root { --live:oklch(.82 .15 168); }
  }
}
:root[data-mode="light"]{
  color-scheme:light;
  --bg:#EAE4D7;--surface:#F6F2E9;--text:#1A1B1E;--muted:#62625A;
  --border:#DAD3C3;--border-strong:#C8BFAD;--border-control:#8C8069;--on-accent:#F6F2E9;
  --error:#B23A2E;
  --shadow:0 1px 2px rgba(26,27,30,.05),0 12px 30px -16px rgba(26,27,30,.20);
  --accent:#1F5D4C;--accent-deep:#164539;--accent-tint:#D8E7E0;--accent-subtle:#B4D2C6;
  --live:#008767;
  --seq1:#D8E7E0;--seq2:#9BC3B4;--seq3:#4E9078;--seq4:#1F5D4C;--seq5:#123D31;
}
@supports (color: oklch(0 0 0)) {
  :root[data-mode="light"] { --live:oklch(.55 .12 170); }
}
:root[data-mode="dark"]{
  color-scheme:dark;
  --bg:#0D1626;--surface:#16233A;--text:#E7ECF5;--muted:#8C99AE;
  --border:#243449;--border-strong:#34475F;--border-control:#5A7290;--on-accent:#06101F;
  --error:#E0796A;
  --shadow:0 2px 6px rgba(0,0,0,.55),0 20px 48px -20px rgba(0,0,0,.8);
  --accent:#4FC0A0;--accent-deep:#7FD9BF;--accent-tint:#143A30;--accent-subtle:#2A5F50;
  --live:#40E3B1;
  --seq1:#1E5445;--seq2:#2C7A64;--seq3:#3FA184;--seq4:#4FC0A0;--seq5:#8FE5CE;
}
@supports (color: oklch(0 0 0)) {
  :root[data-mode="dark"] { --live:oklch(.82 .15 168); }
}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--text);font-family:var(--font-body);font-size:var(--fs-base);line-height:1.65;-webkit-font-smoothing:antialiased;min-height:100vh;display:flex;flex-direction:column}
/* One content measure across the whole page. .wrap used to be 720px while
   .wrap.wide and .topbar were both 1000px, so section headings and body
   prose sat in a narrower column than the card grid and the wordmark above
   them, and none of the left edges lined up. That misalignment is what read
   as "unbalanced proportions". Line length is now constrained on the text
   itself (below), which is where a reading measure belongs; .wide is kept as
   a no-op alias so no markup had to change. */
.wrap{width:100%;max-width:1000px;margin:0 auto;padding:0 var(--s5)}
.wrap.wide{max-width:1000px}
/* 720px SINCE JAN'S REVIEW OF 10 AUGUST 2026 ("width of text is a bit random…
   it can go wider, 70–80% of the page"): 58ch sat at barely half the 1000px
   band, so prose read as a second, unrelated column against the grids and the
   wordmark. 720px is ~72% of the 952px content band — and it is a PIXEL cap,
   not ch, because ch shrinks with the element's font: the 14px fineprint under
   the waitlist form sat ~70px shy of the paragraph above it under a shared ch
   value. One px value, so every block ends on the same right edge regardless
   of type size. The individual .secline/.hero p/.waitlist p rules below carry
   the same value — they are later in the sheet at equal specificity, so THEY
   are the winning caps; this grouped rule keeps the estate list in one place.
   .who folded into .method the same review; its selector stays for site.css
   diffability. */
.sec,.secline,.hero p,.who p,.waitlist p,.method p,.principles{max-width:720px}
/* A hairline at the top of every section but the first, at the FULL content
   measure. Jan's reading on 6 August 2026: prose at about 60% of the width while
   the tiles span all of it looks inconsistent.

   Both widths are deliberate and neither should move. 720px is a reading measure
   and belongs on the text; 1000px is the content width and belongs to the
   container. An earlier attempt to reconcile them by narrowing the container to
   720px is the failure the comment above records: the left edges stopped lining
   up and it read as unbalanced. What was missing is anything that DECLARED the
   full measure, so the inset prose read as an inconsistent width rather than as a
   column deliberately narrower than its container.

   ::before on the wrap, not border-top on the wrap. .wrap carries 24px of
   horizontal padding, so a border on its box would overhang the card edges below
   it by that much on each side and line up with nothing. A generated block sits
   inside the padding and spans exactly the measure the cards do.

   :first-child matters: #examples has two .wrap children, the prose and the grid,
   and only the first one opens the section. */
main section+section>.wrap:first-child::before{
  content:"";display:block;border-top:1px solid var(--border);margin-bottom:var(--s6)}
/* Sticky, matching the app's header exactly (ui/client.css). --bg rather
   than transparent is required, or the page scrolls through it.

   body>header, NOT a bare `header`. This page has one <header> today, so the
   scope changes nothing you can see. It is here because the identical bare
   rule in ui/client.css was a live defect on 1 August 2026: the app grew a
   second <header> (the score block), which inherited the sticky, the
   z-index, the opaque background and the border, and painted itself over the
   navigation. A selector that relies on there being exactly one of something
   states an assumption the markup is free to break later. There are no tests
   in this repository, so this comment is the only thing standing between the
   next second header on this page and the same bug. */
body>header{position:sticky;top:0;z-index:20;background:var(--bg);border-bottom:1px solid var(--border)}
.topbar{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--s2) var(--s4);max-width:1000px;margin:0 auto;padding:var(--s4) var(--s5)}
/* One wordmark across both surfaces (Jan's decision, 31 July 2026: the app
   wins). These rules are copied verbatim from ui/client.css in the prototype
   repository, so the two files stay literally identical and a future drift
   shows up as a diff rather than as a near-miss nobody sees.
   .eyebrow is the brand's tagline, added 1 August 2026. Note the value: .18em,
   the app's. This class already existed here at 0.08em for the section
   kickers, which is one name meaning two things across two surfaces, so those
   are .kicker now (below) and the app keeps the name it was using. */
.brand{display:flex;align-items:baseline;gap:var(--s3);text-decoration:none;color:inherit}
.wordmark{font-family:var(--font-display);font-weight:700;font-size:var(--fs-md);letter-spacing:-.03em}
.wordmark b{color:var(--accent)}
.eyebrow{font-family:var(--font-mono);font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.18em;color:var(--muted)}
.brand:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--r-sm)}
/* THE LINKS ARE THE SYSTEM GREEN, 5 August 2026. They were var(--muted) at
   weight 500, which measures 4.86:1 on the paper background: over the AA
   floor, but the weakest text in the header, and next to a wordmark carrying
   the accent they read as disabled rather than as the way around the site.
   --accent measures 6.08:1 light and 8.09:1 dark against --bg; --accent-deep,
   which carries hover and the current page, 8.53:1 and 10.85:1. Those figures
   come from tests/test_a11y_tokens.py in the prototype repository reading the
   shipped tokens, not from a hand check, and the dark palette's --accent-deep
   is deliberately LIGHTER than its --accent so "deeper" means "more contrast"
   in both modes. Identical rule in ui/client.css.

   Home carries aria-current="page" on index.html; the attribute does the
   stating and this rule only styles it, because colour alone would be 1.4.1. */
/* ONE ROW, SCROLLED, on a phone. 6 August 2026, and identical to ui/client.css
   in the prototype repository so the two headers behave the same way at the same
   widths.

   Measured here before the change, at 375x667: the nav wrapped to two rows of
   pills and the bar came to 218px, a third of the viewport spent before a word of
   content, and until the commit before this one it was 218px of STICKY bar
   (F138). Jan called it weirdly tall on both surfaces.

   So it does not wrap here. The pills sit in a single row that scrolls sideways,
   and the overflow WAS discoverable the way horizontal scroll usually is: the
   last pill visibly cut, which is the affordance.

   THAT AFFORDANCE IS GONE AT 375px, since the CTA moved to second on the evening
   of 6 August 2026, and it went by coincidence rather than by design, which is
   why the numbers are here. Measured in a browser at 375x812, on this page and
   the app's, which share these pill widths: Home 63 + the CTA 138 + "How it
   works" 109, plus two 8px gaps, is 326px against the 327px a 375px phone leaves
   inside the gutters. The row ends flush with ONE pixel to spare, and Examples
   and Waiting list begin wholly off-screen -- no sliver, no cut, nothing saying
   there is more to the right.

   It was traded knowingly. What the row cut before was the CTA, the only action
   on the surface, where both of the pills now hidden are sections this page
   scrolls to anyway. Jan called the phone nav an interim design and expects to
   redesign it; bolting an affordance back on -- an edge fade, a narrower pill, a
   smaller gap -- would be designing it by accident, and tuned to one width. Left
   as measured, and recorded so the paragraph above is not read as still true.
   A bottom bar with icons was Jan's
   first preference and was rejected with his agreement: it is a net-new
   fixed-position pattern, icons need labels to be accessible so it saves less
   height than it promises, and it would have existed on one surface only, which is
   the estate drift the previous round was spent removing.

   scrollbar-width and the WebKit pseudo-element hide the scrollbar, not the
   scrolling. Focusing a pill still scrolls it into view, so the row stays keyboard
   reachable, and every pill keeps its 44px target.

   flex:none matters as much as white-space:nowrap: a flex item's default
   min-width:auto would let "How it works" compress rather than overflow, which is
   the same wrapping one level down.

   Restored to wrapping at 760px by the block near the foot of this file. */
.topnav{display:flex;gap:var(--s2);order:3;width:100%;overflow-x:auto;scrollbar-width:none}
.topnav::-webkit-scrollbar{display:none}
.topnav a{display:inline-flex;align-items:center;min-height:44px;padding:0 var(--s3);border-radius:var(--r-pill);color:var(--accent);text-decoration:none;font-size:var(--fs-sm);font-weight:600;white-space:nowrap;flex:none}
.topnav a:hover{color:var(--accent-deep)}
.topnav a[aria-current="page"]{color:var(--accent-deep)}
/* The CTA, 6 August 2026. The only filled thing in the header, so it reads as
   the one action rather than another destination, and it inherits the 44px
   min-height, the pill radius, the weight and the focus ring from the rules
   around it: only the fill, the text colour and a little more horizontal room are
   new.

   It closed the nav until that evening and is now SECOND, between Home and How
   it works, on this page and on all five app pages. Nothing in this block cares
   -- no :last-child, no margin-left:auto -- which is why the move was markup
   only. The reasoning is in index.html and the cost is measured in the ONE ROW
   note above; the short version is that the row loses its last pill on a phone,
   and the lost pill should not be the only action on the surface.

   --on-accent is restated on hover. .topnav a:hover above sets
   color:var(--accent-deep) and would otherwise win by document order, putting a
   light green label on a light green fill at roughly 1.4:1, which is less a
   contrast failure than an invisible label.

   These two lines are BYTE-IDENTICAL to ui/client.css in the prototype
   repository, deliberately. One nav across the estate means one CTA style, and a
   difference between the two files should show up in a diff. */
.topnav a.navcta{background:var(--accent);color:var(--on-accent);padding:0 var(--s4);font-weight:600}
.topnav a.navcta:hover{background:var(--accent-deep);color:var(--on-accent)}
.topnav a:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* The colour-mode control, also the app's, copied verbatim from ui/client.css
   along with its .hmeta wrapper. It replaced .modewrap and a #modeToggle
   styled as a bare glyph button on 1 August 2026. margin-left:auto is
   redundant beside .topbar's justify-content:space-between and is kept anyway,
   so the rule is byte-identical to the app's and a drift reads as a diff.
   --border-control, not --border-strong: the old button used the latter, which
   is 1.44:1 in light mode and cannot carry a control boundary (WCAG 1.4.11).
   That token exists for exactly this and the app has always used it here. */
.hmeta{margin-left:auto;display:flex;align-items:center;gap:var(--s2);flex-wrap:wrap}
.mode{cursor:pointer;background:none;color:var(--muted);border:1px solid var(--border-control);border-radius:var(--r-pill);
  padding:6px 12px;font-family:var(--font-mono);font-size:var(--fs-xs);letter-spacing:.06em;min-height:40px;
  display:inline-flex;align-items:center;gap:7px}
.mode:hover{border-color:var(--accent);color:var(--text)}
.mode svg{width:15px;height:15px}
.mode:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
main{flex:1}
main a:not([class]){color:var(--accent);text-decoration:underline;text-underline-offset:2px}
main a:not([class]):hover{color:var(--accent-deep)}
main a:not([class]):focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.sec{font-family:var(--font-display);font-weight:600;letter-spacing:-0.01em;font-size:var(--fs-xl);margin-bottom:var(--s3)}
.secline{max-width:720px;margin-bottom:var(--s4)}
.hero{padding:var(--s8) 0 var(--s6)}
/* Section kicker, the small mono line above a hero heading. Was .eyebrow until
   1 August 2026, when the brand tagline took that name to match the app; the
   two are different things at different letter-spacing and sharing one name
   across the two surfaces meant neither could be copied without checking. */
.kicker{font-family:var(--font-mono);font-size:var(--fs-xs);letter-spacing:0.08em;text-transform:uppercase;color:var(--muted)}
h1{font-family:var(--font-display);font-weight:700;letter-spacing:-0.02em;font-size:clamp(2rem,6vw,var(--fs-3xl));line-height:1.15;margin:var(--s4) 0 var(--s4)}
h1 em{font-style:normal;color:var(--accent)}
.hero p{max-width:720px}
.hero .links{margin-top:var(--s5)}
/* The border is load-bearing in light mode, and its absence was a live defect
   (F039, estate audit of 5 August 2026). This is a five-segment scale of the
   sequential palette, and --seq1 measures 1.01:1 against the light --bg, --seq2
   1.53:1: the first segment was invisible and the second nearly so, so a scale
   drawn to say "five bands" showed three. Nothing is wrong with the tokens,
   which are correct as a sequence, and they are deliberately not touched here:
   darkening --seq1 would move a colour the app's own scale and the example
   cards' needles also use. What was missing is an edge for the pale end to sit
   against.

   --border-control, NOT --border, and the audit's own prescription was --border.
   Measured in the browser in light mode before accepting it: --border is 1.18:1
   against the page background, so the border added to make an invisible segment
   visible would itself have been invisible, in exactly the mode where the
   problem lives. --border-strong is 1.44:1 and no better. --border-control is
   3.07:1 and exists for precisely this, "the boundary you need in order to
   identify the thing" (WCAG 1.4.11).

   The app already learned this and wrote it down: ui/client.css's .avail.na .mk
   and .pill.na used --border-strong, measured effectively invisible in light
   mode, and moved to --border-control, pinned by
   test_unavailable_state_markers_use_the_accessible_border. This is the same
   defect one repository over. .scale below and .scale-continuous in
   ui/client.css both still use --border; they are gradients with no pale end
   against --bg, so they read, but they are the weaker choice and worth revisiting
   together rather than one at a time. */
.band{display:flex;height:6px;border-radius:var(--r-pill);overflow:hidden;margin:var(--s6) 0 0;max-width:280px;
  border:1px solid var(--border-control)}
.band span{flex:1}
.band .b1{background:var(--seq1)}.band .b2{background:var(--seq2)}.band .b3{background:var(--seq3)}.band .b4{background:var(--seq4)}.band .b5{background:var(--seq5)}
.steps{padding:var(--s6) 0}
/* Air before both card grids, and its absence was a real measured defect, not a
   matter of taste. The rhythm inside #examples ran h2, 12px, lead paragraph,
   16px, second paragraph, and then ZERO to the top of the tile row: the grid's
   own `gap` spaces the cards from each other and nothing spaced the grid from
   the prose introducing it. #how had the same 0px. Reported by Jan on 6 August
   2026 as missing spacing, and confirmed in the browser at 1280px before the
   change.

   --s5 (24px), not the --s3 (12px) the plan first proposed. 12px is the tightest
   gap in the section, so the grid would still have read as attached to the
   paragraph above it; a row of cards is a bigger structural break than a line of
   prose and should have more air, not the same. 24px is the plan's own stated
   acceptance figure, and it is a step on the existing scale rather than a new
   number. */
.stepgrid,.examplegrid{margin-top:var(--s5)}
.stepgrid{display:grid;gap:var(--s4)}
/* display:flex;flex-direction:column ADDED, Jan's review of 14 August 2026:
   the 02 stage list and the 03 stamp sit CENTRED in their tile's resting
   space (auto margins on a flex column), not parked bottom-left. */
.step{background:var(--surface);border:1px solid var(--border-strong);border-radius:var(--r-lg);padding:var(--s5);box-shadow:var(--shadow);display:flex;flex-direction:column}
.step .k{font-family:var(--font-mono);font-size:var(--fs-xs);letter-spacing:0.08em;text-transform:uppercase;color:var(--accent);display:block;margin-bottom:var(--s2)}
.step h3{font-family:var(--font-display);font-weight:600;letter-spacing:-0.01em;font-size:var(--fs-md);margin-bottom:var(--s2)}
.step p{color:var(--muted)}
/* -- the deep-run stage strip --------------------------------------------
   The four .stg rules and both keyframes below are BYTE-IDENTICAL to the ones
   in ui/client.css in the prototype repository, and .runbadge is copied on the
   same terms, so any difference between the two files shows up in a diff rather
   than as a near-miss nobody sees. The same rule already governs .topnav and
   body>footer above.

   .stageflow's margin-top is the app's value, where the strip follows a button.
   Here it follows a .secline and .stagedemo carries the outer spacing.
   Byte-identity is worth more than tuning it: do not "fix" it.

   WHAT IS NOT COPIED is the walk. In the app it is driven by a real job through
   createStageWalker; here it loops from site.js, and only while it is on
   screen. .stagedemo is the one rule genuinely new to this page. */
.stageflow{display:flex;gap:var(--s2);flex-wrap:wrap;margin-top:var(--s5)}
.stg{font-family:var(--font-mono);font-size:var(--fs-xs);padding:7px 11px;border-radius:var(--r-sm);border:1px solid var(--border);color:var(--muted)}
.stg.done{color:var(--on-accent);background:var(--accent);border-color:var(--accent)}
.stg.next{color:var(--text);border-color:var(--border-strong)}
@keyframes stg-pulse{50%{border-color:var(--accent);color:var(--accent-deep)}}
.stg.next{animation:stg-pulse 1.4s ease-in-out infinite}
@keyframes stg-land{from{background:var(--accent-tint);border-color:var(--accent-subtle);color:var(--accent-deep)}}
.stg.done:not(:has(~ .stg.done)){animation:stg-land .26s ease-out}
.runbadge{display:block;font-family:var(--font-mono);font-size:.7rem;line-height:1.6;color:var(--muted);
  max-width:72ch;margin:var(--s3) 0 0}
.stagedemo{margin-top:var(--s5)}
/* -- the strip lives INSIDE the 02 · Evidence tile since Jan's review of
   10 August 2026, second pass (the first framed it as its own labelled panel;
   still too much page for one demonstration — "only what's needed"). Chips
   only, no runbadge on this page. Third pass, same day, drawn on the chips:
   "make this more visual process" — the three stages render as a vertical
   stepper, dots on a rail in the film's circuit grammar, instead of three
   stacked boxes. All of it landing-only presentation scoped under .step: the
   nine lifted rules above stay byte-identical to ui/client.css, and site.js's
   .done/.next walk lights the DOTS here instead of filling the chip. The bare
   .stagedemo rule above and .runbadge above it are unused on this page now and
   kept anyway: both are shared vocabulary with ui/client.css and stay for
   diffability. */
.step .stagedemo{margin:auto;padding:var(--s3) 0}
.step .stagedemo .stageflow{margin-top:0;flex-direction:column;flex-wrap:nowrap;gap:2px;position:relative;padding-left:20px}
.step .stagedemo .stageflow::before{content:"";position:absolute;left:4px;top:12px;bottom:12px;width:2px;background:var(--border)}
.step .stagedemo .stg{border:0;border-radius:0;background:none;padding:4px 0;position:relative}
.step .stagedemo .stg::before{content:"";position:absolute;left:-20px;top:50%;translate:0 -50%;width:10px;height:10px;border-radius:50%;border:2px solid var(--border-control);background:var(--surface)}
.step .stagedemo .stg.done{background:none;border-color:transparent;color:var(--muted)}
.step .stagedemo .stg.done::before{background:var(--accent);border-color:var(--accent);box-shadow:0 0 10px var(--glow-2)}
.step .stagedemo .stg.next{color:var(--text)}
.step .stagedemo .stg.next::before{border-color:var(--accent)}
/* the 03 tile's stamp of approval (Jan's drawing, 10 August 2026, in the
   tile's resting space): rubber-stamp lockup — double ring, slight rotation,
   signature squiggle with deliberately NO name. Decorative; the tile's
   paragraph carries the claim, and "who signed and when" stays the
   dossier's. */
.step .cstamp{display:inline-flex;flex-direction:column;align-items:center;gap:2px;margin:auto;padding:9px 16px;border:1.5px solid var(--accent);border-radius:var(--r-sm);color:var(--accent);font-family:var(--font-mono);font-size:var(--fs-xs);letter-spacing:.14em;text-transform:uppercase;rotate:-3deg;position:relative;filter:drop-shadow(0 0 10px var(--glow-1,transparent))}
.step .cstamp::before{content:"";position:absolute;inset:3px;border:1px solid color-mix(in oklab,var(--accent) 45%,transparent);border-radius:calc(var(--r-sm) - 3px)}
.step .cstamp svg{width:64px;height:14px}
/* the 01 tile's data layers as a list, lever icons for bullets (Jan,
   10 August 2026). Inline glyphs, not lever-icons.js: the public CSP drops
   that component's injected <style>. Mono at chip size, so 01's layers and
   02's stages read as the same kind of thing — the machine's parts. */
.step .layerlist{list-style:none;margin:var(--s3) auto;padding:0;display:flex;flex-direction:column;gap:7px}
.step .layerlist li{display:flex;align-items:center;gap:9px;font-family:var(--font-mono);font-size:var(--fs-xs);color:var(--muted)}
.step .layerlist .lico{display:inline-flex;width:15px;height:15px;color:var(--accent);flex:none}
.step .layerlist .lico svg{width:100%;height:100%}
.examples{padding:var(--s6) 0}
.examplegrid{display:grid;gap:var(--s4)}
/* --border-strong, not --border: the card fill and the page background are
   close in both modes and closest in dark, so the edge is what separates
   them. The accent rule along the top doubles as the affordance that says
   this card leads somewhere, which a card of plain prose does not. */
.ex{background:var(--surface);border:1px solid var(--border-strong);border-radius:var(--r-lg);padding:var(--s5);box-shadow:var(--shadow);display:flex;flex-direction:column;gap:var(--s3);position:relative;overflow:hidden}
.ex::before{content:"";position:absolute;inset:0 0 auto;height:2px;background:var(--accent)}
.ex .k{font-family:var(--font-mono);font-size:var(--fs-xs);letter-spacing:0.08em;text-transform:uppercase;color:var(--muted)}
/* -- the sample result, as a MINI-CERTIFICATE (17 August 2026) ----------
   This was a giant numeral over a continuous scale with a needle, and a band
   pill behind a <details>. That is the layout ui/client.js drew until the same
   day; the app draws a certificate now, on every surface that shows a readiness
   result, so these three cards were advertising a UI the product had stopped
   having. .score, .of, .scale, .needle, .p77/.p61/.p45, .bandtag and the whole
   .bandwhat/.bandline/.bandwhy disclosure left together, on both sides at once
   (design/shared-baseline.json was regenerated in the same commit).

   The ring encodes the score as a 0-100 arc via pathLength="100", the same
   identity map the app's card uses, and NO band threshold -- the retired scale
   encoded none either and that was the point of it.

   The arc length is a CLASS, never a style="stroke-dasharray:…" attribute, for
   exactly the reason the needle positions were classes: the CSP ships
   style-src 'self', so an inline style attribute is dropped silently in the
   browser and in no test here. One class per curated site. */
.exscore{display:flex;align-items:center;gap:var(--s4);margin-top:var(--s2)}
.exring{position:relative;width:88px;height:88px;flex:none}
.exring svg{width:100%;height:100%;display:block}
.extrk{fill:none;stroke:var(--border-control);stroke-width:6}
.exarc{fill:none;stroke:var(--live);stroke-width:6;stroke-linecap:round;
  transform:rotate(-90deg);transform-origin:50% 50%;transform-box:fill-box}
.s77{stroke-dasharray:77 23}.s61{stroke-dasharray:61 39}.s45{stroke-dasharray:45 55}
.ex .exnum{position:absolute;inset:0;display:grid;place-content:center;text-align:center}
.ex .exn{font-family:var(--font-display);font-weight:700;font-size:var(--fs-xl);line-height:1;
  letter-spacing:-0.04em;font-variant-numeric:tabular-nums}
.ex .exd{display:block;font-family:var(--font-mono);font-size:0.62rem;color:var(--muted);margin-top:2px}
.exgrade{display:flex;align-items:center;gap:var(--s3);min-width:0}
.exgl{font-family:var(--font-display);font-weight:700;font-size:var(--fs-2xl);line-height:.85;
  letter-spacing:-0.02em;color:var(--live)}
.exgt{display:flex;flex-direction:column;gap:3px;min-width:0}
.exgt b{font-family:var(--font-mono);font-weight:400;font-size:var(--fs-xs);
  letter-spacing:0.16em;text-transform:uppercase}
.exgt small{font-family:var(--font-mono);font-size:0.62rem;letter-spacing:0.08em;color:var(--muted)}
/* Visible, with nothing to open, matching the card it mirrors. Same copy as the
   app (ui/client.js BANDS), which is what it always was. */
.exband{font-size:var(--fs-sm);color:var(--muted);line-height:1.6;margin:0}
.exband a{color:var(--accent)}
.ex p{color:var(--muted)}
.ex .exstate{font-family:var(--font-mono);font-size:var(--fs-xs);letter-spacing:0.04em;color:var(--muted)}
.ex .exlink{display:inline-flex;align-items:center;min-height:44px;margin-top:auto;color:var(--accent);font-weight:500;text-decoration:underline;text-underline-offset:2px}
.ex .exlink:hover{color:var(--accent-deep)}
.ex .exlink:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

.method{padding:var(--s6) 0}
.principles{list-style:none;display:flex;flex-direction:column;gap:var(--s4);margin:var(--s4) 0}
.principles li{border-left:2px solid var(--accent-subtle);padding-left:var(--s4)}
.who{padding:var(--s6) 0}
.who p{max-width:720px}
.partners{padding:var(--s6) 0}
.partner{background:var(--surface);border:1px solid var(--border-strong);border-radius:var(--r-lg);padding:var(--s5);box-shadow:var(--shadow)}
.partner p{max-width:66ch}
.partnercols{display:grid;gap:var(--s4);margin:var(--s4) 0}
.partnercols h3{font-family:var(--font-mono);font-size:var(--fs-xs);letter-spacing:0.14em;text-transform:uppercase;color:var(--muted);margin-bottom:var(--s2);font-weight:600}
.partnercols ul{list-style:none}
.partnercols li{position:relative;padding-left:var(--s4)}
.partnercols li::before{content:"·";position:absolute;left:0;color:var(--accent);font-weight:700}
.waitlist{padding:var(--s6) 0 var(--s8)}
.waitlist p{max-width:720px;margin-bottom:var(--s4)}
.waitlist .alt{color:var(--muted);font-size:var(--fs-sm)}
.wlform{display:flex;flex-direction:column;gap:var(--s4);max-width:480px;margin:var(--s5) 0}
.wlform .field{display:flex;flex-direction:column;gap:var(--s1)}
.wlform label{font-size:var(--fs-sm);font-weight:500}
.wlform .opt{color:var(--muted);font-weight:400}
.wlform input[type=email],.wlform input[type=text],.wlform select{min-height:44px;padding:0 var(--s3);background:var(--surface);color:var(--text);border:1px solid var(--border-control);border-radius:var(--r-md);font:inherit;font-size:var(--fs-base)}
.wlform input:focus-visible,.wlform select:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.wlform input[type=email]:user-invalid{border-color:var(--error)}
.wlform .consent{display:flex;align-items:flex-start;gap:var(--s3);min-height:44px}
.wlform .consent input{width:20px;height:20px;margin-top:2px;accent-color:var(--accent);flex-shrink:0}
.wlform .consent label{font-weight:400}
.wlform button.primary{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:0 var(--s5);border:0;border-radius:var(--r-pill);background:var(--accent);color:var(--on-accent);font:inherit;font-size:var(--fs-sm);font-weight:500;cursor:pointer;align-self:flex-start}
.wlform button.primary:hover{background:var(--accent-deep)}
.wlform button.primary:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* Visually hidden, not display:none, which the naive bots skip too. */
.hp{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden}
.fineprint{color:var(--muted);font-size:var(--fs-sm)}
.links{display:flex;flex-wrap:wrap;gap:var(--s3)}
.links a{display:inline-flex;align-items:center;min-height:44px;padding:0 var(--s5);border-radius:var(--r-pill);text-decoration:none;font-weight:500;font-size:var(--fs-sm)}
.links a:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.links .primary{background:var(--accent);color:var(--on-accent)}
.links .primary:hover{background:var(--accent-deep)}
.links .ghost{border:1px solid var(--border-strong);color:var(--text);background:var(--surface)}
.links .ghost:hover{border-color:var(--accent)}
/* body>footer, not footer, since 5 August 2026. These rules were unscoped,
   and unscoped they held an assumption nobody had written down: that this page
   has exactly one <footer>. The app's stylesheet made that same bet on
   <header> and lost it on 1 August 2026, when a bare header{} rule, correct
   while the page had one header, painted its background and border over the
   navigation the moment client.js rendered a second one. Nothing here renders
   a second footer today. The scope costs four characters and removes the bet.

   The app carries the identical block under body>footer in ui/client.css. It
   differs by exactly one number, max-width, and the note there explains why.

   .foot SINCE 6 AUGUST 2026, WHERE THIS SAID .wrap, AND THAT WAS A LIVE BUG.
   The footer reused .wrap, this page's container class, and then wrote
   `padding:var(--s5) 0` to give itself vertical room. .wrap declares
   `padding:0 var(--s5)`, and the footer rule is higher specificity and a
   SHORTHAND, so it did not add a top and bottom to the sides already there --
   it replaced the whole box and set the sides to ZERO. On a phone "Terms" then
   sat flush against the viewport edge while every other band on this page kept
   its 24px gutter. Jan saw it as "the app's footer has the spacing". It did:
   the app never borrowed .wrap, because .wrap means something else in
   client.css, and so it never had this to lose.

   The rule worth keeping is not the pixel. Do not borrow a container class for
   a different job and then override its shorthand -- a footer row is not a page
   band. It has its own class and its own complete box now, on both surfaces.
   max-width is restated here rather than inherited for the same reason.

   THE TAGLINE IS ITS OWN CENTRED ROW, same date, on both surfaces. width:100%
   in a flex-wrap container guarantees it rather than leaving it to wrapping: it
   was the third column of a space-between row, so it sat left on a phone (where
   the row wrapped) and hard right on a desktop (where it did not), and the same
   three words landed in three different places depending on how wide your
   window was. One row, centred, at every width. */
body>footer{border-top:1px solid var(--border)}
body>footer .foot{max-width:1000px;margin:0 auto;display:flex;flex-wrap:wrap;align-items:center;gap:var(--s3);justify-content:space-between;padding:var(--s5);color:var(--muted);font-size:var(--fs-sm)}
body>footer nav{display:flex;flex-wrap:wrap;gap:var(--s4)}
body>footer nav a{color:var(--muted);text-decoration:underline;text-underline-offset:2px}
body>footer nav a:hover{color:var(--text)}
body>footer nav a:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
body>footer .mono{width:100%;text-align:center;font-family:var(--font-mono);font-size:var(--fs-xs)}
/* Where an in-page anchor lands while the header is sticky: clear of the bar,
   and about a third of the way down, which is where the eye already is.
   scroll-margin-top ONLY, never together with scroll-padding-top: the two
   compose and setting both overshoots by their sum. Same rule, same values,
   as ui/client.css in the prototype repository. Overridden below 900px, where
   the header is not sticky and this offset would only add dead space. */
[id]{scroll-margin-top:max(calc(var(--head-h) + var(--s4)), 32vh)}
@media(prefers-reduced-motion:no-preference){html{scroll-behavior:smooth}}
/* The first prefers-reduced-motion:reduce block on this page, and it arrived
   with the first animation on it. The stage strip breathes and fills. The band
   caret rotated here too, unguarded since it was written and then cancelled in
   this block; both went on 17 August 2026 with the disclosure that held them,
   when the sample cards became mini-certificates and the band sentence stopped
   being something to open. None of that asked, and vestibular disorders make
   motion genuinely unpleasant rather than merely busy (WCAG 2.3.3).
   ui/client.css carries the identical concern; this is the landing half.

   NOT a blanket *{animation:none}: that reads as thorough and is the rule
   ui/client.css records rejecting, because it takes the loading spinner with
   it. Nothing here needs a spinner today, and a blanket rule would quietly
   swallow the first one that does.

   Note what is NOT in here: the walk. site.js checks the same query and paints
   all three stages done on load instead of walking them, because switching the
   animations off while a JS loop keeps adding and removing classes leaves a
   strip that changes silently and pointlessly forever. The CSS half stops the
   motion; the JS half stops the churn. Both are needed.

   The .stg.done off-switch below has to carry the whole :not(:has()) tail, and
   the short .stg.done it replaced did not work. Both :not() and :has() take the
   specificity of their most specific argument, so the rule it is cancelling is
   (0,4,0) against a bare (0,2,0), and a media query adds nothing of its own to
   close a gap like that. stg-land played for exactly the reader who had asked
   for no motion. ui/client.css carried the same mistake and fixed it on
   9 August 2026; this is the landing half, and it was found by the design
   bundle's drift gate rather than by anyone looking. The chip still ends up
   filled: animation:none removes the arrival, not the destination. */
@media(prefers-reduced-motion:reduce){
  .stg.next{animation:none}
  .stg.done:not(:has(~ .stg.done)){animation:none}
}
/* The one CONTENT breakpoint. Below it every grid is a single column. */
@media(min-width:760px){
  .stepgrid,.examplegrid{grid-template-columns:repeat(3,1fr)}
  .partnercols{grid-template-columns:1fr 1fr}
}
/* The bar's own breakpoint, and it is deliberately NOT the content one. The
   nav restore used to sit in the 760px block above; it moved out on 6 August
   2026, when the scrolling row stopped being cosmetic and became the thing the
   pinned strip is made of. --headhide-h describes a bar of exactly two rows,
   and between 760 and 899 the nav returned to width:auto and let the row count
   become emergent, so the constant would have been wrong across a 140px band
   while the header was pinned.

   SPLIT, NOT MOVED. .stepgrid, .examplegrid and .partnercols stay at 760px:
   they are content grids and have nothing to do with the bar. Editing that
   block wholesale would push the three-column card grids out to 900px, which
   is the most likely way to break this page while appearing to fix the header.
   ui/client.css carries the identical boundary. */
@media(min-width:900px){
  .topnav{order:0;width:auto;flex-wrap:wrap;overflow-x:visible}
}
/* The tagline appears only on a bar that fits ONE ROW with it. "readiness,
   evidenced" is the brand's promise and worth its line on any screen with a
   line to spare, and the wordmark still says GRIDREADY where there is not one.

   1000px, raised from 480px on 6 August 2026, and the reason is --head-h rather
   than taste. That constant describes a single-row bar and every anchor's
   clearance derives from it. Measured at 1440px: .brand 300 (187 of it the
   tagline), .topnav 528, .hmeta 80, plus two 16px gaps -- 940px of bar needed
   against the 952px .topbar allows at its max-width:1000px cap. Twelve pixels
   of slack at the top end, and NONE below about 988px: through the whole
   900-987 band this bar measured 125px while --head-h claimed 77, so every
   heading a reader followed a link to landed 32px underneath it. That shipped,
   and nothing here could have caught it, because this repository has no tests.

   The same threshold is set in ui/client.css, where it is the larger of two
   requirements. One number in both files is worth more than a tighter fit in
   each: these files are kept diff-clean by hand, and a rule that differs by a
   number between them is the kind of drift nobody sees. */
@media(max-width:999px){.brand .eyebrow{display:none}}
/* Narrow viewports: the bar is a different height, so the anchor offset is
   RE-DERIVED rather than dropped.

   THE HEADER IS STICKY ON A PHONE AGAIN, 6 August 2026, AND THIS IS NOT A
   REVERT OF F138. Read the diff before assuming otherwise. What F138 fixed was
   a 218px bar pinned on every scroll -- a HEIGHT defect, not a stickiness one.
   Jan asked for the navigation to still be there when he scrolls, which
   un-sticking had taken away, so it is pinned again at 61px instead of 125px,
   by hiding the bar's top row rather than by keeping all of it.

   The mechanism is a NEGATIVE sticky top: the header scrolls up with the page
   until `top` is satisfied, then pins with --headhide-h of itself above the
   viewport. Sticky hides the TOP of a box and never the bottom, so the row
   that stays is whatever the markup puts last -- here the nav, which is the row
   worth keeping and is already last on a phone (.topnav's order:3 above). The
   wordmark is what scrolls away. A strip carrying the brand and the CTA instead
   cannot be built in CSS: a.navcta is a child of nav.topnav on both pages here
   and on all five app pages, and CSS cannot lift a grandchild out of its
   parent's flex context.

   The hidden height must be EXACT -- one pixel too much clips the first nav
   pill, one too little leaves a sliver of the row above -- so the bar has to be
   two rows here and never three. It already is on this page, which has never
   carried the app's state chip. That chip is what made the app's bar three rows
   and 180px at 375px, and ui/client.css now hides it below 1100px for exactly
   this reason. Below that width the two bars are structurally identical: brand
   row, nav row, same padding, same gap, same two constants.

   --head-h is a constant and .topbar is flex-wrap:wrap, so the constant is
   only true while the bar fits on one line. It does not below roughly 900px:
   measured live, the bar is 124px at 760px wide and 222px at 375px, where the
   links take two rows of their own. A sticky bar that size eats a fifth to a
   third of a phone viewport on every scroll, and the 32vh floor above does
   not rescue the anchor, because that floor grows with viewport HEIGHT while
   the bar grows with narrow WIDTH. At 375x667 the floor is 213px against a
   222px bar, so the target heading landed 8px underneath it. Un-sticking
   fixes both at once and needs no second measured constant.

   900px, not the 760px above: at 760 the bar is still two rows. The exact
   width where it collapses depends on the nav label text, so this is set wide
   enough to hold for both surfaces rather than pinned to one measurement.
   ui/client.css in the prototype repository carries the identical block.

   body>header, NOT a bare `header`, AND THIS RULE SHIPPED BROKEN FOR FIVE DAYS.
   It was written `header{position:static}` while the sticky rule it overrides is
   spelled `body>header{position:sticky}` at line 130. Two type selectors beat
   one, so the override lost every time, and this header stayed sticky on every
   phone that ever loaded the page. Found by the estate audit of 5 August 2026
   (F138) and measured on production at 375px: 218px of sticky bar, a third of
   the viewport, pinned in place on every scroll. Every symptom the paragraphs
   above describe as fixed was in fact live.

   The lesson is the one 1 August taught a layer up, and it is worth stating
   plainly: RAISING A RULE'S SPECIFICITY IS A BREAKING CHANGE TO EVERY OVERRIDE
   OF IT. Scoping `header` to `body>header` at line 130 silently broke this line
   in the same commit, and nothing failed, because this repository has no tests.
   Scope the rule and every override of it together, or do neither.

   That lesson binds this block harder now than it did when it only set
   `position`. The override below sets `top`, and so does the base rule at
   body>header, so a bare `header{top:...}` would lose in precisely the same way
   and leave the whole bar pinned at 0 -- which is the 125px failure wearing the
   fix's clothes. The prototype's test_the_page_anchors_clear_the_sticky_bar
   asserts the app's half of this; nothing asserts this file's half.

   The 32vh floor does not come down here with the offset. On a 667px viewport
   it is 213px, a third of the screen of blank space above every heading a
   reader follows a link to. */
@media(max-width:899px){
  body>header{top:calc(-1 * var(--headhide-h))}
  /* The hidden row gives itself back when something in it takes focus. Two
     focusable controls live in the --headhide-h that scrolls out of sight: the
     wordmark link and the colour-mode button, measured at 375px scrolled to
     800 at -35.5 and -40, both entirely off screen.

     A sticky header cannot be scrolled back into view the way ordinary content
     can, because scrolling moves it WITH the page, so the usual guarantee that
     focusing a control reveals it has nothing to work with. Without this rule a
     keyboard user can land on two controls with no focus ring anywhere on
     screen, which is WCAG 2.4.11.

     WHAT IS VERIFIED AND WHAT IS NOT, narrowed on 6 August 2026. Two
     independent halves, and only one resisted testing.

     Half A, the layout: does top:0 reveal the two controls? MEASURED on the
     app, whose header is this one, at 375x812 scrolled to y=600, by forcing the
     declaration through CSSOM and reading the boxes. Pinned, .brand spans y -35
     to -4 and .mode y -40 to 0, both wholly above the viewport; with top:0 they
     are 13 to 44 and 8 to 48, both FULLY visible with 8px of clearance, more
     than the 2px outline plus 2px offset a focus ring needs.
     CSS.supports(selector(:has(*))) is true, so the selector is live rather
     than silently dropped.

     Half B, the trigger: does :focus match when a keyboard reaches these? NOT
     verified, and not verifiable under automation -- the pane is hidden, so
     document.hasFocus() is false and :focus matches nothing, which makes such
     a measurement meaningless rather than noisy. That half is standard browser
     behaviour rather than anything this file controls, as is whether the
     browser's own scroll-into-view would have rescued the control unaided.
     CHECK IT IN ANY BROWSER WINDOW UNDER 900px WIDE WITH A KEYBOARD: the gate
     is viewport width, not the device, so a narrowed desktop window runs this
     exact code path.

     The rule is correct either way: if the browser does rescue the control it
     can only do so above --headhide-h, where the header has not reached its pin
     point and top:0 renders identically.

     :focus rather than :focus-visible, because a pointer user can only click
     what they can already see, while :focus-visible can miss focus moved
     programmatically or by assistive technology. .topnav is deliberately not in
     the selector: those pills are in the strip that stays visible, and
     expanding the bar under them while a reader tabs along the row would move
     what they are reading for no reason. Identical rule in ui/client.css. */
  body>header:has(.brand:focus,.hmeta :focus){top:0}
  .topbar{padding:var(--s2) var(--s5)}
  [id]{scroll-margin-top:calc(var(--headstrip-h) + var(--s4))}
}
/* ══ DIRECTION B · "ENERGISE", adopted 10 August 2026, shipped 17 August ═══
   The rollout's own additions, kept as one block below everything above for
   the same reason the rest of this file reads top to bottom as it does:
   everything above is what shipped before this round; this is what the round
   added. Chosen from directions/B-energise.html, refined in
   directions/B3-electron-intro.html in the prototype's design bundle. The
   four tokens this block reads (--live, --ease-flow, --m-rise, --m-sweep) are
   declared in the four colour blocks and the FOUNDATION area of :root above,
   not here — see the file's opening comment.

   The grammar (design/README.md · Motion): the hero brightens into place in
   reading order — opacity and colour only, nothing moves — the accent clause
   takes its gradient glow, ONE sweep runs the readiness band, and then
   everything is still. NO BEAM: B-energise's crossing light was dropped in
   B3 and stays dropped. Under reduced motion the head gate never mounts the
   film and the block's own reduce rule below renders the final lit state
   instantly. */
/* --f-* are the film/honesty-band's own dark-stage palette, mode-independent
   (the film looks the same regardless of the page's light/dark mode). Same
   plain-sRGB-then-@supports-oklch pattern as --live above and for the same
   reason: these were bare oklch() literals with no fallback at all until a
   review pass on 17 August 2026 caught it, and an unsupported oklch() value
   makes the WHOLE declaration invalid, not "close enough" -- with no sRGB
   line at all, a browser that can't parse oklch() would have --f-bg,
   --f-line, --f-dim, --f-text and --f-live all compute to nothing, which
   breaks the film into invisible text on an unstyled background and breaks
   the honesty band the same way (.method reads its own --f-body the same
   two-step way, below). --f-live's sRGB value is deliberately identical to
   dark-mode --live's (#40E3B1): the film's "live" glow and the app's dark
   accent are the same green on purpose. --fglow/--fbloom-c stay unconditional
   color-mix() reading var(--f-live), same as --glow-*/--bloom-c/--sheen read
   var(--live) above with no extra guard of their own -- color-mix() and
   oklch() shipped in browsers close enough together that one guard on the
   base colour token covers both. */
:root{--glow-1:color-mix(in oklab,var(--live) 22%,transparent);--glow-2:color-mix(in oklab,var(--live) 32%,transparent);--glow-3:color-mix(in oklab,var(--live) 55%,transparent);--bloom-c:color-mix(in oklab,var(--live) 15%,transparent);--sheen:color-mix(in oklab,var(--live) 40%,#fff);
  --f-bg:#0D1420;--f-line:#2E3B4E;--f-dim:#67748A;--f-text:#F5F7FA;--f-live:#40E3B1;--fglow:color-mix(in oklab,var(--f-live) 55%,transparent);--fbloom-c:color-mix(in oklab,var(--f-live) 22%,transparent)}
@supports (color: oklch(0 0 0)) {
  :root{--f-bg:oklch(.14 .025 255);--f-line:oklch(.34 .04 255);--f-dim:oklch(.55 .035 250);--f-text:oklch(.97 .005 250);--f-live:oklch(.82 .15 168)}
}
/* ── hero energisation. The lede is the hero's one unclassed <p>, hence
   :not(.kicker) rather than a new class on shipped markup. ── */
.hero{position:relative;overflow:hidden}
.hero::before{content:"";position:absolute;left:-8%;top:-28%;width:56%;aspect-ratio:1;background:radial-gradient(circle,var(--bloom-c),transparent 66%);pointer-events:none;animation:bloom 1.6s var(--ease-flow) .3s both}
@keyframes bloom{from{opacity:0;transform:scale(.85)}}
.hero .kicker{animation:comeup var(--m-rise) var(--ease-flow) .1s backwards}
.hero h1{animation:comeup var(--m-rise) var(--ease-flow) .25s backwards}
.hero p:not(.kicker){animation:comeup var(--m-rise) var(--ease-flow) .4s backwards}
@keyframes comeup{from{opacity:.45}}
@supports(-webkit-background-clip:text){
.hero h1 em{background:linear-gradient(90deg,var(--accent),var(--live) 85%);-webkit-background-clip:text;background-clip:text;color:transparent;filter:drop-shadow(0 1px 12px var(--glow-2));animation:emglow 1s var(--ease-flow) .55s backwards}
@keyframes emglow{from{opacity:.45;filter:none}}
}
.hero .links{animation:comeup2 var(--m-rise) var(--ease-flow) .5s backwards}
@keyframes comeup2{from{opacity:.7}}
.hero .band{position:relative;animation:comeup2 var(--m-rise) var(--ease-flow) .6s backwards}
.hero .band::after{content:"";position:absolute;inset:0;transform:translateX(-110%);background:linear-gradient(90deg,transparent,var(--sheen),transparent);animation:sweep .9s var(--ease-flow) 1.05s both}
@keyframes sweep{to{transform:translateX(110%)}}
/* ── the mark joins the lockup (stage 6): W7 "GR switch", the brand's SVG
   geometry inlined in the header markup, accent ink, 22px beside the fs-md
   wordmark. ── */
.brand .mark{height:22px;width:auto;flex:none;color:var(--accent);align-self:center}
/* ── current on hover/focus of the page's actions: the estate's one CTA
   behaviour. Colour and shadow only, so it stays outside the reduce rule. ── */
.links a{transition:box-shadow .3s var(--ease-flow),background .3s var(--ease-flow),border-color .3s var(--ease-flow)}
.links .primary:hover,.links .primary:focus-visible{background:var(--accent-deep);box-shadow:0 0 20px var(--glow-2)}
.links .ghost:hover{border-color:var(--accent);box-shadow:0 0 14px var(--glow-1)}
.wlform button.primary{transition:box-shadow .3s var(--ease-flow),background .3s var(--ease-flow)}
.wlform button.primary:hover,.wlform button.primary:focus-visible{background:var(--accent-deep);box-shadow:0 0 20px var(--glow-2)}
/* ── the nav CTA takes the same behaviour (Jan's review, 10 August 2026: one
   CTA grammar across the estate). Same specificity as the snapshot's own
   hover rule above, later in document order, so this wins; --on-accent is
   restated for the same reason the snapshot restates it.
   THIS IS A SHARED-CLASS CHANGE: .navcta and .topnav both gain rules here that
   ui/client.css in the prototype repository does not yet carry (checked
   17 August 2026 against .claude/worktrees/adopt-estate-skin/ui/client.css).
   The design bundle's drift gate will report both as changed on the landing
   side only; that is expected and is this round's own record of the delta,
   not a defect. ── */
.topnav a.navcta{transition:background .3s var(--ease-flow),box-shadow .3s var(--ease-flow)}
.topnav a.navcta:hover,.topnav a.navcta:focus-visible{background:var(--accent-deep);color:var(--on-accent);box-shadow:0 0 20px var(--glow-2)}
/* ── ≥900px the nav sits centred in the bar (Jan, 10 Aug 2026), its auto
   margins splitting the free space against the brand and .hmeta — same rule
   as the app bar. Below 900px the snapshot's width:100% makes this a no-op,
   so no media query is needed. ── */
.topnav{margin-inline:auto}
/* ── the honesty band (Jan's review, 10 August 2026: the method section read
   bland — "bring visuals, colour contrast, motion" — and the .who line under
   it floated unanchored; it closes the band now as .whofor). The band borrows
   the intro film's mode-independent palette, so the film's circuit surfaces
   once mid-page. The snapshot's .method and .principles rules above stay as
   the shipped record; everything here overrides them. Rails charge and text
   brightens into place on first scroll-into-view — once, nothing looping at
   rest (Direction B). .arm is added by script only when motion is allowed, so
   no-JS and reduced-motion both render the lit state.
   THE .wrap HAIRLINE RULE BELOW IS A SHARED-CLASS CHANGE TOO: .wrap gains this
   one new rule, which hides the generic section-divider hairline immediately
   before and after the dark band (it draws its own top/bottom border via
   border-block on .method instead, so the two would otherwise double up). ── */
.method{--f-body:#C3CCDD;background:var(--f-bg);border-block:1px solid var(--f-line);padding:var(--s7) 0}
@supports (color: oklch(0 0 0)) {
  .method{--f-body:oklch(.78 .02 250)}
}
main section+section.method>.wrap:first-child::before,.method+section>.wrap:first-child::before{display:none}
.method .sec{color:var(--f-text)}
.method .principles{max-width:none;display:grid;gap:var(--s5);margin:var(--s5) 0}
@media(min-width:760px){.method .principles{grid-template-columns:repeat(3,1fr)}}
.method .principles li{border-left:0;padding:var(--s4) 0 0;position:relative;color:var(--f-body);transition:opacity var(--m-rise) var(--ease-flow),translate var(--m-rise) var(--ease-flow)}
.method .principles li::before{content:"";position:absolute;left:0;top:0;width:100%;height:2px;background:var(--f-line)}
.method .principles li::after{content:"";position:absolute;left:0;top:0;width:100%;height:2px;background:var(--f-live);box-shadow:0 0 14px var(--fglow);transform-origin:left;transition:transform var(--m-sweep) var(--ease-flow)}
.method .principles li strong{display:block;font-family:var(--font-display);font-weight:600;letter-spacing:-0.01em;font-size:var(--fs-md);color:var(--f-text);margin-bottom:var(--s2)}
.method p{color:var(--f-body)}
.method p a{color:var(--f-live)}
.method p a:hover{color:var(--f-text)}
.method p a:focus-visible{outline-color:var(--f-live)}
.method .whofor{margin-top:var(--s6);padding-top:var(--s4);border-top:1px solid var(--f-line);color:var(--f-dim)}
/* one icon per principle (Jan, 10 Aug 2026: "icons or visual aid for each of
   the three points") — pen, dashed unknown, cited document. Drawn on the
   lever-icons.js grammar (16 grid, 1.5 stroke, round caps, currentColor),
   landing-only and inline: the lever set stays the app's five criteria.
   Decorative, aria-hidden; they brighten with their li under the charge. */
.method .principles li .pico{display:flex;width:22px;height:22px;color:var(--f-live);margin-bottom:var(--s3);filter:drop-shadow(0 0 6px var(--fglow))}
.method .principles li .pico svg{width:100%;height:100%}
.method.arm .principles li{opacity:.3;translate:0 10px}
.method.arm .principles li::after{transform:scaleX(0)}
.method.arm.lit .principles li{opacity:1;translate:0 0}
.method.arm.lit .principles li::after{transform:scaleX(1)}
.method .principles li:nth-child(2),.method .principles li:nth-child(2)::after{transition-delay:.14s}
.method .principles li:nth-child(3),.method .principles li:nth-child(3)::after{transition-delay:.28s}
/* wider fields under the wider measure (same review): 480px read as a third,
   unrelated column width against the prose above the form. 560px, not the
   full 720px measure — a single-line email field at the full prose width
   reads as a banner and its caret drifts a hand-span from its label; the
   fineprint below the form DOES sit on the 720px edge via .waitlist p. */
.wlform{max-width:560px}
/* ── the film. Scoped under .film THROUGHOUT. The B3 source used .wordmark
   for this element, which is ALSO the header brand's class on this page; an
   unscoped copy would have absolutely-positioned the site header's wordmark,
   and even the .film-scoped version would still have been caught by the
   drift gate's substring match on the shared "wordmark" class (checked
   17 August 2026: `.film-wordmark` and its three descendant rules all
   showed up fingerprinted alongside the header's actual .wordmark, despite
   never touching it). Renamed to .film-wordmark instead, its own class, so
   this decorative overlay element isn't recorded as a shared-class change at
   all -- one less thing for T10's re-baseline to explain. ── */
html.intro{overflow:hidden}
.film{display:none}
html.intro .film,.film.leave{display:grid;place-items:center;position:fixed;inset:0;z-index:60;background:var(--f-bg)}
.film.leave{animation:f-out .5s ease .05s forwards}
@keyframes f-out{to{opacity:0;visibility:hidden}}
/* .film[hidden] is what makes site.js's `film.hidden=true` (760ms after
   finish(), past f-out's .55s) a REAL dismissal rather than a no-op: the
   built-in [hidden] UA rule is display:none at (0,1,0) specificity, which
   .film.leave{display:grid...} at (0,2,0) beats regardless of source order,
   so setting the attribute alone did nothing -- confirmed 17 August 2026,
   f-out's own visibility:hidden was doing all the actual hiding and the
   attribute was dead weight. This rule matches .leave's (0,2,0) and sits
   after it, so once both are true the attribute wins and the node is
   removed from layout, paint and hit-testing outright -- a genuine second
   line of defence if the animation itself is ever interrupted or unsupported,
   not merely a restatement of what f-out already does on its own. */
.film[hidden]{display:none}
.film-scene{position:relative;width:min(94vw,980px);aspect-ratio:1000/560}
.film-scene svg{position:absolute;inset:0;width:100%;height:100%}
.film .logo-grid{animation:f-dimdown .5s var(--ease-flow) 2.4s forwards}
@keyframes f-dimdown{to{opacity:.45}}
.film .nd{fill:var(--f-dim);transform-box:fill-box;transform-origin:center;animation:f-pop .3s var(--ease-flow) backwards;animation-delay:calc(var(--d,0)*1s)}
/* Per-node stagger. These used to be seven inline style="--d:…" attributes on
   the circles themselves, which the CSP's style-src 'self' (no style-src-attr)
   silently disallows -- the attributes were dropped on load and every node
   popped at once. nth-of-type counts the .logo-grid's circle children in
   document order, and the seven .nd circles are the first seven circles in
   that group (the pivot and terminal circles come later and are unaffected),
   so this reaches exactly the same seven nodes the attributes did. */
.film .nd:nth-of-type(1){--d:.15}
.film .nd:nth-of-type(2){--d:.3}
.film .nd:nth-of-type(3){--d:.42}
.film .nd:nth-of-type(4){--d:.54}
.film .nd:nth-of-type(5){--d:.66}
.film .nd:nth-of-type(6){--d:.76}
.film .nd:nth-of-type(7){--d:.86}
@keyframes f-pop{from{opacity:0;transform:scale(.2)}}
@keyframes f-fade{from{opacity:0}}
@keyframes f-pulse{0%{transform:scale(1)}40%{transform:scale(1.7)}100%{transform:scale(1)}}
.film .garc{stroke:var(--f-dim);fill:none;stroke-dasharray:400;animation:f-arc .8s steps(8,end) .15s backwards}
@keyframes f-arc{from{stroke-dashoffset:400}}
.film .bus{stroke:var(--f-live);fill:none;stroke-dasharray:125;animation:f-seg .45s linear 1s backwards}
@keyframes f-seg{from{stroke-dashoffset:125}}
.film .rbody{stroke:var(--f-dim);fill:none;animation:f-fade .35s var(--ease-flow) 1.42s backwards}
.film .pivot{fill:var(--f-live);transform-box:fill-box;transform-origin:center;animation:f-pop .25s var(--ease-flow) 1.42s backwards,f-pulse .5s var(--ease-flow) 1.46s}
.film .arm{stroke:var(--f-live);fill:none;stroke-dasharray:75;animation:f-seg2 .3s linear 1.62s backwards}
@keyframes f-seg2{from{stroke-dashoffset:75}}
.film .term{fill:var(--f-live);transform-box:fill-box;transform-origin:center;animation:f-pop .25s var(--ease-flow) 1.88s backwards,f-pulse .5s var(--ease-flow) 1.92s}
.film .dropline{stroke:var(--f-line);fill:none;stroke-dasharray:240;animation:f-seg3 .35s linear 2s backwards,f-recede .3s ease 2.4s forwards}
@keyframes f-seg3{from{stroke-dashoffset:240}}
@keyframes f-recede{to{opacity:.3}}
.film .hz{stroke:var(--f-line);stroke-width:1.5;transform-origin:50% 50%;animation:f-hz .8s var(--ease-flow) .5s backwards}
@keyframes f-hz{from{transform:scaleX(0)}}
.film .py{stroke:var(--f-dim);stroke-width:1.5;fill:none;animation:f-fade .5s var(--ease-flow) backwards}
.film .py1{animation-delay:.8s}.film .py2{animation-delay:.92s}
.film .cat{stroke:var(--f-live);stroke-width:1.5;fill:none;stroke-dasharray:600;animation:f-cat .5s var(--ease-flow) 1.05s backwards,f-hot .4s ease 2.3s backwards}
@keyframes f-cat{from{stroke-dashoffset:600}}
@keyframes f-hot{from{stroke:var(--f-dim)}to{stroke:var(--f-live)}}
.film .electron{fill:var(--f-live);transform-box:fill-box;filter:drop-shadow(0 0 6px var(--f-live)) drop-shadow(0 0 14px var(--fglow));animation:e-run 1.75s linear 1s both}
@keyframes e-run{0%{opacity:0;translate:0 0}5%{opacity:1;translate:0 0}26%{translate:146px 0}32%{translate:146px 0}52%{translate:212px 52px}58%{translate:212px 52px}82%{translate:212px 276px}86%{opacity:1}100%{translate:77px 282px;opacity:0}}
.film-wordmark{position:absolute;left:50%;top:60%;transform:translate(-50%,-50%);text-align:center;white-space:nowrap;font-size:var(--fs-base);letter-spacing:normal}
.film-wordmark b{display:inline-block;font-family:var(--font-display);font-weight:700;letter-spacing:-.02em;font-size:clamp(2.2rem,6.5vw,3.8rem);color:var(--f-text);filter:drop-shadow(0 0 18px var(--fglow));clip-path:inset(-20% 0 -20% 0);animation:f-wm .5s var(--ease-flow) 2.35s backwards}
.film-wordmark b i{font-style:normal;color:var(--f-live)}
@keyframes f-wm{from{clip-path:inset(-20% 100% -20% 0);opacity:.3}}
.film-wordmark span{display:block;margin-top:12px;font-family:var(--font-mono);font-size:var(--fs-xs);letter-spacing:.18em;text-transform:uppercase;color:var(--f-dim);animation:f-fade .4s ease 2.7s backwards}
.film::after{content:"";position:absolute;inset:0;background:radial-gradient(circle at 50% 60%,var(--fbloom-c),transparent 52%);opacity:0;pointer-events:none}
.film.leave::after{animation:f-bloom .4s var(--ease-flow) forwards}
@keyframes f-bloom{to{opacity:1}}
.film-skip{position:absolute;right:var(--s5);bottom:var(--s5);font:inherit;font-family:var(--font-mono);font-size:var(--fs-xs);letter-spacing:.1em;color:var(--f-dim);background:none;border:1px solid var(--f-line);border-radius:var(--r-pill);padding:8px 16px;cursor:pointer;animation:f-fade .4s ease .8s backwards,f-invite .5s ease 3.05s forwards}
@keyframes f-invite{to{color:var(--f-text);border-color:var(--f-dim)}}
.film-skip:hover{color:var(--f-text);border-color:var(--f-dim)}
.film-enter{position:absolute;bottom:calc(var(--s5) + 3px);left:50%;transform:translateX(-50%);margin:0;font-family:var(--font-mono);font-size:var(--fs-xs);letter-spacing:.14em;text-transform:uppercase;color:var(--f-dim);animation:f-fade .5s ease 3.05s backwards}
/* the page holds at frame 0 while the film runs, then plays beneath the fade;
   a tab that loads in the background holds the film for first visibility */
html.intro .replayable *{animation-play-state:paused !important}
html.hold .film,html.hold .film *{animation-play-state:paused !important}
/* Targeted, like the snapshot's own reduce block above and for its reason: a
   blanket rule would swallow the first spinner this page ever gains. The
   film needs no line here — the head gate (intro-gate.js) never mounts it
   under reduce, and the honesty band's own JS gate (site.js) never arms it
   either, so neither needs a CSS canceller here. This block cancels only the
   hero's own unconditional animations, which mount in HTML with no JS gate at
   all and so need one:

     rule                             | canceller (below)                 | spec
     .hero::before{animation:bloom}   | .hero::before{animation:none}      | (0,1,1)=(0,1,1)
     .hero .kicker{animation:comeup}  | .hero .kicker{animation:none}      | (0,2,0)=(0,2,0)
     .hero h1{animation:comeup}       | .hero h1{animation:none}           | (0,1,1)=(0,1,1)
     .hero h1 em{animation:emglow}    | .hero h1 em{animation:none}        | (0,1,2)=(0,1,2)
     .hero p:not(.kicker){anim:comeup}| .hero p:not(.kicker){anim:none}    | (0,2,1)=(0,2,1)
     .hero .links{animation:comeup2}  | .hero .links{animation:none}       | (0,2,0)=(0,2,0)
     .hero .band{animation:comeup2}   | .hero .band{animation:none}        | (0,2,0)=(0,2,0)
     .hero .band::after{anim:sweep}   | .hero .band::after{anim:none}      | (0,2,1)=(0,2,1)

   Eight rules, eight cancellers. Every pairing repeats the base rule's full
   selector at matching specificity
   and sits later in source (a @media adds no specificity of its own), which is
   the 38f7408 lesson this file already learned once on .stg.done: an
   off-switch that does not match the rule it cancels loses silently. */
@media(prefers-reduced-motion:reduce){
  .hero::before,.hero .kicker,.hero h1,.hero h1 em,.hero p:not(.kicker),.hero .links,.hero .band,.hero .band::after{animation:none}
}
