/* ============================================================
   GRIMSDALE — Office Walkthrough · styles.css  (M1)
   Screen state machine · carbon / bone / signal-orange.
   Type: Clash Display (display) · Satoshi (body) · mono (data).
   Hologram cyan #4be1e3 is scene-only; orange #ff4d00 is type/UI only.
   ============================================================ */

:root{
  --carbon:#08090b;
  --soft:#0e1013;
  --ink:#eae7de;
  --dim:rgba(234,231,222,0.76);
  --faint:rgba(234,231,222,0.55); /* F1 2026-07-18: 0.38→0.55 — 0.38 measured ~3.05:1 on carbon (AA fail); 0.55 ≈ 5.25:1 */
  --line:rgba(234,231,222,0.14);
  --accent:#ff4d00;
  --ease:cubic-bezier(0.16,1,0.3,1);
  /* MOTION TOKENS v2 — this block is carried by FOUR files: styles.css :root
     (the source), 404.html, success.html and privacy.html (read-only copies in
     their own inline <style>, because those three pages ship self-contained and
     load no stylesheet). Edit all four or none. */
  /* MOTION TOKENS v2 (2026-08-10) — --ease is LOCKED and stays the workhorse
     (shared byte-identical with scene.js:47). Every curve below is either tuned
     in-family or lifted verbatim from scene.js.
       --ease-land    confirmation only, transforms only. 4.8% peak overshoot,
                      capped below the furniture's 6.5% landBump.
       --ease-decay   flash decay only — scene.js:2726's easeFlashOut verbatim.
       --ease-breathe sinusoidal, infinite loops only (velocity-continuous wrap).
     Durations collapse the shipped 200/250/260/280/300/320/400/520/620 census
     onto one scale; app.js reads them once at boot into T (no JS literal may
     mirror a token). */
  --ease-land:cubic-bezier(0.2,1.38,0.4,1);
  --ease-decay:cubic-bezier(0.64,0,0.78,0);
  --ease-breathe:cubic-bezier(0.37,0,0.63,1);
  --t-press:90ms;
  --t-quick:200ms;
  --t-state:300ms;
  --t-spark:380ms;
  --t-draw:520ms;
  --t-enter:620ms;
  --stagger:60ms;
  /* Batch 6 (2026-08-11) — the fallback CTA's hover/press orange was the file's
     last raw hex. It is a lift of --accent, not a second brand colour; naming it
     keeps the pair single-sourced and mirrors into 404.html with the rest. */
  --accent-bright:#ff6a26;
  --mono:"JetBrains Mono",Consolas,ui-monospace,monospace;
  --display:"Clash Display","Arial Black",system-ui,sans-serif;
  --body:"Satoshi",system-ui,sans-serif;
  --gutter:clamp(1.4rem,5vw,6rem);
  --gate-fill:0;
}

*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
/* A2 (2026-08-10) — scrollbar-gutter:stable. The page toggles between scrollable
   and not during boot (Lenis sets html{height:auto}, and the spacer goes from the
   enter screen's 100vh to hero's 500vh), so the classic scrollbar appeared and
   vanished under the enter frame. Every centred fixed element — .enter-inner,
   .enter-cta, .enter-alt, the preloader column — jumped ~8px sideways with it.
   Reserving the gutter permanently costs nothing visually on a carbon page and
   the scene is unaffected: scene.js sizes everything from window.innerWidth,
   which counts the scrollbar either way. */
html{background:var(--carbon);scrollbar-gutter:stable}
body{
  background:var(--carbon);
  color:var(--ink);
  font-family:var(--body);
  overflow-x:hidden;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}
.mono{font-family:var(--mono);text-transform:uppercase;letter-spacing:0.08em}
a{color:inherit;text-decoration:none}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px}

/* ---------- preloader (M4) ---------- */
/* Batch 5 (§4.4) — the preloader LEAVES, so it leaves at the exit tier
   (--t-quick). Its exit is the first half of the match cut: the enter content is
   held at opacity 0 by html.pre-hold until this fade has finished, so there is
   never more than one wordmark on screen. */
#preloader{
  position:fixed;inset:0;z-index:70;background:var(--carbon);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1.4rem;
  transition:opacity var(--t-quick) var(--ease);
}
#preloader.done{opacity:0;pointer-events:none}
.pre-mark{display:flex;flex-direction:column;align-items:center;gap:.4rem}
.pre-word{font-family:var(--display);font-weight:700;font-size:clamp(2rem,6vw,3.6rem);letter-spacing:0.01em;line-height:1}
.pre-sub{font-size:11px;letter-spacing:0.36em;color:var(--dim)}
/* A2 (2026-08-10) — both wordmarks are CENTRED display type, so when Clash
   Display resolved mid-boot they re-centred horizontally: measured 0.0029, the
   single largest layout shift on the site, and a visible FOUT on top of it.
   They hold until the face lands (html.fonts-ready, set by the inline script in
   the head — see index.html). visibility, not display/opacity: the box stays, so
   the preloader's column never re-flows, and Chromium does not track shifts in
   an invisible subtree. Heights are font-independent either way (.pre-word and
   .enter-word are both line-height:1), so nothing moves vertically.
   Both lockups are unpainted before this point anyway — .enter-mark sits behind
   the opaque preloader, and the preloader's own first label says "Loading type". */
.pre-mark,.enter-mark{transition:opacity var(--t-state) var(--ease)}
html:not(.fonts-ready) .pre-mark,
html:not(.fonts-ready) .enter-mark{visibility:hidden;opacity:0}
/* HONEST PROGRESS (2026-07-27) — the percentage was
   Math.min(95, p + Math.random()*11 + 4) on a 70ms interval: it invented a wait
   on fast connections and lied at 95 on slow ones. It now moves on the two real
   milestones the boot actually has (fonts resolved, first frame painted), and
   says which one it is rather than counting at the visitor. */
/* A2 (2026-08-10) — min-width: the label is centred and its text changes
   ("Loading type" -> "Building the room" -> "Ready"), so it re-centred on every
   milestone. Sized past the longest of the three. */
.pre-load{font-size:11px;letter-spacing:0.18em;color:var(--faint);min-width:15rem;text-align:center}
/* §4.4 — the bar is its own emitter: at full it flashes once, and the spark is
   handed on to the enter dot. Inverse-size law as data, at .copy-email's 12px
   tier; it rests transparent because the bar carries no lit state.
   The flash rides the TRACK, not .pre-bar-fill: the track is overflow:hidden (it
   is what rounds the fill's leading edge), so a box-shadow on the fill would be
   clipped away by its own parent. At the instant the spark fires the fill is at
   100%, so the track and the fill are the same lit bar. */
.pre-bar{
  width:min(200px,54vw);height:2px;background:var(--line);border-radius:2px;overflow:hidden;
  --spark-peak:0 0 12px rgba(255,77,0,0.7);
}
.pre-bar.is-spark{animation:gm-spark var(--t-spark) var(--ease-decay)}
.pre-bar-fill{
  display:block;height:100%;width:0%;background:var(--accent);border-radius:2px;
  transition:width var(--t-draw) var(--ease);
}
/* enter CTA gates on ready: orange lead-dot appears when the scene is ready */
/* Batch 5 — the dot is the last beat of the boot: opacity in at --t-state with
   gm-spark over it, flaring to 12px and settling on its own persistent 8px glow
   (--spark-rest == the resting box-shadow, exactly the gate-dot pattern). */
.enter-dot{
  width:7px;height:7px;border-radius:50%;background:var(--accent);
  box-shadow:0 0 8px rgba(255,77,0,0.7);opacity:0;
  transition:opacity var(--t-state) var(--ease);
  --spark-peak:0 0 12px rgba(255,77,0,0.7);
  --spark-rest:0 0 8px rgba(255,77,0,0.7);
}
.enter-dot.is-spark{animation:gm-spark var(--t-spark) var(--ease-decay)}
/* Batch 5 — the .5 -> 1 was a hard snap (a listed defect): the CTA arrives
   dimmed-waiting and powers on with the dot, so the change is a --t-state
   state change like every other. Nothing here is scrub-driven: the enter screen
   has no p-reveal, so this transition can never smear a scrubbed value.
   Ruling 2026-08-11 — the dimming hangs off :not(.ready), NOT [disabled]. The
   two were the same thing until the boot ignition separated them: the button is
   ENABLED the instant fonts + scene are actually ready (function never waits on
   choreography — a visitor who clicks before the dot lights just advances), while
   .ready is the visual power-on that rides the entrance's last beat. */
.enter-cta[disabled]{cursor:default}
.enter-cta:not(.ready){opacity:.5}
.enter-cta.ready{opacity:1}
.enter-cta.ready .enter-dot{opacity:1}

/* ---------- scroll driver ---------- */
/* A2 (2026-08-10) — was 500vh, but app.js's first showScreen() sets the enter
   screen's 100vh, so the page went scrollable -> not scrollable during boot and
   the classic scrollbar vanished under it. Every centred fixed element on the
   enter frame (.enter-inner, .pre-mark, .enter-cta, .enter-alt) jumped ~8px
   sideways as the viewport widened: one shift, five sources, 0.00068 measured.
   Matching the enter screen's own value means the scrollbar state never changes
   while anything centred is on screen. */
#scroll-spacer{height:100vh;width:1px;pointer-events:none}

/* ---------- scene layers ---------- */
#scene{position:fixed;inset:0;width:100vw;height:100vh;display:block;z-index:0;background:var(--carbon)}
/* CSS GRADE — REMOVED (M1, 2026-08-04).
   The tilt-shift bands (backdrop-filter:blur(5px)) and the vignette+feTurbulence
   grain used to composite over the always-repainting canvas every frame, costing
   ≈4–10 ms/frame on the open stages (A6-verified: enable 32.5 → 48.5 fps with
   both off). All three effects — gradient tilt-shift, radial vignette and the
   film grain — now live inside the WebGL grade pass in scene.js, where they run
   in linear HDR before the tone map instead of as a paint-over afterwards.
   The two elements stay in the DOM (aria-hidden, no styling) so the boot guard's
   hide list and any cached markup keep resolving; they render nothing. */
#tiltshift,#vignette{display:none}

/* fade-through-carbon veil */
#veil{
  position:fixed;inset:0;z-index:40;pointer-events:none;
  background:var(--carbon);opacity:0;
  transition:opacity var(--t-state) var(--ease);
}
html.is-transitioning #veil{pointer-events:auto}

/* ============ NAV CHROME ============ */
#chrome{
  position:fixed;top:0;left:0;right:0;z-index:30;
  display:flex;align-items:flex-start;justify-content:space-between;
  padding:clamp(1rem,2.4vw,1.7rem) var(--gutter);
  pointer-events:none;
}
#chrome > *{pointer-events:auto}
/* padding/negative-margin pair gives the wordmark a real hit area without moving it */
/* Batch 2 (press): the base list carries the RELEASE timing; :active below
   overrides transition-duration for the 90ms press-in. */
.brand{display:flex;flex-direction:column;gap:2px;line-height:1;padding:.4rem .25rem;margin:-.4rem -.25rem;transition:opacity var(--t-state) var(--ease),transform var(--t-quick) var(--ease)}
/* A10 (2026-08-10) — 110×29 on mobile, where .brand-sub is hidden and the lockup
   is one line. The hit area is grown by a pseudo, NOT by the padding pair above:
   .brand::before (the M3 AA copy pocket, near the end of this file) insets from
   the PADDING box, so raising the padding to .88rem inflated the pocket with it
   and turned a "soft darkening under the type" into the visible dark plate that
   block explicitly rejects. The padding is left exactly as the AA work tuned it. */
.brand::after{content:"";position:absolute;inset:-7.5px 0}
/* ITEM 2B — aesthetic yield: while the hero OR outro screen is active the brand
   lockup yields to the headline (opacity .38); restored on every other screen.
   Driven by the state-<screen> class that app.js's screen-change logic sets on
   <html>; releases automatically when scrolling back (e.g. to enable). */
html.state-hero .brand,
html.state-outro .brand{opacity:0.38}
/* Batch 2 (press) — the brand's FIRST hover/press states. The yield rule above is
   0,2,0 and sits later in the source than a bare .brand:hover would, so the lift
   is written against the state selectors as well: it wins on specificity, not on
   source order, and cannot be broken by a later re-order. Hover is the light
   (opacity to 1); press adds the compression. The :active half carries the tint
   too — on touch there is no hover to have supplied it (A11 contract). */
@media (hover:hover){
  .brand:hover,
  html.state-hero .brand:hover,
  html.state-outro .brand:hover{opacity:1}
}
.brand:active,
html.state-hero .brand:active,
html.state-outro .brand:active{opacity:1;transform:scale(0.97);transition-duration:var(--t-press)}
.brand-word{font-family:var(--display);font-weight:600;font-size:1.02rem;letter-spacing:0.02em;color:var(--ink)}
.brand-sub{font-size:10px;letter-spacing:0.24em;color:var(--dim)}

.chrome-right{display:flex;align-items:center;gap:clamp(.6rem,1.5vw,1.1rem)}
/* PERSISTENT CTA (2026-07-27) — the header's most prominent element used to be
   "What is responsible AI?", a definitional footnote, while the only path to a
   conversation lived inside an unlabeled hamburger. The explainer moved into the
   menu; this took its slot. Accent-bordered rather than filled so it is findable
   without fighting the composition; fills on hover. Hidden on the outro, where
   the full-width panel already owns the ask. */
.nav-cta{
  /* A10 (2026-08-10) — NOT in the snag list, which listed five sub-44px targets
     and missed this one: the persistent CTA measured 77.5×36 on mobile. Fixed
     with the same pseudo idiom as .outro-email rather than by raising min-height,
     because the 36px pill is a deliberate piece of chrome sizing and growing it
     would push against the badge and hamburger on the narrow row. */
  position:relative;
  display:inline-flex;align-items:center;white-space:nowrap;cursor:pointer;
  font-family:var(--body);font-size:13px;letter-spacing:0.01em;color:var(--accent);
  background:rgba(14,16,19,0.55);border:1px solid var(--accent);border-radius:999px;
  padding:.46rem 1rem;min-height:36px;
  transition:background var(--t-quick) var(--ease),color var(--t-quick) var(--ease),
             transform var(--t-quick) var(--ease),opacity var(--t-quick) var(--ease);
}
/* -5px not -4px: an absolutely positioned pseudo insets from the PADDING box, so
   the 1px border on each side has to be paid back. 34 + 10 = 44. */
.nav-cta::after{content:"";position:absolute;inset:-5px 0}
/* A11 2026-08-10 · Batch 2 2026-08-10 — :active = the hover tint PLUS compression.
   transition-duration alone is overridden, so the property list and the curve are
   still the base list's: press in at 90ms, release out at 200ms on --ease. */
@media (hover:hover){ .nav-cta:hover{background:var(--accent);color:var(--carbon)} }
.nav-cta:active{background:var(--accent);color:var(--carbon);transform:scale(0.97);transition-duration:var(--t-press)}
.nav-cta-short{display:none}
/* A2 (2026-08-10) — was display:none. #chrome is space-between, so .chrome-right's
   RIGHT edge is pinned and its left edge floats: removing the widest flex item
   moved the whole group (0.0018 on enable->outro, the largest of the five walk
   shifts). visibility keeps the slot and stays untabbable and unclickable. */
html.state-outro .nav-cta{visibility:hidden}
/* the badge used to render "—" in a bordered circle on five of nine screens,
   which reads as a broken component rather than as "no stage" */
/* A2 (2026-08-10) — same reasoning as .nav-cta above, twice over: the badge used
   to enter and leave the row via display, AND its .sb-name text changes per stage
   ("Discover"/"Plan"/"Implement"/"Enable"). Those were five measured shifts on a
   nine-screen walk — one as the badge appears, three as the name width changes,
   one as the CTA leaves — and nothing else in the walk shifted at all.
   Now: the badge always holds its slot (visibility, which keeps it out of the
   tab order and the a11y tree exactly as display:none did), and the name slot is
   sized by an invisible "Implement" ghost so no stage name can resize it. A
   min-width in px would have been platform-specific — --mono resolves to
   whatever is installed, since JetBrains Mono is NOT in the Fontshare request.
   The badge is aria-hidden, so the ghost is never announced. */
/* Batch 5 — the badge fades in AS THE VEIL CLEARS rather than being revealed
   already-changed. html.is-transitioning spans exactly the veil's out-swap-in, so
   the number swaps behind opaque carbon and the badge arrives with the room. The
   visibility pinning above is untouched: opacity is the only thing moving, so the
   slot never leaves the row and CLS stays 0. */
/* ENQUIRY-M1 (2026-08-16) — position:relative is the anchor for .sb-fl below.
   The badge is inline-flex and unpositioned otherwise; nothing else changes. */
.stage-badge{
  position:relative;
  display:inline-flex;visibility:hidden;align-items:center;gap:.4rem;
  font-size:11px;letter-spacing:0.14em;color:var(--dim);
  transition:opacity var(--t-state) var(--ease);
}
/* ENQUIRY-M1 — the badge's intake label. While the first-look intake is live the
   badge reads "FIRST LOOK", with no stage number. Written OUT OF FLOW and
   right-anchored, for the same A2 reason the rest of this group is pinned:
   .sb-name's ghost reserves "Implement", "FIRST LOOK" is wider, and
   .chrome-right's right edge is pinned while its left edge floats — in flow this
   label would have moved the badge and the (hidden but slotted) nav CTA every
   time the intake opened. Number and name keep their boxes and lose only their
   ink, so the swap is paint-only and CLS stays 0. The badge is aria-hidden;
   #screen-status carries the announcement. */
.sb-fl{
  position:absolute;right:0;top:50%;translate:0 -50%;
  visibility:hidden;white-space:nowrap;color:var(--accent);
}
html.fl-open .stage-badge .sb-num,
html.fl-open .stage-badge .sb-name{visibility:hidden}
html.fl-open .stage-badge .sb-fl{visibility:visible}
html.is-transitioning .stage-badge{opacity:0}
html.state-discover .stage-badge,
html.state-plan .stage-badge,
html.state-implement .stage-badge,
html.state-enable .stage-badge{visibility:visible}
/* state-outro deliberately ABSENT (F-S31, 2026-08-17): the outro is Contact,
   not a fifth stage, so its badge stays hidden like hero/problem/start-gate. */
.sb-name::after{content:"Implement";display:block;height:0;overflow:hidden;visibility:hidden}
.stage-badge .sb-num{
  display:inline-flex;align-items:center;justify-content:center;
  /* A2 (2026-08-10) — was min-width:22px. Empty it sat at 22px and grew to 27px
     the moment setStage() wrote "01", which was the last 5px of .chrome-right
     movement left after the badge and CTA were pinned. 27px is the populated
     width, and all four numbers are two digits, so this is now constant. The
     only state it changes is the empty one, which is never visible. */
  min-width:27px;height:22px;padding:0 5px;border-radius:999px;
  border:1px solid var(--line);color:var(--ink);
}
html.state-discover .stage-badge .sb-num,
html.state-plan .stage-badge .sb-num,
html.state-implement .stage-badge .sb-num,
html.state-enable .stage-badge .sb-num{border-color:var(--accent);color:var(--accent)}
/* 44×44 touch target around the same 34×24 visual box (was 34×24 overall) —
   the negative margin keeps the bars exactly where they were */
.hamburger{
  display:flex;flex-direction:column;justify-content:center;align-items:center;gap:4px;
  width:44px;height:44px;margin:-10px -5px;
  background:none;border:none;cursor:pointer;
  /* M2 — the button itself now has an opacity tier, for the one state that asks
     for it: standing down under the explainer card. The bars keep their own
     lists below; nothing here is a layout property. */
  transition:opacity var(--t-state) var(--ease);
}
.hamburger i{
  display:block;height:1.5px;width:22px;background:var(--ink);opacity:.8;
  transition:opacity var(--t-quick) var(--ease),transform var(--t-state) var(--ease),
             scale var(--t-quick) var(--ease);
}
/* A11 2026-08-10 · Batch 2 2026-08-10 — glyph-scale press: the 0.94 rides the BARS,
   never the 44px hit box (.hamburger itself is the target).
   Batch 4 2026-08-10 — the press moved from `transform` to the independent
   `scale` property so it COMPOSES with the morph below instead of replacing it:
   pressing the open X must not straighten the bars for 90ms. Same 0.94, same
   curve, same 90/200 timing; individual properties apply before `transform`, so
   each bar is compressed about its own centre and then rotated. */
@media (hover:hover){ .hamburger:hover i{opacity:1} }
.hamburger:active i{opacity:1;scale:0.94;transition-duration:var(--t-press)}
/* Batch 4 (§4.2 grammar, overlay pair) — the control states which state it is in:
   on [aria-expanded="true"] the two bars converge and cross. There are exactly
   two bars (no middle bar to fade). Each sits half a gap plus half a bar off the
   button's centre line — 4px/2 + 1.5px/2 = 2.75px — so the translate is that
   distance back to centre and the rotation is ±45°. The 44×44 hit box is the
   BUTTON; nothing here touches it, and nothing here is a layout property. */
.hamburger[aria-expanded="true"] i:first-child{transform:translateY(2.75px) rotate(45deg)}
.hamburger[aria-expanded="true"] i:last-child{transform:translateY(-2.75px) rotate(-45deg)}
/* B8 — THE BOOT DRAW (M2). The drafted-line grammar, miniaturised: the interface
   is the last thing the crew builds, and you watch its smallest control get
   built. The two bars draw themselves left to right, --t-draw on --ease, one
   --stagger apart, ONCE per page load, on the beat the chrome first appears.
   Restrained on purpose — it is chrome, not a signature moment.
   transform-origin is set ONLY for the duration of the draw, by the same
   one-shot class: the open X rotates about each bar's CENTRE, and a permanent
   left origin would swing it off its pivot. `backwards` (no forwards) hands each
   bar straight back to its own rules the moment the draw ends, so the morph, the
   press and the hover are all untouched by the time any of them can fire. */
@keyframes gm-hb-draw{from{transform:scaleX(0)}to{transform:scaleX(1)}}
html.hb-draw .hamburger i{
  transform-origin:left center;
  animation:gm-hb-draw var(--t-draw) var(--ease) backwards;
}
html.hb-draw .hamburger i:last-child{animation-delay:var(--stagger)}
/* Batch 4 ruling (2026-08-11) — THE X IS THE CLOSE CONTROL. #menu is z-index 50
   over a 97%-opaque carbon backdrop and #chrome sits at 30, so the morph was
   computing correctly and being painted behind the panel: neither visible nor
   hit-testable (elementFromPoint at the button's centre returned #menu).
   While the menu is open the chrome is lifted above it and everything on the row
   EXCEPT the hamburger stands down — the X is then the only chrome on screen and
   the only thing clickable there, which is also what it says it is. It closes the
   menu through the toggle handler that was already wired.
   Lifting the whole bar is what makes this safe rather than lifting the button
   alone: an elevated .brand or .nav-cta would be a live navigation control
   sitting on top of an open menu. Opacity + pointer-events only — the visibility
   pinning that holds .nav-cta's and the badge's slots is untouched, nothing here
   is a layout property, and CLS stays 0.
   html.menu-open is added and removed with .is-open (never at teardown): the
   chrome must be back under #veil (z-index 40) before a menu jump starts its
   transition, or it would paint through the veil.
   Scoped through #chrome deliberately: `#chrome > *{pointer-events:auto}` is an
   ID selector, so a bare `html.menu-open .brand` loses to it and the invisible
   wordmark stays clickable — measured, and exactly the latent bug this rule is
   here to close. */
html.menu-open #chrome{z-index:60}
html.menu-open #chrome .brand,
html.menu-open #chrome .nav-cta,
html.menu-open #chrome .stage-badge{opacity:0;pointer-events:none;transition-duration:var(--t-quick)}
/* .chrome-right is the row's wrapper and takes pointer-events:auto from
   `#chrome > *`, so with its contents stood down it was left swallowing clicks in
   the CTA's and badge's slots — a dead patch over the open menu where a click had
   previously fallen through to the backdrop and closed it. The row goes inert and
   the X alone opts back in. */
html.menu-open #chrome .chrome-right{pointer-events:none}
html.menu-open #chrome .hamburger{pointer-events:auto}

/* screen-reader-only utility (live region + fallback landmarks) */
.sr-only{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;
}

/* ============ SCREENS ============ */
#screens{position:relative;z-index:5}
.screen{position:fixed;inset:0;z-index:5;pointer-events:none}
.screen[hidden]{display:none}
.screen-inner{position:absolute;pointer-events:auto}
.pill{
  display:inline-flex;align-items:center;gap:.5rem;
  border:1px solid var(--line);border-radius:999px;
  font-family:var(--body);font-size:1rem;color:var(--ink);
  background:rgba(14,16,19,0.55);
  padding:.7rem 1.35rem;cursor:pointer;
  transition:border-color var(--t-quick) var(--ease),background var(--t-quick) var(--ease),transform var(--t-quick) var(--ease),scale var(--t-quick) var(--ease);
}
/* A11 2026-08-10 · Batch 2 2026-08-10 — pill-scale press. */
@media (hover:hover){ .cta-pill:hover{border-color:var(--accent);background:rgba(255,77,0,0.08)} }
.cta-pill:active{border-color:var(--accent);background:rgba(255,77,0,0.08);transform:scale(0.97);transition-duration:var(--t-press)}
/* The hero's CTA is the one pill whose `transform` is not ours to write: the
   arrival animation (html.state-hero .hero-inner .cta-pill, gm-rise ... both)
   sits in the animation origin and its forwards fill pins transform:none for the
   life of the screen, so the rule above computes but never paints there. The
   independent `scale` property composes with the animation instead of losing to
   it — same 0.97, same curve, same 90/200 timing. */
html.state-hero .hero-inner .cta-pill:active{scale:0.97;transition-duration:var(--t-press)}

.eyebrow{
  display:inline-flex;align-items:center;gap:.5rem;
  font-size:11px;letter-spacing:0.22em;color:var(--dim);
}
.eyebrow-dot{width:6px;height:6px;border-radius:50%;background:var(--accent);box-shadow:0 0 8px rgba(255,77,0,0.7)}

/* word-reveal primitive */
.w{display:inline-block;transform:translateY(0.42em);opacity:0;will-change:transform,opacity}
.lead{color:var(--accent)}
[data-reveal]{opacity:0;transform:translateY(0.4em);will-change:opacity,transform}

/* ---- ENTER ---- */
/* chrome appears from hero onward — keep the enter frame clean (no duplicate wordmark) */
html.state-enter #chrome{display:none}
/* ITEM 4 — Enter Overture: lockup lifts to ~30% vh; the "Step inside" pill drops to
   the visual threshold just above the recomposed floor-plate front edge. Pill is
   fixed to the viewport (not centred in the flow) so the diorama grounds beneath it.
   No DOM change → no CLS on this LCP screen. */
/* A2 (2026-08-10) — the column was min(92vw,48ch). `ch` resolves against the
   loaded body face, so when Satoshi arrived the column jumped 414px -> 524px and
   every centred line inside it re-laid out: measured 0.0029, the largest single
   shift on the site. 33rem is that same settled width with the face loaded, but
   it no longer depends on which font is resolved. Measure is unchanged in the
   shipped state; only the pre-swap frame differs, and that frame sits behind the
   opaque preloader. */
.enter-inner{left:50%;top:35%;transform:translate(-50%,-50%);text-align:center;width:min(92vw,33rem)}
.enter-mark{display:flex;flex-direction:column;gap:.4rem;align-items:center;margin-bottom:1.4rem}
.enter-word{font-family:var(--display);font-weight:700;font-size:clamp(2.4rem,7vw,4.4rem);letter-spacing:0.01em;line-height:1}
.enter-line{font-size:11px;letter-spacing:0.4em;color:var(--dim)}
.enter-sub{font-size:1.05rem;color:var(--dim);margin-bottom:0;line-height:1.5}
/* 2026-07-27 — the landing frame offered a wordmark, a seven-word tagline and a
   disabled button: a visitor could leave knowing the firm's name and nothing about
   what it sells or how to begin. This states the offer in frame one. Every clause
   is lifted from copy already shipped elsewhere on the site (hero-sub, start-gate,
   promise 01) — no new claim is made here. */
.enter-offer{margin-top:.95rem;font-size:.98rem;line-height:1.55;color:var(--faint)}
/* anchored to the enter .screen (inset:0) — NOT inside .enter-inner, whose transform
   would otherwise capture it as the containing block. pointer-events restored since
   it now sits under .screen (pointer-events:none) rather than .screen-inner. */
.enter-cta{
  position:absolute;left:50%;top:77%;transform:translate(-50%,-50%);pointer-events:auto;
  /* .pill's list, plus the opacity tier the ready state needs */
  transition:border-color var(--t-quick) var(--ease),background var(--t-quick) var(--ease),
             transform var(--t-quick) var(--ease),scale var(--t-quick) var(--ease),
             opacity var(--t-state) var(--ease);
}
/* Batch 2 (press) — transform-positioned: the press restates the FULL composed
   transform, or the 0.97 from .cta-pill:active would drop the centring offset and
   throw the button half its own size down and right. Equal specificity to
   .cta-pill:active, later in source, so it wins. */
.enter-cta:active{transform:translate(-50%,-50%) scale(0.97);transition-duration:var(--t-press)}
/* a conversion path that exists in frame one, for the visitor who will not walk
   47 viewport-heights before being allowed to ask a question */
.enter-alt{
  position:absolute;left:50%;top:77%;transform:translate(-50%,calc(-50% + 3.6rem));
  pointer-events:auto;font-size:12px;letter-spacing:0.03em;color:var(--faint);
  /* Batch 7 (2026-08-11) — LINK IDIOM CONVERGENCE. The inline text links all speak
     .outro-email's sweep now: the accent is DRAWN across the line (Plan's grammar,
     §4.2) instead of the whole rule swapping colour at once.
     The resting line is the second gradient layer, not a border: two layers at the
     same `0 100%` position means the sweep draws directly OVER the resting rule
     rather than beside it, and the control still reads as a link at rest in its
     original 0.22 bone. The border's 1px of box height moves into padding-bottom
     (2px -> 3px), so the underline lands on the same pixel row and the box is
     byte-identical — zero CLS. The ::after hit area and the text-shadow stack are
     untouched. */
  transition:color var(--t-quick) var(--ease);
}
/* A3 (2026-08-13, M2) — THE DRAWN RULE MOVES INSIDE.
   Both gradient layers used to be painted by the ANCHOR, which is a block: one
   background box, so one underline, drawn under the block's bottom edge. When the
   line wrapped at 320 and 390 the rule appeared under the second line only and the
   first line was left bare. Painted by the inner INLINE span with
   box-decoration-break:clone, each line fragment gets its own background box and
   therefore its own rule — and the sweep still animates as one gesture because
   both fragments share the animated background-size. */
.enter-alt .ea-line{
  -webkit-box-decoration-break:clone; box-decoration-break:clone;
  background-image:linear-gradient(var(--accent),var(--accent)),linear-gradient(rgba(234,231,222,0.22),rgba(234,231,222,0.22));
  background-repeat:no-repeat;background-position:0 100%,0 100%;background-size:0% 1px,100% 1px;
  padding-bottom:3px;
  transition:background-size var(--t-state) var(--ease);
}
/* A10 (2026-08-10) · re-cut 2026-08-14 (M2) — the pseudo hit area, not padding,
   because padding would push the underline off the text baseline. The vertical
   inset now carries the box to ≥44px on its own (the 17px line box + 2×14) rather
   than relying on the neighbouring gap. */
.enter-alt::after{content:"";position:absolute;inset:-14px -10px}
/* A11 2026-08-10 · Batch 2 2026-08-10 — TEXT IDIOM: no compression. Compression is
   for objects; a line of text has no body to squash. The press is the tint
   arriving at --t-press instead of --t-quick. Same for .outro-email and
   .menu-info. */
@media (hover:hover){ .enter-alt:hover{color:var(--ink)} .enter-alt:hover .ea-line{background-size:100% 1px,100% 1px} }
.enter-alt:active{color:var(--ink);transition-duration:var(--t-press)}
.enter-alt:active .ea-line{background-size:100% 1px,100% 1px;transition-duration:var(--t-press)}

/* ------------------------------------------------------------
   BATCH 5 · THE BOOT IGNITION (§4.4) — Enable's power-on grammar
   The handoff used to cross-fade two GRIMSDALE wordmarks over each other. It is
   now a MATCH CUT: html.pre-hold holds every line of the enter frame at opacity
   0 (opacity only — this is the LCP screen and nothing may move), the preloader
   exits at --t-quick, and only then does app.js swap pre-hold for enter-in and
   the frame assembles: lockup, line, sub/offer, CTA, each --t-enter on --ease,
   one --stagger apart. The dot ignites after all of it, on readiness (JS).
   Fail-safe by construction: html.pre-hold is added by app.js, so if the script
   never runs the enter frame is simply visible, and the animation's `backwards`
   fill — not a base opacity:0 — is what holds each line through its delay.
   The rack-focus (§5) rides the same keyframe: blur(5px) -> 0 on display type at
   an arrival, keyframe-only, never per-frame.
   The keyframe deliberately has NO `to`: the implicit final keyframe is the
   element's own computed style, so the CTA arrives at whatever opacity its state
   says (.5 dimmed-waiting, 1 once ready) instead of animating to a hardcoded 1
   and snapping back.
   The settle rides the independent `translate` property, so .enter-cta and
   .enter-alt keep their centring transforms untouched.
   ------------------------------------------------------------ */
@keyframes gm-enter-rise{from{opacity:0;translate:0 0.4em;filter:blur(5px)}}
html.pre-hold .enter-word,
html.pre-hold .enter-line,
html.pre-hold .enter-sub,
html.pre-hold .enter-offer,
html.pre-hold .enter-cta,
html.pre-hold .enter-alt{opacity:0}
html.enter-in .enter-word{animation:gm-enter-rise var(--t-enter) var(--ease) backwards}
html.enter-in .enter-line{animation:gm-enter-rise var(--t-enter) var(--ease) var(--stagger) backwards}
html.enter-in .enter-sub,
html.enter-in .enter-offer{animation:gm-enter-rise var(--t-enter) var(--ease) calc(var(--stagger) * 2) backwards}
html.enter-in .enter-cta,
html.enter-in .enter-alt{animation:gm-enter-rise var(--t-enter) var(--ease) calc(var(--stagger) * 3) backwards}

/* ---- HERO ---- */
.hero-inner{left:var(--gutter);top:50%;transform:translateY(-50%);max-width:42ch}
.hero-head{
  font-family:var(--display);font-weight:700;
  font-size:clamp(2.6rem,6.4vw,5.2rem);line-height:1.02;letter-spacing:-0.015em;
  margin-bottom:1.3rem;
}
.hero-sub{font-size:1.08rem;line-height:1.55;color:var(--dim);max-width:40ch;margin-bottom:1.8rem}
.hero-inner .cta-pill{opacity:0;transform:translateY(0.4em)}
/* ITEM 2B — spacing guarantee (CD-approved v2): the stacked headline SCALES with
   viewport height rather than being pushed down, so the whole hero (headline+sub+CTA)
   stays on-screen while "Give" still clears the brand. Keeps the 1.02 line-height ratio
   and the one-word-per-line stack; at heights >= ~1000px it pins to the 82px ceiling
   (pixel-identical to the original design). Desktop only — mobile keeps the width-based
   size. The padding-top clamp remains only as a capped fine-trim (never clips the CTA). */
@media (min-width:641px){
  .hero-head{font-size:clamp(56px, calc((100vh - 340px) / 7), 82px)}
  /* preserve the one-word-per-line stack (7 lines) at every scaled size — the 42ch
     column is tied to the 16px body em so it no longer forces the wrap once the
     headline shrinks; block each word instead. Identical layout at the 82px ceiling. */
  .hero-head .w{display:block}
  /* fine-trim only: 0 at the target viewports (the scale already clears the brand);
     engages below ~700px height and is capped at 24px so it can never clip the CTA. */
  .hero-inner{padding-top:clamp(0px, calc(700px - 100vh), 24px)}
}

/* ------------------------------------------------------------
   HERO — COPY PRESENT ON ARRIVAL (2026-07-27)
   The hero copy used to be scroll-gated: headline finished revealing at p≈0.34,
   subhead at p≈0.44, CTA not solid until p≈0.56 — roughly 2,000px of scrolling on
   a 500vh spacer. So the frame a visitor landed on immediately after the one act
   of commitment the site asked for ("Step inside") was empty: a dim diorama and a
   scroll cue. The reveal now fires ONCE on arrival and the scrub animates the
   scene, not the sell. Word index rides --wi, set in app.js on boot.
   Animations sit in the animation origin, so they win over any stale inline
   opacity/transform left by an earlier scroll-driven reveal.
   ------------------------------------------------------------ */
/* THE FOCUS PULL (§5) — the camera racks onto the copy as it arrives:
   blur(5px) -> 0 riding the existing rise. Depth-of-field is already this site's
   depth language, so the arrival borrows it rather than adding a new idea.
   KEYFRAME ARRIVALS ONLY. Conducted arrivals stay scrub-driven and get no blur:
   a per-frame filter write is a paint cost and a contract violation. Excluded
   under reduced motion (both homes — the whole animation is switched off). */
@keyframes gm-word-in{from{opacity:0;transform:translateY(0.42em);filter:blur(5px)}to{opacity:1;transform:none;filter:blur(0)}}
@keyframes gm-rise{from{opacity:0;transform:translateY(0.4em);filter:blur(5px)}to{opacity:1;transform:none;filter:blur(0)}}
html.state-hero .hero-head .w{
  animation:gm-word-in var(--t-enter) var(--ease) both;
  animation-delay:calc(260ms + var(--wi,0) * var(--stagger));   /* 260ms clears the transition veil */
}
html.state-hero .hero-sub{animation:gm-rise var(--t-enter) var(--ease) 640ms both}
html.state-hero .hero-inner .cta-pill{animation:gm-rise var(--t-enter) var(--ease) 780ms both}
html.reduced .hero-head .w,
html.reduced .hero-sub,
html.reduced .hero-inner .cta-pill{animation:none}

.cue{
  position:absolute;left:50%;bottom:2rem;transform:translateX(-50%);
  font-size:11px;letter-spacing:0.34em;color:var(--faint);white-space:nowrap;pointer-events:none;
}

/* ---- PROBLEM ---- */
.problem-inner{left:var(--gutter);top:50%;transform:translateY(-46%);max-width:46ch}
.problem-inner .eyebrow{margin-bottom:1.1rem}
.problem-head{
  font-family:var(--display);font-weight:700;
  font-size:clamp(2rem,4.6vw,3.7rem);line-height:1.06;letter-spacing:-0.01em;
  margin-bottom:1.3rem;
}
.problem-head .line{display:block}
.problem-body{font-size:1.02rem;line-height:1.6;color:var(--dim);max-width:44ch;margin-bottom:1.3rem}
.problem-note{max-width:42ch;font-size:.95rem;line-height:1.5}
.note-lead{display:block;color:var(--ink)}
.note-illus{display:block;color:var(--dim);font-size:.82rem;margin-top:.3rem} /* F2: informational, not decorative — --faint failed AA over the pale partition */

/* ---- START GATE ---- */
.startgate-inner{left:var(--gutter);top:50%;transform:translateY(-54%);max-width:44ch}
.startgate-head{
  font-family:var(--display);font-weight:700;
  font-size:clamp(2.4rem,5.6vw,4.8rem);line-height:1.0;letter-spacing:-0.015em;
  margin-bottom:1.2rem;
}
.startgate-head .line{display:block}
.startgate-sub{font-size:1.08rem;line-height:1.5;color:var(--dim);max-width:34ch}

/* ============================================================
   OPTIONAL PARTICIPATION (2026-07-27)
   The pitch is "we come to you and measure YOUR real week" — and the site never
   asked the visitor for anything, never let them touch a desk, never reflected
   them back. It was a beautiful thing to be watched, made by a firm whose
   proposition is participation.
   This is the smallest honest correction: one optional question, on the least
   dense screen, using the exact task vocabulary the Discover hotspots already
   carry. Answering it (a) marks the matching hotspot chip in the Discover
   diorama, (b) reads the answer back in the visitor's own terms, and (c) rides
   into the outro mailto so the first conversation starts from their problem.
   Ignoring it costs nothing: the gate is right there and the walk is identical.
   ============================================================ */
/* The picker adds ~230px to the start-gate column, which pushed the headline up
   into the brand lockup at 800px viewport height. Same treatment as the outro:
   the headline scales with available height so the whole column stays clear of
   the chrome. Measured after: column top 100 vs brand bottom 62 at 1280×800. */
@media (min-width:641px){
  .startgate-head{font-size:clamp(1.9rem, calc((100vh - 250px) / 10.4), 4.8rem);margin-bottom:1rem}
}
.picker{margin-top:1.4rem;max-width:40ch}
.picker-label{
  display:block;font-family:var(--mono);font-size:10px;letter-spacing:0.2em;
  text-transform:uppercase;color:var(--faint);margin-bottom:.75rem;
}
.picker-chips{display:flex;flex-wrap:wrap;gap:.5rem}
.pick-chip{
  /* A10 (2026-08-10) — min-height was 40px, 4px under target */
  display:inline-flex;align-items:center;min-height:44px;padding:.42rem .9rem;cursor:pointer;
  font-family:var(--body);font-size:.92rem;color:var(--dim);
  background:rgba(14,16,19,0.55);border:1px solid var(--line);border-radius:999px;
  transition:border-color var(--t-quick) var(--ease),color var(--t-quick) var(--ease),background var(--t-quick) var(--ease),transform var(--t-quick) var(--ease);
}
/* A11 2026-08-10 — :active must stay ABOVE the [aria-pressed] rule: equal
   specificity, so the pressed state wins on source order alone. */
@media (hover:hover){ .pick-chip:hover{border-color:rgba(255,77,0,0.55);color:var(--ink)} }
.pick-chip:active{border-color:rgba(255,77,0,0.55);color:var(--ink);transform:scale(0.97);transition-duration:var(--t-press)}
.pick-chip[aria-pressed="true"]{border-color:var(--accent);color:var(--accent);background:rgba(255,77,0,0.10)}
/* Batch 3 (confirm) — the landing plus the weld's spark, at chip size: the
   inverse-size law says small things land hard and flare gently, so --spark-peak
   is the quietest on the site (10px @ 0.5) and rests transparent (the keyframe's
   default --spark-rest).
   The [aria-pressed="true"] half is what keeps DESELECTION silent — the selector
   simply stops matching, so nothing replays on unset.
   The .is-land half is the boot/replay gate, and it is JS-set on the user's own
   click only (app.js, applyPick). A pure-CSS trigger cannot work here: (a) the
   restored chip is already aria-pressed="true" when a boot-time html.booted class
   would be added, and adding the class starts the animation exactly as the click
   would; (b) every .screen is display:none while inactive, and returning to
   start-gate re-renders the chip, which restarts any animation the selector still
   matches. The class is removed on animationend so it can never be a resting
   state. */
.pick-chip.is-land[aria-pressed="true"]{
  --spark-peak:0 0 10px rgba(255,77,0,0.5);
  animation:gm-land var(--t-state) var(--ease-land),gm-spark var(--t-spark) var(--ease-decay);
}
.picker-readback{
  display:block;margin-top:.85rem;font-size:.95rem;line-height:1.5;color:var(--dim);
  opacity:0;transform:translateY(0.3em);
  transition:opacity var(--t-state) var(--ease),transform var(--t-state) var(--ease);
}
.picker-readback.on{opacity:1;transform:none}
.picker-readback b{color:var(--ink);font-weight:500}
/* the picked task's hotspot chip holds accent in the Discover diorama instead of
   cyan, so the visitor's own answer is the one marker that stays lit */
.hlabel.is-pick{
  color:var(--accent);border-color:rgba(255,77,0,0.55);
  text-shadow:0 0 8px rgba(255,77,0,0.45),0 1px 3px rgba(8,9,11,0.95);
}
.hlabel.is-pick::before{background:var(--accent);box-shadow:0 0 7px rgba(255,77,0,0.85)}

/* reusable progress-ring gate */
.gate{position:absolute;left:50%;bottom:2.4rem;transform:translateX(-50%);pointer-events:auto}
/* Batch 2 (press) — the gate's FIRST hover/press states. .gate itself is
   scrub-positioned (app.js writes translateX(-50%) translateY(..em) inline every
   frame), so the compression rides .gate-pill, its untransformed child: a CSS
   transform here can never be shadowed by an inline one, and no scrub value is
   transitioned. */
.gate-pill{
  display:inline-flex;align-items:center;gap:.7rem;
  border:1px solid var(--line);border-radius:999px;background:rgba(14,16,19,0.6);
  padding:.4rem 1.1rem .4rem .4rem;cursor:pointer;color:var(--ink);
  transition:border-color var(--t-quick) var(--ease),transform var(--t-quick) var(--ease);
}
@media (hover:hover){ .gate-pill:hover{border-color:rgba(255,77,0,0.55)} }
.gate-pill:active{border-color:rgba(255,77,0,0.55);transform:scale(0.97);transition-duration:var(--t-press)}
.gate-ring{position:relative;width:44px;height:44px;display:inline-flex;align-items:center;justify-content:center}
.gate-ring svg{position:absolute;inset:0;transform:rotate(-90deg)}
.gate-ring-track{fill:none;stroke:var(--line);stroke-width:2}
.gate-ring-fill{
  fill:none;stroke:var(--accent);stroke-width:2.5;stroke-linecap:round;
  stroke-dasharray:119.38;
  stroke-dashoffset:calc(119.38 * (1 - var(--gate-fill)));
}
.gate-dot{width:7px;height:7px;border-radius:50%;background:var(--accent);opacity:.5;transition:opacity var(--t-quick) var(--ease),transform var(--t-quick) var(--ease)}
.gate-label{font-size:11px;letter-spacing:0.14em;color:var(--dim)}
/* activated state — THE FOCAL SEQUENCE (§4.5, "the weld that opens the door").
   t=0    completion (click, or scroll-out at p>=0.99): the ring DRAWS closed over
          --t-draw. The transition is scoped to .is-full and exists nowhere else,
          so the per-frame --gate-fill scrub is never transitioned and never
          smears — the scrub path is byte-identical to before.
   t=520  draw end: the dot IGNITES — scale(1.5) on --ease-land at --t-state,
          delayed by the draw, while gm-spark flares to 22px and cools to the
          dot's persistent 12px lit glow. The static box-shadow below equals
          --spark-rest, so the frame the animation releases on is seamless.
          The label lifts to --ink on the same beat.
   t=653  --t-draw + --t-spark*0.35 — fireGate hands off to the veil (app.js).
   The dot's opacity is deliberately NOT delayed: it stays on the base tier, so
   the dot brightens with the draw and only its scale/bloom wait for the weld. */
.gate.is-full .gate-ring-fill{stroke-dashoffset:0;transition:stroke-dashoffset var(--t-draw) var(--ease)}
.gate.is-full .gate-dot{
  opacity:1;transform:scale(1.5);box-shadow:0 0 12px rgba(255,77,0,0.9);
  --spark-peak:0 0 22px rgba(255,77,0,1);
  --spark-rest:0 0 12px rgba(255,77,0,0.9);
  transition:opacity var(--t-quick) var(--ease),transform var(--t-state) var(--ease-land) var(--t-draw);
  /* no fill-mode: before the delay expires the static 12px lit glow reads as the
     weld arming through the draw; the 22px flare lands only at draw end. A
     `both` fill put the full flare on screen from t=0 and killed the ignition. */
  animation:gm-spark var(--t-spark) var(--ease-decay) var(--t-draw);
}
.gate.is-full .gate-label{color:var(--ink);transition:color var(--t-quick) var(--ease) var(--t-draw)}

/* idle invitations (M4) — cue float + gate ring breathe; both off under reduced motion */
/* THE CUE FLOAT MOVED TO THE FRAME LOOP (2026-08-14, perf).
   It used to be `@keyframes gm-cue-float` + `.cue{animation:… 3.4s infinite}`.
   A CSS keyframe animation on a compositable property runs on the COMPOSITOR
   thread, so it makes the compositor draw a real frame every vsync — including
   every vsync while app.js's paint loop is streaming WebGL commands into the
   full-viewport canvas underneath it. Each of those draws has to resolve the
   canvas layer mid-stream, and the synchronisation lands on the frame budget:
   traced at 33 DrawFrames injected into a 150-frame benchPaint sweep, +4.6ms/
   frame at 1x and ~+16ms at 4x. The cost is per-drawn-frame, NOT per pixel — a
   4x4px dummy div animating over the canvas cost the same, and the same
   animation moved off-screen (or written on a non-compositable property) cost
   nothing at all. No amount of `contain`/`isolation`/`will-change` helps: a
   static promoted layer over the canvas is already free; it is the drawing that
   is not.
   So the float is now written by the ONE rAF writer in app.js (cueFloat()) on
   the `translate` property — same 6px amplitude, same 3.4s period, raised-cosine
   in place of the --ease-breathe bezier (max deviation 0.18px, and the keyframe
   comment below already described the intent as sinusoidal). Single writer, one
   commit per frame the loop already owns, and nothing ticking behind a busy
   main thread. `will-change` is set by that writer while it runs so the write
   stays a compositor-only property change.
   Reduced motion: app.js never calls it, and the static rules at the
   reduced-motion block below still pin the cue visible (A2). */
.cue{will-change:auto}
/* Ambient loops run --ease-breathe (sinusoidal, velocity-continuous at the wrap)
   and are retimed toward the scene's 4.5-5.2s ambient band: idle stays calmer
   than any feedback. */
@keyframes gm-breathe{0%,100%{opacity:.72}50%{opacity:1}}
.gate:not(.is-full) .gate-ring{animation:gm-breathe 2.6s var(--ease-breathe) infinite}

/* Defined here, wired to surfaces in Group B — no element references either
   keyframe yet, so both are inert this group.
   gm-land: the confirmation landing (scene's landBump at type scale, --ease-land).
   gm-spark: the weld flash (scene.js:2747's inverse-size emissive flash), one
   parameterised keyframe; each surface supplies its own --spark-peak / --spark-rest. */
@keyframes gm-land{from{transform:scale(0.97)}to{transform:scale(1)}}
@keyframes gm-spark{from{box-shadow:var(--spark-peak)}to{box-shadow:var(--spark-rest,0 0 0 rgba(255,77,0,0))}}

/* ---- DISCOVER stub ---- */
.stub-inner{right:var(--gutter);bottom:clamp(2rem,7vh,4.5rem);text-align:right;max-width:40ch}
.stub-inner .eyebrow{margin-bottom:1rem}
.stub-head{font-family:var(--display);font-weight:700;font-size:clamp(3rem,8vw,6.5rem);line-height:0.94;letter-spacing:-0.02em}
.stub-note{font-size:10px;letter-spacing:0.2em;color:var(--faint);margin-top:1rem}

/* ============ DISCOVER STAGE-WORLD (M2) ============ */
/* projected scene markers */
.scene-markers{position:fixed;inset:0;z-index:4;pointer-events:none}
.zlabel,.hlabel{
  position:absolute;top:0;left:0;white-space:nowrap;opacity:0;
  font-family:var(--mono);font-weight:500;text-transform:uppercase;
  color:var(--cyan,#4be1e3);transform:translate(-50%,-50%);will-change:transform,opacity;
}
:root{--cyan:#4be1e3}
.zlabel{
  font-size:11px;letter-spacing:0.24em;
  background:rgba(6,8,11,0.66);border:1px solid rgba(75,225,227,0.35);border-radius:4px;
  padding:3px 7px;text-shadow:0 0 10px rgba(75,225,227,0.55);
}
.zlabel::before{
  content:"";position:absolute;left:-6px;top:50%;width:5px;height:5px;margin-top:-2.5px;
  border-radius:50%;background:var(--cyan);box-shadow:0 0 8px rgba(75,225,227,0.8);
}
/* hotspot labels — same chip treatment as zones, at ~85% scale (zones > hotspots) */
.hlabel{
  font-size:10px;letter-spacing:0.2em;
  background:rgba(6,8,11,0.66);border:1px solid rgba(75,225,227,0.35);border-radius:4px;
  padding:2px 6px;text-shadow:0 0 8px rgba(75,225,227,0.55);
}
.hlabel::before{
  content:"";position:absolute;left:-5px;top:50%;width:4px;height:4px;margin-top:-2px;
  border-radius:50%;background:var(--cyan);box-shadow:0 0 7px rgba(75,225,227,0.8);
}
/* board pins */
.pin{position:absolute;top:0;left:0;opacity:0;transform:translate(-50%,-100%);will-change:transform,opacity}
.pin-line{position:absolute;left:50%;bottom:0;width:1px;height:60px;background:linear-gradient(to bottom,transparent,var(--cyan));transform:translateX(-50%)}
.pin-dot{position:absolute;left:50%;bottom:-4px;width:8px;height:8px;border-radius:50%;background:var(--cyan);box-shadow:0 0 10px rgba(75,225,227,0.9);transform:translateX(-50%)}

/* title card — default anchor is bottom-right (Discover); the other three stages
   re-anchor it (see PER-STAGE COMPOSITION below).
   FIX 2026-07-27 — the old `max-width:40ch` resolved against the inherited 1rem
   body size, not the ~102px display size on .stage-name, so the box computed to
   345px while DISCOVER measured 571px and IMPLEMENT 664px: three of four stage
   names overflowed past the viewport's right edge and were clipped mid-word.
   Sized in rem/vw now, with the display clamp capped so the widest word
   (IMPLEMENT ≈ 6.48 × font-size) fits the box at every width above the mobile
   breakpoint. Assert scrollWidth <= clientWidth on all four when touching these. */
.stage-title{right:var(--gutter);bottom:clamp(2rem,7vh,4.5rem);text-align:right;max-width:min(46rem,52vw);opacity:0}
.stage-title .eyebrow{
  border:1px solid var(--line);border-radius:999px;padding:.34rem .72rem;margin-bottom:1rem;color:var(--ink);
}
.stage-name{font-family:var(--display);font-weight:700;font-size:clamp(3rem,7vw,5.8rem);line-height:0.94;letter-spacing:-0.02em}
.stage-purpose{font-size:1.02rem;line-height:1.5;color:var(--dim);margin-top:1rem;margin-left:auto;max-width:34ch}
/* M1 — COPY POCKET for the stage title.
   The 07-18 F5 run gave .step a tight feathered pocket on top of the broad
   Item-2A scrim; the stage title only ever got the broad scrim, which is
   deliberately soft and has fallen off by the time it reaches display-size
   glyphs. That is a live readability defect, worst at implement p≈0.55 where
   the construction front's picket curtain runs straight under the title.
   Same pocket family as .step::before — identical stops and feathering, no
   panel, no visible edge — carried on ::after so the Item-2A scrim on ::before
   stays exactly as shipped, plus the .promise-num dark backing on the eyebrow
   pill and the purpose line. Pure darkening: invisible over the dark room. */
.stage-title::after{
  content:"";position:absolute;z-index:-1;pointer-events:none;
  inset:-1.5rem -2.6rem -1.9rem;
  background:radial-gradient(ellipse 78% 78% at 50% 46%,
    rgba(8,9,11,0.66) 0%,
    rgba(8,9,11,0.66) 38%,
    rgba(8,9,11,0.58) 54%,
    rgba(8,9,11,0.44) 68%,
    rgba(8,9,11,0.28) 79%,
    rgba(8,9,11,0.14) 88%,
    rgba(8,9,11,0.05) 95%,
    rgba(8,9,11,0.00) 100%);
}
.stage-title .eyebrow{text-shadow:0 1px 3px rgba(8,9,11,0.95),0 0 12px rgba(8,9,11,0.9)}
.stage-name{text-shadow:0 1px 3px rgba(8,9,11,0.72),0 0 18px rgba(8,9,11,0.55)}
.stage-purpose{text-shadow:0 1px 3px rgba(8,9,11,0.9),0 0 10px rgba(8,9,11,0.8)}

/* steps, left */
.steps{position:absolute;left:var(--gutter);top:50%;transform:translateY(-50%);width:min(38ch,40vw);min-height:9rem;pointer-events:none}
.step{position:absolute;top:0;left:0;width:100%;opacity:0;transform:translateY(0.4em);will-change:opacity,transform}
/* F5 (Part B) — copy pocket over the construction wave: a tight feathered halo
   behind each step's copy block. It is a pseudo of .step, so it rides the step's
   reveal opacity (enters/exits with the text), and being pure darkening it is
   invisible over the dark room — it only shows where the scene gets loud
   (measured L up to 0.99 behind the Implement front). The wave's pickets stay
   bright outside the glyph zone; no panel, no visible edge, no JS. */
.step::before{
  content:"";position:absolute;z-index:-1;pointer-events:none;
  inset:-1.4rem -2.4rem -1.8rem;
  background:radial-gradient(ellipse 78% 78% at 50% 46%,
    rgba(8,9,11,0.66) 0%,
    rgba(8,9,11,0.66) 38%,
    rgba(8,9,11,0.58) 54%,
    rgba(8,9,11,0.44) 68%,
    rgba(8,9,11,0.28) 79%,
    rgba(8,9,11,0.14) 88%,
    rgba(8,9,11,0.05) 95%,
    rgba(8,9,11,0.00) 100%);
}
/* dark readability backing ahead of the orange glow (same pattern as .promise-num):
   orange is mid-luminance, so over the bright wave it needs near-carbon behind it */
.step-eyebrow{display:block;font-size:11px;letter-spacing:0.22em;color:var(--accent);text-shadow:0 1px 3px rgba(8,9,11,0.95),0 0 12px rgba(8,9,11,0.9),0 0 8px rgba(255,77,0,0.45);margin-bottom:.7rem}
.step-lead{display:block;font-family:var(--display);font-weight:600;font-size:clamp(1.5rem,2.8vw,2.2rem);line-height:1.05;margin-bottom:.5rem}
.step-line{display:block;font-size:1rem;line-height:1.5;color:var(--dim);max-width:34ch}

/* right-edge vertical step indicator */
.step-dots{position:absolute;right:calc(var(--gutter) * 0.5);top:50%;transform:translateY(-50%);display:flex;flex-direction:column;gap:.7rem}
.step-dots i{width:7px;height:7px;border-radius:50%;background:var(--line);transition:background var(--t-quick) var(--ease),box-shadow var(--t-quick) var(--ease)}
.step-dots i.on{background:var(--accent);box-shadow:0 0 8px rgba(255,77,0,0.7)}

/* boards, right half */
.boards{position:absolute;right:var(--gutter);top:clamp(6rem,20vh,10rem);width:min(340px,42vw);min-height:11rem;pointer-events:none}
.board{
  position:absolute;top:0;left:0;width:100%;opacity:0;transform:translateY(12px);will-change:opacity,transform;
  background:rgba(14,16,19,0.72);border:1px solid rgba(234,231,222,0.14);
  -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
  border-radius:16px;padding:20px;
}
/* per-stage board motif (abstract, no text — NRG boards carry art; ours carry a mark) */
.board::before{content:"";display:block;width:22px;height:22px;margin-bottom:10px;opacity:.85;background:var(--bmotif) no-repeat center/contain}
[data-screen="discover"] .board{--bmotif:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='rgba(75,225,227,0.8)' stroke-width='1.5'%3E%3Ccircle cx='8.5' cy='8.5' r='5.5'/%3E%3Cpath d='M13 13 L20 20'/%3E%3C/svg%3E")}
[data-screen="plan"] .board{--bmotif:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='rgba(75,225,227,0.8)' stroke-width='1.5'%3E%3Crect x='3.5' y='3.5' width='11' height='11'/%3E%3Crect x='9.5' y='9.5' width='11' height='11'/%3E%3C/svg%3E")}
[data-screen="implement"] .board{--bmotif:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='rgba(75,225,227,0.8)' stroke-width='1.5'%3E%3Cpath d='M3 12 H13 M10 8 L14 12 L10 16'/%3E%3Cpath d='M18.5 5 V19'/%3E%3C/svg%3E")}
[data-screen="enable"] .board{--bmotif:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='rgba(75,225,227,0.8)' stroke-width='1.5'%3E%3Cpath d='M5.5 19 V12 M12 19 V6 M18.5 19 V15'/%3E%3C/svg%3E")}
.board h3{font-family:var(--display);font-weight:600;font-size:1.15rem;color:var(--ink);margin-bottom:.5rem;line-height:1.1}
.board p{font-size:0.95rem;line-height:1.5;color:var(--dim)}

/* ============================================================
   PER-STAGE COMPOSITION (2026-07-27)
   The scene evolved through four states while the interface held still: title
   bottom-right, steps left, boards right, gate centre — identical on all four
   stages, down to the same board centres and step windows in makeStage(). By
   stage three the visitor could predict every transition. Each stage now owns
   its own anchor set, and the walk crosses the frame instead of replaying it:

     Discover   title BR · steps L · boards R-top       (the surveyor reads the room)
     Plan       title TR · steps L · boards R-bottom    (findings settle to the drafting table)
     Implement  title BL · steps R · boards L-top       (full mirror — the build comes from the other side)
     Enable     title TL · steps R · boards L-mid       (+ boards ACCUMULATE, see below)

   Timing differs too — see STAGE_LAYOUT in app.js, which owns the per-stage
   board centres and step windows. Desktop only: below 640px the stage screens
   use the single flow column in the RESPONSIVE block, which must win.
   ============================================================ */
@media (min-width:641px){
  /* --- title anchors --- */
  [data-screen="plan"] .stage-title{top:clamp(5.5rem,13vh,8rem);bottom:auto}
  [data-screen="implement"] .stage-title{left:var(--gutter);right:auto;text-align:left}
  [data-screen="enable"] .stage-title{left:var(--gutter);right:auto;text-align:left;top:clamp(5.5rem,13vh,8rem);bottom:auto}
  [data-screen="implement"] .stage-purpose,
  [data-screen="enable"] .stage-purpose{margin-left:0;margin-right:auto}

  /* --- mirrored axis: steps move right, boards move left.
         The step-dots rail stays on the right edge on every stage (it reads as a
         global progress rail, not a per-column ornament); mirrored steps sit
         inboard of it, so they never collide. --- */
  [data-screen="implement"] .steps,
  [data-screen="enable"] .steps{left:auto;right:var(--gutter)}
  [data-screen="implement"] .boards,
  [data-screen="enable"] .boards{right:auto;left:var(--gutter)}

  /* --- Plan: the shortlist settles low, under the raised title --- */
  [data-screen="plan"] .boards{top:auto;bottom:clamp(9rem,16vh,11rem)}

  /* --- Enable: boards ACCUMULATE into a settled column instead of crossfading.
         This is the one stage whose grammar differs, not just its arrangement,
         and it lands on the payoff: by the gate all four promises are held at
         once ("yours to keep") rather than replaced one by one. Compact card
         variant so four fit the column; JS stops hiding them (see STAGE_LAYOUT
         .accumulate). Reverts to the crossfade below 640px, where four cards
         cannot fit the mobile budget. --- */
  [data-screen="enable"] .boards{
    top:50%;bottom:auto;transform:translateY(-50%);
    display:flex;flex-direction:column;gap:.55rem;
    width:min(340px,34vw);min-height:0;
  }
  [data-screen="enable"] .board{position:relative;top:auto;left:auto;padding:14px 16px;border-radius:12px}
  [data-screen="enable"] .board::before{display:none}
  [data-screen="enable"] .board h3{font-size:1.02rem;margin-bottom:.28rem}
  [data-screen="enable"] .board p{font-size:.9rem;line-height:1.45}
}

/* ============ OUTRO / CONTACT (M3) ============ */
.outro-inner{left:var(--gutter);top:50%;transform:translateY(-50%);max-width:46ch}
.outro-head{
  font-family:var(--display);font-weight:700;
  font-size:clamp(2.4rem,5.6vw,4.8rem);line-height:1.0;letter-spacing:-0.015em;
  margin-bottom:1.6rem;
}
.outro-head .line{display:block}
/* PROMISES → NUMBERED LEDGER: mono index in --accent + promise text in --ink, 1px rules between rows */
.ledger{list-style:none;display:flex;flex-direction:column;margin-bottom:1.9rem;max-width:44ch}
.promise{
  display:grid;grid-template-columns:2.2rem 1fr;column-gap:1rem;align-items:baseline;
  padding:.95rem 0;font-size:1.08rem;line-height:1.5;color:var(--ink);
  opacity:0;transform:translateY(0.4em);will-change:opacity,transform;
}
.promise + .promise{border-top:1px solid var(--line)}
/* dark readability backing (not an orange glow) so the accent numbers hold over
   bright furniture where the radial copy-scrim falls off at the left column */
.promise-num{font-size:11px;letter-spacing:0.18em;color:var(--accent);text-shadow:0 1px 3px rgba(8,9,11,0.95),0 0 12px rgba(8,9,11,0.9)}
.promise-text{color:var(--ink)}
/* CTA PANEL — full column-width panel button.
   FIX 2026-07-27 — this was a 1px rgba(234,231,222,0.14) outline on translucent
   carbon with a 16px label, sitting directly above a 38px full-opacity email:
   the primary control was the quietest object in its own group, at the one frame
   that decides the sale. It is now filled --accent at rest and the sweep is
   inverted (carbon sweeps IN on hover, label goes accent). Same mechanism, run
   the other way round. Carbon-on-orange ≈ 8.9:1; accent-on-carbon ≈ 5.5:1. */
.cta-panel{
  position:relative;display:flex;align-items:center;justify-content:space-between;gap:1rem;
  width:100%;max-width:44ch;min-height:72px;padding:0 1.4rem;overflow:hidden;
  border:1px solid var(--accent);border-radius:16px;background:var(--accent);
  cursor:pointer;opacity:0;transform:translateY(0.4em);will-change:opacity,transform;
}
.cta-panel::before{
  content:"";position:absolute;inset:0;background:var(--carbon);
  transform:scaleX(0);transform-origin:left center;transition:transform var(--t-state) var(--ease);
}
.cta-panel-label,.cta-panel-arrow{position:relative;z-index:1;transition:color var(--t-state) var(--ease)}
.cta-panel-label{font-family:var(--body);font-weight:500;font-size:1.08rem;color:var(--carbon)}
.cta-panel-arrow{font-size:1.05rem;color:var(--carbon)}
@media (hover:hover){
  .cta-panel:hover::before{transform:scaleX(1)}
  .cta-panel:hover .cta-panel-label,.cta-panel:hover .cta-panel-arrow{color:var(--accent)}
}
/* Batch 2 (press) — 0.985, not the pill's 0.97: this panel is 44ch wide and a
   0.97 would displace its edges by ~5px, which reads as a jolt rather than as
   weight (ruling, 2026-08-10). The full-width panel is also the one control that
   cannot take a `transform`: .cta-panel IS .outro-cta, and app.js's revealEl
   writes translateY() into its inline style every frame of the outro scrub, which
   outranks any rule here. The independent `scale` property composes with that
   inline transform instead of fighting it, and transitioning `scale` leaves the
   scrub-written `transform` untransitioned — the pure-function-of-p contract is
   intact either way. */
.cta-panel{transition:scale var(--t-quick) var(--ease)}
.cta-panel:active{scale:0.985;transition-duration:var(--t-press)}
.cta-panel:active::before{transform:scaleX(1);transition-duration:var(--t-press)}
.cta-panel:active .cta-panel-label,.cta-panel:active .cta-panel-arrow{color:var(--accent);transition-duration:var(--t-press)}
/* the visitor's optional pick (start-gate picker) rides the CTA as a factual tag —
   it states what the mailto will carry, it does not make a claim */
.cta-tag{
  display:none;margin-top:.65rem;font-size:11px;letter-spacing:0.16em;color:var(--faint);
}
html.has-pick .cta-tag{display:block}
.cta-tag b{color:var(--dim);font-weight:500}
/* EMAIL → secondary now: the panel above is the primary instrument, so the raw
   address steps down from 38px display to a quieter line, and carries a
   copy-to-clipboard affordance (mailto: fails silently with no mail client). */
.outro-email{
  /* A10 (2026-08-10) — position:relative so the ::after hit area below has a
     containing block. The box measured 202×36; the pseudo takes it to 44 without
     padding, which would have dragged the underline off the baseline. */
  position:relative;
  display:inline-block;margin-top:1.2rem;
  font-family:var(--display);font-weight:600;font-size:clamp(1.15rem,1.9vw,1.6rem);
  line-height:1.15;color:var(--dim);
  background-image:linear-gradient(var(--accent),var(--accent));
  background-repeat:no-repeat;background-position:0 100%;background-size:0% 2px;
  transition:background-size var(--t-state) var(--ease),color var(--t-quick) var(--ease);
}
.outro-email::after{content:"";position:absolute;inset:-4px -6px}
/* A11 2026-08-10 · Batch 2 2026-08-10 — TEXT IDIOM: tint only, no compression. */
@media (hover:hover){ .outro-email:hover{background-size:100% 2px;color:var(--ink)} }
.outro-email:active{background-size:100% 2px;color:var(--ink);transition-duration:var(--t-press)}
.email-row{display:flex;align-items:center;gap:.9rem;flex-wrap:wrap}
.copy-email{
  /* A10/A15 (2026-08-10) — was min-height:34px at font-size:10px, the smallest
     type on the site and 10px under the touch target. 44px and 12px.
     min-width holds the pill steady across its three labels ("Copy address" ->
     "Copied" / "Copy failed"), so the email row cannot reflow mid-interaction. */
  display:inline-flex;align-items:center;justify-content:center;min-height:44px;min-width:8.5rem;
  margin-top:1.2rem;padding:.3rem .8rem;cursor:pointer;
  font-family:var(--mono);font-size:12px;letter-spacing:0.14em;text-transform:uppercase;
  color:var(--faint);background:none;border:1px solid var(--line);border-radius:999px;
  transition:color var(--t-quick) var(--ease),border-color var(--t-quick) var(--ease),transform var(--t-quick) var(--ease);
}
/* A11 2026-08-10 — :active must stay ABOVE .done/.fail: equal specificity, so the
   result states win on source order alone. */
@media (hover:hover){ .copy-email:hover{color:var(--ink);border-color:var(--accent)} }
.copy-email:active{color:var(--ink);border-color:var(--accent);transform:scale(0.97);transition-duration:var(--t-press)}
/* Batch 3 (confirm) — success lands and sparks (12px @ 0.7, between the chip and
   the gate dot on the inverse-size law), and the label decodes to "Copied".
   .done is only ever added by the click handler, so the animation is event-driven
   by construction. */
.copy-email.done{
  color:var(--accent);border-color:var(--accent);
  --spark-peak:0 0 12px rgba(255,77,0,0.7);
  animation:gm-land var(--t-state) var(--ease-land),gm-spark var(--t-spark) var(--ease-decay);
}
/* A12 (2026-08-10) — when both clipboard paths failed the label was silently
   replaced by the raw address in the same styling as the resting state: identical
   to the control having done nothing. The failure now says so and looks distinct.
   --dim on a dashed border rather than a red: nothing has broken on the visitor's
   side, and the address is right there above the button to copy by hand.
   Batch 3: failure is GRAVE — it arrives at --t-state rather than the feedback
   tier, and it never sparks and never lands. Its text swap is plain, not a
   decode: the decode is a survey readout settling on a value, and there is no
   value here. */
.copy-email.fail{color:var(--dim);border-color:var(--dim);border-style:dashed;transition-duration:var(--t-state)}
/* the label lives in its own span so the 2400ms revert can cross-fade it (a hard
   textContent swap under a still-lit button was the one ungoverned change in the
   sequence) and so decodeIn has a box to write glyph spans into. min-width on the
   button already pins the pill across all three labels, so nothing here can
   reflow the email row. */
/* width:100% pins the label box to the button's content box, so swapping
   "Copy address" (12 glyphs) for "Copied" (6) re-centres the text inside a box
   that never moves. Measured: the span was the file's only layout-shift source
   at 0.000023 before this line, 0 after. */
.copy-email .ce-label{display:block;width:100%;text-align:center;transition:opacity var(--t-quick) var(--ease)}
.copy-email .ce-label.fading{opacity:0}
.copy-email .ce-label.decoding{white-space:pre}
/* decodeIn's per-character box (§4.1). Equal-width cells are what make the decode
   width-stable: the noise glyphs are drawn from whatever font resolves them, and
   without a fixed cell a proportional fallback would jiggle the line. Mono
   surfaces only. */
.dec-ch{display:inline-block;width:1ch;text-align:center}
.dec-ch.is-noise{color:var(--faint)}
.outro-cta{opacity:0;transform:translateY(0.4em)}
/* F1b+F4: informational → --dim. 2026-07-27: dropped the uppercase mono — this is
   the risk-reversal that closes a founder-led sale, and all-caps at 11px destroys
   word-shape recognition at exactly the size that needs it. Sentence case, 12px. */
.outro-reassure{
  display:block;margin-top:.9rem;font-family:var(--body);font-size:12px;
  letter-spacing:0.01em;text-transform:none;color:var(--dim);
}

/* ------------------------------------------------------------
   OUTRO — HEIGHT-AWARE FIT (2026-07-27)
   .outro-inner measured 799px tall in an 800px viewport on a fixed, unscrollable
   screen: the headline touched y=0 (overprinting the brand lockup) and the
   reassurance line sat flush at the bottom edge, so on any viewport under ~800px
   — which is most 1366×768 and 1280×720 laptops after browser chrome — the
   risk-reversal was simply off-screen. The headline now scales with viewport
   height (same technique as .hero-head), and the ledger/panel/email tighten in
   two steps below 900px and 700px. Measured after: ~567px at 800h, ~493px at
   640h, both clearing the chrome. Re-measure .outro-inner height when editing.
   ------------------------------------------------------------ */
@media (min-width:641px){
  .outro-head{font-size:clamp(2.15rem, calc((100vh - 300px) / 9.4), 4.8rem)}
}
@media (min-width:641px) and (max-height:900px){
  .outro-head{margin-bottom:1.1rem}
  .promise{padding:.72rem 0;font-size:1.02rem}
  .ledger{margin-bottom:1.35rem}
  .cta-panel{min-height:64px}
  .outro-email,.copy-email{margin-top:1rem}
  .outro-reassure{margin-top:.7rem}
}
@media (min-width:641px) and (max-height:700px){
  .promise{padding:.5rem 0;font-size:.98rem}
  .ledger{margin-bottom:1rem}
  .cta-panel{min-height:58px}
  .outro-email{font-size:clamp(1.05rem,1.6vw,1.35rem)}
  .outro-email,.copy-email{margin-top:.8rem}
}

/* ============ MENU OVERLAY (M3) ============ */
/* Batch 4 — the asymmetry rule, one grammar for both overlays: the backdrop
   arrives at --t-state and leaves at --t-quick. The base rule carries the EXIT
   duration and .is-open carries the entrance, because a transition runs on the
   after-change style. */
/* M1 2026-08-12 — THE BACKDROP IS NOW TRANSLUCENT. rgba(8,9,11,0.97) hid the
   room completely, and the blur(3px) sat on top of an opaque plate doing nothing
   measurable (a full-viewport backdrop-filter pass for a fiction). The room is
   held in depth by the canvas filter instead (html.menu-open #scene below), so
   the backdrop's only remaining jobs are the depth step and legibility.
   LEGIBILITY SCRIM: the ledger now sits over a live, lit room, so the left third
   carries its own gradient — measured, not assumed (see the AA sweep in the M1
   verification). Colour layer + gradient layer composite: 1-(1-.55)(1-.85) =
   0.93 at the left edge, falling to the flat 0.55 by 75%. */
.menu{
  position:fixed;inset:0;z-index:50;
  background-color:rgba(8,9,11,0.55);
  background-image:linear-gradient(90deg,
    rgba(8,9,11,0.85) 0%, rgba(8,9,11,0.35) 55%, rgba(8,9,11,0) 75%);
  display:flex;align-items:stretch;justify-content:flex-start;
  opacity:0;pointer-events:none;transition:opacity var(--t-quick) var(--ease);
}
.menu[hidden]{display:none}
.menu.is-open{opacity:1;pointer-events:auto;transition-duration:var(--t-state)}
/* NIGHT-DEPTH PARK — the parked room steps back behind the index rather than
   being covered by it. All THREE filter functions are present in every state:
   CSS only interpolates filter lists of matching length and order, so dropping
   one would SNAP the rack instead of easing it (measured on the P2 rig). */
#scene{transition:filter var(--t-state) var(--ease)}
html.menu-open #scene{filter:blur(6px) brightness(0.55) saturate(0.85)}
/* the copy steps aside (JS adds inert + aria-hidden on the same beat) and the
   CSS tilt-shift is suppressed — the canvas filter is doing that job now */
html.menu-open #screens{opacity:0;transition:opacity var(--t-quick) var(--ease)}
html.menu-open #tiltshift{display:none}

/* THE PANEL — two zones. Left: the ledger. Right: the survey card. The panel
   spans the viewport inside --gutter, so the ledger keeps the estate's left
   margin and the card sits out over the room. */
.menu-panel{
  --menu-gap:clamp(2rem,6vw,7rem);
  display:grid;grid-template-columns:minmax(0,1.35fr) minmax(0,1fr);
  align-content:center;column-gap:var(--menu-gap);
  width:100%;padding:clamp(5rem,12vh,8rem) var(--gutter) clamp(3rem,8vh,5rem);
  /* Stagger law: step = min(60ms, 300ms/(n-1)). The LEFT ZONE has TEN entries
     (Start · The problem · group label · four stage rows · Contact · CTA · info
     link), so the step is 300/9 = 33ms. The survey card drafts in PARALLEL with
     the stage rows (--di 3, alongside entry 4) rather than after entry 10 — the
     accepted P1 deviation that keeps the full open inside the ~700ms budget. */
  --gm-draft-step:33ms;
}
/* align-items:flex-start — the secondary entries are content-width targets, as
   they have always been. Only the stage ledger stretches, because its hairline
   rules are the thing that makes it read as an index. */
/* THE DEPTH STEP (M2) — while the explainer card is up, the index RECEDES one
   step rather than being torn down: room → index → card, one world at three
   depths. The base rule carries the RETURN duration (--t-quick) and the state
   rule carries the recede (--t-state) — the same asymmetry .menu and .modal use,
   for the same reason: a transition runs on the after-change style.
   Opacity + filter only; the panel's box never moves, so CLS is untouched. The
   canvas park is deliberately unchanged — the room does not step twice. */
.menu-panel{transition:opacity var(--t-quick) var(--ease),filter var(--t-quick) var(--ease)}
/* the hint is scoped to the open menu, not to the transition itself: a layer
   created on the frame the blur starts costs the frame it was meant to save, and
   scoping it to a state that lasts seconds — never the page's life — is what
   will-change is for. Measured: the dips below 60 on the depth step were the
   card's backdrop-filter re-snapshotting a panel that was re-rasterising under
   it every frame. */
html.menu-open .menu-panel{will-change:opacity,filter}
html.modal-over-menu .menu-panel{opacity:.35;filter:blur(2px);transition-duration:var(--t-state)}
/* the menu's 0.55 backdrop is already under this one, so the card's own veil is
   the DIFFERENCE between the two depths, not a second full-strength scrim */
html.modal-over-menu .modal{background:rgba(8,9,11,0.35)}
/* the X stands down with the rest of the chrome row: the card's own × is the
   only close control while it is up (JS makes both inert on the same beat) */
html.modal-over-menu #chrome .hamburger{opacity:0;pointer-events:none;transition-duration:var(--t-quick)}
.menu-zone-l{display:flex;flex-direction:column;justify-content:center;align-items:flex-start;min-width:0}
.menu-stages{align-self:stretch}
.menu-zone-r{position:relative;display:flex;flex-direction:column;justify-content:center;min-width:0}
/* BACKDROP CLICK. The panel used to be a content-width flex column, so a click
   beside it landed on #menu and the `e.target === menu` handler closed the
   overlay. The two-zone grid spans the whole viewport, which would have made
   that handler unreachable — the backdrop would have stopped closing without a
   single line of JS changing. The panel and its zones are therefore transparent
   to the pointer and only the CONTROLS (and the survey card, which is content,
   not backdrop) take hits back. The handler is untouched. */
.menu-panel,.menu-zone-l,.menu-zone-r,.menu-stages{pointer-events:none}
.menu-item,.menu-cta,.menu-info,.menu-survey{pointer-events:auto}

/* ============ THE DRAFT-IN (§4.2) — Plan's blueprint grammar ============
   Lines are DRAWN, not faded: a clip-path inset reveal left-to-right plus a 6px
   settle, per entry at --t-state on --ease, --gm-draft-step apart in DOM order.
   One grammar, two overlays — the modal card is the same move with n = 1.
   Three deliberate mechanics:
   · An ANIMATION, not a transition. .menu-item's hover slide owns `transform`
     and its own transition list; a competing transition on the same element
     would replace that list and the settle would fight the slide. The animation
     sits in the animation origin, touches only clip-path and the independent
     `translate` property, and leaves both alone.
   · Keyed on .is-open, so it RE-ARMS on every open by construction — the class
     goes on, the animation starts from zero. No remove/re-add park to get wrong.
   · `backwards` fill, not `both`. Backwards holds the undrawn state through the
     stagger delay (which is the whole point) and then hands the element back to
     its own styles, so nothing is left clipped — a lingering inset(0) would cut
     the .menu-lead's glow at the item's border box. It is also what makes the
     exit safe: when .is-open goes, the animation goes with it and the entries
     stay drawn while the whole overlay fades at --t-quick. Never a reverse-draft.
   --di is written once at boot by app.js, the .hero-head --wi idiom. */
@keyframes gm-draft-in{
  from{clip-path:inset(0 100% 0 0);translate:6px 0}
  to  {clip-path:inset(0 0 0 0);translate:0 0}
}
.menu.is-open .gm-draft,
.modal.is-open .gm-draft{
  animation:gm-draft-in var(--t-state) var(--ease) backwards;
  animation-delay:calc(var(--di,0) * var(--gm-draft-step,0ms));
}
/* min-height carries the 44px touch target on every row.
   M1 2026-08-12 — the SECONDARY tier. Start · The problem · Contact are utility
   now: they sit at 1.15rem while the four stages take the display scale below.
   The idiom (lead bar + 8px slide) is identical on both tiers. */
.menu-item{
  display:inline-flex;align-items:center;background:none;border:none;cursor:pointer;
  font-family:var(--display);font-weight:600;font-size:1.15rem;
  line-height:1.2;color:var(--ink);letter-spacing:-0.01em;text-align:left;
  min-height:44px;min-width:44px;padding:.1rem 0;
  transition:transform var(--t-state) var(--ease),color var(--t-state) var(--ease);
}
/* the menu presented eight flat options — over the working-memory limit for a
   decision point. The four stages are one idea, so they group under one label
   and read as a cluster: Start · The problem · [stages] · Contact. */
.menu-group{
  margin:.9rem 0 .1rem;font-family:var(--mono);font-size:10px;letter-spacing:0.22em;
  text-transform:uppercase;color:var(--faint);
}
/* THE LEDGER — the four stages STACKED, never a wrapped row. The old
   flex-wrap row re-flowed to two lines at narrow widths and put two stages on
   one baseline, which read as pairs; a column of numbered rows under a hairline
   reads as an index. Stacking also kills the wrap defect structurally. */
.menu-stages{display:flex;flex-direction:column;margin:.15rem 0 .4rem}
.menu-stages .menu-item{
  display:flex;align-items:center;width:100%;
  border-bottom:1px solid var(--line);
  padding:clamp(.4rem,1.2vh,.85rem) 0;
  font-size:clamp(2.2rem,4.5vw,3.4rem);line-height:1.04;letter-spacing:-0.025em;
}
.menu-stages .menu-lead{height:1.5px}
/* the numeral column is fixed-width so the four names align and the "you are
   here" dot has a reserved lane beside the numeral */
.ms-num{
  position:relative;flex:0 0 4.4rem;
  font-family:var(--mono);font-weight:400;font-size:13px;letter-spacing:0.22em;
  color:var(--accent);
}
.ms-name{min-width:0}
/* "YOU ARE HERE" — a lit dot ON the numeral line, optically aligned, breathing.
   Absolutely positioned inside the numeral's own box: it takes no space, so it
   can appear and disappear per screen with zero layout cost.
   It sits mid-lane, ~27px clear of the numeral's last glyph: measured hard
   against it (right:.55rem in a 3.4rem lane) the 12px glow landed inside the 2px
   ring the AA sweep reads as the numeral's local background and took
   orange-on-orange to 2.20:1. Clear of it the same numeral measures 5.96:1. */
.ms-dot{
  position:absolute;right:.9rem;top:50%;margin-top:-4px;
  width:8px;height:8px;border-radius:50%;
  background:var(--accent);box-shadow:0 0 12px rgba(255,77,0,0.9);
  opacity:0;pointer-events:none;
}
.menu-item[aria-current="true"] .ms-dot{opacity:1;animation:gm-dot-breathe 2.6s var(--ease-breathe) infinite}
@keyframes gm-dot-breathe{0%,100%{opacity:1;scale:1}50%{opacity:.45;scale:.82}}
/* the "what is responsible AI?" explainer, demoted here from the header slot it
   used to occupy in front of the conversion CTA */
.menu-info{
  display:inline-flex;align-items:center;min-height:44px;
  align-self:flex-start;margin-top:1rem;background:none;border:none;cursor:pointer;
  font-family:var(--body);font-size:.95rem;color:var(--faint);text-align:left;
  padding:.5rem 0;
  /* Batch 7 (2026-08-11) — LINK IDIOM CONVERGENCE (see .enter-alt). The inset
     box-shadow rule becomes the same two-layer sweep. The element has no border,
     so the inset shadow's 1px row and a `0 100%` background layer occupy the same
     pixels: the rest state is unchanged and the box never moves. 44px min-height
     and the colours stay exactly as they were. */
  background-image:linear-gradient(var(--accent),var(--accent)),linear-gradient(rgba(234,231,222,0.22),rgba(234,231,222,0.22));
  background-repeat:no-repeat;background-position:0 100%,0 100%;background-size:0% 1px,100% 1px;
  transition:color var(--t-quick) var(--ease),background-size var(--t-state) var(--ease);
}
/* A11 2026-08-10 · Batch 2 2026-08-10 — TEXT IDIOM: tint only, no compression. */
@media (hover:hover){ .menu-info:hover{color:var(--ink);background-size:100% 1px,100% 1px} }
.menu-info:active{color:var(--ink);background-size:100% 1px,100% 1px;transition-duration:var(--t-press)}

/* THE LEAD BAR — transforms, not box metrics (M1, 2026-08-12).
   This one rule was the site's ENTIRE standing CLS (0.001255 measured): the bar
   animated `width` 0->22px and `margin-right` 0->14px on hover, so every hover
   re-laid-out the row and every row after it. The box is now RESERVED at its
   final size and never changes; only a scaleX on the inner layer moves, from
   transform-origin:left, so the bar still grows left-anchored with its glow
   intact and the rendered hover/focus/active states are the ones that shipped.
   No width/margin is animated anywhere in this menu now. */
.menu-lead{
  position:relative;display:inline-block;width:22px;height:2px;margin-right:14px;
  vertical-align:middle;flex:0 0 auto;
}
.menu-lead::before{
  content:"";position:absolute;inset:0;background:var(--accent);
  border-radius:2px;box-shadow:0 0 8px rgba(255,77,0,0.7);
  transform:scaleX(0);transform-origin:left center;
  transition:transform var(--t-state) var(--ease);
}
/* A11 2026-08-10 — the :focus-visible half stays OUTSIDE the hover query:
   keyboard focus is not a pointer capability. This is the one component that
   keeps focus styling on top of the global :focus-visible outline, because its
   hover is a positional state indicator (the lead bar marks where you are in the
   list), not decoration. */
@media (hover:hover){
  .menu-item:hover{transform:translateX(8px);color:var(--ink)}
  .menu-item:hover .menu-lead::before{transform:scaleX(1)}
}
.menu-item:focus-visible{transform:translateX(8px);color:var(--ink)}
.menu-item:focus-visible .menu-lead::before{transform:scaleX(1)}
/* Batch 2 (press) — the one control whose hover already displaces (documented
   exception: the slide is a positional state indicator). Press does not compress
   it; it DEEPENS the slide, 8px -> 10px, at the press tier. Hover and focus stay
   at 8px above. */
.menu-item:active{transform:translateX(10px);color:var(--ink);transition-duration:var(--t-press)}
.menu-item:active .menu-lead::before{transform:scaleX(1)}

/* THE CTA — sweep-fill, the estate's primary-action recipe at pill scale
   (.cta-panel, styles.css ~945): a ::before layer sweeping scaleX(0->1) from the
   left at --t-state on --ease, the label inverting toward --accent and the
   border going --accent with it. Replaces the flat rgba(255,77,0,0.08) tint,
   which was the only primary action on the site that did not sweep.
   isolation + z-index:-1 keeps the layer behind the anchor's own text without a
   wrapper span (the anchor's content is a bare text node). */
.menu-cta{
  position:relative;isolation:isolate;overflow:hidden;
  align-self:flex-start;margin-top:clamp(1.2rem,3vh,2rem);font-size:1rem;
  transition:border-color var(--t-state) var(--ease),color var(--t-state) var(--ease),
             transform var(--t-quick) var(--ease);
}
.menu-cta::before{
  content:"";position:absolute;inset:0;z-index:-1;
  background:linear-gradient(90deg,var(--soft),rgba(255,77,0,0.18));
  transform:scaleX(0);transform-origin:left center;
  transition:transform var(--t-state) var(--ease);
}
/* A11 2026-08-10 — .menu-cta is .pill, not .cta-pill, so this is its only hover */
@media (hover:hover){
  .menu-cta:hover{border-color:var(--accent);color:var(--accent)}
  .menu-cta:hover::before{transform:scaleX(1)}
}
.menu-cta:active{border-color:var(--accent);color:var(--accent);transform:scale(0.97);transition-duration:var(--t-press)}
.menu-cta:active::before{transform:scaleX(1);transition-duration:var(--t-press)}

/* ---- THE SURVEY CARD (non-interactive; no tab stop, no ARIA surface) ----
   Material is the .board recipe verbatim: rgba(14,16,19,0.86), 1px --line, 16px
   radius, backdrop-filter blur(8px) (the value measured to actually bite). */
.menu-survey{
  position:relative;max-width:26rem;
  background:rgba(14,16,19,0.86);border:1px solid var(--line);border-radius:16px;
  -webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
  padding:clamp(1.1rem,2.4vw,1.7rem);
}
.ms-eyebrow{font-family:var(--mono);font-size:10px;letter-spacing:0.22em;color:var(--faint)}
.ms-position{
  font-family:var(--mono);font-size:clamp(.95rem,1.5vw,1.15rem);
  letter-spacing:0.06em;color:var(--ink);margin:.85rem 0 .7rem;
}
.msp-num{color:var(--accent)}
/* min-height RESERVES two lines: the descriptor swaps text on stage hover, and a
   swap that changed the card's height would be a layout shift charged to a hover. */
.ms-desc{
  font-family:var(--body);font-size:.95rem;line-height:1.5;color:var(--dim);
  min-height:3em;transition:opacity var(--t-quick) var(--ease);
}
/* THE SURVEY PIN — the room-annotation register (MOTION-SPEC §7 amendment,
   owner-approved 2026-08-12): a 1px cyan hairline running left out of the card
   into the plate, ending in a 6px lit dot. Cyan appears NOWHERE else in this
   menu; every interface accent stays orange. Spans exactly the column gap, so
   it can never reach across the ledger's text. */
.menu-pin{
  position:absolute;top:38%;left:calc(-1 * var(--menu-gap));
  width:6px;height:6px;border-radius:50%;
  background:var(--cyan);box-shadow:0 0 8px rgba(75,225,227,0.9);
}
.menu-pin::after{
  content:"";position:absolute;left:6px;top:2.5px;height:1px;
  width:calc(var(--menu-gap) - 6px);background:var(--cyan);opacity:.45;
}

/* ============ "RESPONSIBLE AI" MODAL (M3) ============ */
/* Batch 4 — same asymmetry as .menu above: in at --t-state, out at --t-quick. */
.modal{
  position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;
  padding:var(--gutter);background:rgba(8,9,11,0.72);
  opacity:0;pointer-events:none;transition:opacity var(--t-quick) var(--ease);
}
.modal[hidden]{display:none}
.modal.is-open{opacity:1;pointer-events:auto;transition-duration:var(--t-state)}
/* Batch 4 — the card's translateY(10px) entrance is REPLACED by the draft-in
   (.gm-draft, in the markup): one grammar, two overlays. */
.modal-card{
  position:relative;width:min(520px,92vw);
  background:rgba(14,16,19,0.86);border:1px solid var(--line);border-radius:16px;
  -webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
  padding:clamp(1.6rem,3vw,2.4rem);
}
.modal-close{
  position:absolute;top:.7rem;right:.9rem;background:none;border:none;cursor:pointer;
  font-size:1.5rem;line-height:1;color:var(--dim);transition:color var(--t-quick) var(--ease);
}
/* A9 (2026-08-10) — the glyph box was 14×24, against the 44px minimum that
   .hamburger, .menu-item and .menu-info all deliberately meet. Expanded by a
   pseudo rather than by width/height + negative margin (the .hamburger idiom)
   because the cross sits in a corner: sizing the button itself would have moved
   the glyph off its inset, and re-inseting it would have moved it visually. The
   pseudo centres 44×44 on the glyph exactly where it already is. */
.modal-close::after{content:"";position:absolute;inset:-10px -15px}
/* A11 2026-08-10 · Batch 2 2026-08-10 — glyph-scale press. The 44×44 target is the
   ::after above, which is a child of the button, so compressing the BUTTON would
   compress the hit area with it (44 -> 41.4). The × therefore sits in its own
   span and the 0.94 rides that: the glyph moves, the target does not. */
@media (hover:hover){ .modal-close:hover{color:var(--accent)} }
.modal-close:active{color:var(--accent);transition-duration:var(--t-press)}
.modal-close .mc-glyph{display:inline-block;transition:transform var(--t-quick) var(--ease)}
.modal-close:active .mc-glyph{transform:scale(0.94);transition-duration:var(--t-press)}
/* M2 · CARD DRESSING (2026-08-12) — the card was an unbranded box of prose in a
   room that indexes everything else. It keeps its geometry recipe exactly and
   gains the estate's register: the lead-bar material as a tick, a mono eyebrow
   that decodes in, a title at display scale (A4 — 1.5rem was the same size as
   the body's own measure), and below the hairline a ledger that restates the
   paragraph's three clauses in the ledger idiom the menu now uses. The
   paragraph itself is untouched, and the ledger makes NO new claim. */
.modal-tick{
  display:block;width:22px;height:2px;background:var(--accent);border-radius:2px;
  box-shadow:0 0 8px rgba(255,77,0,0.7);margin-bottom:.95rem;
}
.modal-eyebrow{
  font-family:var(--mono);font-size:10px;letter-spacing:0.22em;color:var(--faint);
  margin-bottom:.7rem;
}
/* padding-right clears the × column, so the title can never share a line with it
   however the head wraps (A2). It is dropped under 640px: there the eyebrow
   already puts the whole title well below the × (measured clearance 20.8px), and
   the 2.4rem would spend a sixth of a 320px card's measure buying nothing —
   which was the difference between a two-line and a three-line head. */
.modal-title{
  font-family:var(--display);font-weight:600;font-size:clamp(1.5rem,3vw,2.2rem);
  line-height:1.14;letter-spacing:-0.02em;color:var(--ink);margin-bottom:.9rem;
  padding-right:2.4rem;
}
.modal-body{font-size:1.02rem;line-height:1.6;color:var(--dim)}
.modal-ledger{margin-top:clamp(1.2rem,2.6vh,1.8rem);border-top:1px solid var(--line)}
.ml-row{
  display:grid;grid-template-columns:2.4rem 1fr;gap:.9rem;align-items:baseline;
  padding:.7rem 0;border-bottom:1px solid var(--line);
}
.ml-num{font-family:var(--mono);font-size:13px;letter-spacing:0.14em;color:var(--accent)}
.ml-label{font-family:var(--mono);font-size:11px;letter-spacing:0.1em;line-height:1.5;color:var(--dim)}

/* ============ REDUCED MOTION ============ */
html.reduced #scroll-spacer{display:none}
html.reduced #tiltshift{display:none}
/* A2 (2026-08-13, M1): reduced motion used to `display:none` the cue, which on the
   phone hero removed the ONLY affordance saying the page scrolls. Motion was never
   the problem — the cue does not animate; the reveal fn that fades it is skipped
   under reduced motion anyway. It now stays, static and visible. */
html.reduced .cue{display:block;opacity:1;animation:none;translate:none;will-change:auto}
html.reduced .gate-ring{animation:none !important}
html.reduced .w{opacity:1;transform:none}
html.reduced [data-reveal],
html.reduced .hero-inner .cta-pill,
html.reduced .promise,
html.reduced .outro-cta{opacity:1;transform:none}
html.reduced .menu,html.reduced .menu-card,html.reduced .modal,html.reduced .modal-card,
html.reduced .menu-item,html.reduced .menu-lead{transition:none !important}
/* M1 2026-08-12 — the menu's reduced-motion layer is AUTHORED FINALS, not a
   nuke: the open lands on the finished composition instantly. The room is still
   held in depth (a static defocus is a STATE, not motion) — only the rack's
   tween goes; the copy is still stepped aside; the entries are drawn (the
   .gm-draft rule below); the "you are here" dot keeps its lit glow and loses
   only the breathe; the lead bar and the CTA sweep arrive at their final
   transform with no tween. */
html.reduced #scene,html.reduced #screens,
html.reduced .menu-lead::before,html.reduced .menu-cta,html.reduced .menu-cta::before,
html.reduced .ms-desc,
/* M2 — the depth step is a STATE under reduced motion: the index is still
   receded behind the card, it simply arrives there with no tween. The boot draw
   does not happen at all (JS never arms it; this is the no-JS parity). */
html.reduced .menu-panel,html.reduced .hamburger{transition:none !important}
html.reduced .ms-dot,html.reduced .hamburger i{animation:none !important}
html.reduced .gate-ring svg{display:none}
html.reduced .gate-ring{width:auto}
/* Batch 2/3 (2026-08-10) — reduced motion is an authored second layout, so every
   state added by the press vocabulary, the gate focal sequence and the confirm
   grammar exists here as an INSTANT final state. Two deliberate survivals: the
   completed gate keeps the lit dot's scale(1.5) and its steady 12px glow (that is
   state, not motion), and .menu-item keeps its slide (a positional state
   indicator, the documented hover exception) — it simply arrives with no tween.
   Compressions do not happen at all. The decode is handled in JS: REDUCED writes
   the final string with no timers. Mirrored in the @media block below. */
html.reduced .gate.is-full .gate-ring-fill,
html.reduced .gate.is-full .gate-dot,
html.reduced .gate.is-full .gate-label,
html.reduced .gate-pill,
html.reduced .brand,
html.reduced .nav-cta,
html.reduced .hamburger i,
html.reduced .pick-chip,
html.reduced .copy-email,
html.reduced .copy-email .ce-label,
html.reduced .cta-panel,
html.reduced .modal-close,
html.reduced .modal-close .mc-glyph{transition:none !important}
html.reduced .gate.is-full .gate-dot,
html.reduced .pick-chip.is-land[aria-pressed="true"],
html.reduced .copy-email.done{animation:none !important}
html.reduced .gate.is-full .gate-dot{transform:scale(1.5);box-shadow:0 0 12px rgba(255,77,0,0.9)}
html.reduced .brand:active,
html.reduced .nav-cta:active,
html.reduced .cta-pill:active,
html.reduced .pick-chip:active,
html.reduced .copy-email:active,
html.reduced .gate-pill:active,
html.reduced .menu-cta:active,
html.reduced .modal-close:active .mc-glyph{transform:none !important}
/* the hamburger press is the independent `scale` now, and its transform carries
   the open X — a STATE, which reduced motion keeps; only the tween goes. */
html.reduced .hamburger:active i{scale:none !important}
/* ---- Batch 4/5 (2026-08-10) — the same authored second layout for the overlay
   draft-in, the boot ignition, the focus pull and the badge. Every new state
   exists here as its instant equivalent: entries and cards are drawn on arrival,
   the enter frame is simply present, nothing sparks, nothing blurs. ---- */
html.reduced .stage-badge,
html.reduced .enter-cta,
html.reduced .enter-dot,
html.reduced #preloader{transition:none !important}
html.reduced .gm-draft,
html.reduced .menu.is-open .gm-draft,
html.reduced .modal.is-open .gm-draft{animation:none !important;clip-path:none !important;translate:none !important}
html.reduced .enter-dot.is-spark,
html.reduced .pre-bar.is-spark{animation:none !important}
html.reduced.pre-hold .enter-word,
html.reduced.pre-hold .enter-line,
html.reduced.pre-hold .enter-sub,
html.reduced.pre-hold .enter-offer,
html.reduced.pre-hold .enter-cta,
html.reduced.pre-hold .enter-alt,
html.reduced .enter-word,
html.reduced .enter-line,
html.reduced .enter-sub,
html.reduced .enter-offer,
html.reduced .enter-alt{animation:none !important;opacity:1;translate:none !important;filter:none !important}
/* the CTA keeps its own dimmed/ready opacity — only the animation goes */
html.reduced .enter-cta{animation:none !important;translate:none !important;filter:none !important}
/* not `none`: this one's transform carries its centring, not its press */
html.reduced .enter-cta:active{transform:translate(-50%,-50%) !important}
html.reduced .hero-inner .cta-pill:active,
html.reduced .cta-panel:active{scale:none !important}
/* ---- Batch 6/7 (2026-08-11) — the fallback path and the converged link idiom.
   NOTE ON THE FALLBACK: html.reduced is written by app.js, and the fallback is
   served precisely when app.js may not have run — so the @media block below is the
   home that actually matters there. This half is parity for completeness (the
   fallback also shows on a WebGL failure with JS alive). The sweep is a transition,
   so under reduced motion the underline is simply present at its final width. ---- */
html.reduced .fb-cta,
html.reduced .fb-submit,
/* ENQUIRY-M2 — the dressed flat tier's two new transitions, parity half */
html.reduced .fb-input,
html.reduced .fb-privacy a,
html.reduced footer.fb-foot a,
html.reduced .enter-alt,
html.reduced .enter-alt .ea-line,
html.reduced .menu-info,
html.reduced .outro-email{transition:none !important}
html.reduced .fb-cta:active,
html.reduced .fb-submit:active{transform:none !important}
.continue-btn{margin-top:1.6rem}

/* reduced: Discover stage → scrollable stacked column, all copy visible */
/* 0a (2026-08-10) — the column was scrollable and unreachable. Measured at
   1440x900 on Discover: .stage scrollHeight 1539 vs clientHeight 885, scrollTop
   stuck at 0 under real wheel input, elementFromPoint at frame centre returning
   the scene CANVAS. The base rule .screen{pointer-events:none} (see SCREENS)
   meant the wheel never hit-tested onto the scrollable box, and #scroll-spacer is
   display:none under html.reduced so the document could not scroll either — the
   Continue gate sat ~516px below the fold with no input that could reach it.
   Only the active screen is in the DOM without [hidden], the chrome sits at
   z-index 30 above it, and nothing here changes a box: pointer-events is not a
   layout property, so CLS stays 0. */
html.reduced .stage{overflow-y:auto;pointer-events:auto}
html.reduced .stage .scene-markers,
html.reduced .stage .step-dots{display:none}
/* position:relative (not static) keeps the Item 2A scrim ::before anchored to its own
   copy block in the reduced-motion stacked column instead of the fixed screen.
   FIX 2026-07-27 — relative stays in normal flow but STILL APPLIES inset offsets,
   and these two rules inherited live ones from their fixed-screen base: .stage-title
   kept `right:var(--gutter); bottom:clamp(2rem,7vh,4.5rem)` (shunting the STAGE 01
   pill up onto the GRIMSDALE lockup) and .steps kept `top:50%` (dropping the whole
   steps column down onto the boards). The sibling rules for .step / .board / .gate
   already zeroed or avoided their offsets; these two were missed. Zero them
   explicitly — reduced-motion is a supported path and it was rendering overlaps. */
html.reduced .stage .stage-title{
  position:relative;top:auto;right:auto;bottom:auto;left:auto;
  text-align:left;max-width:none;opacity:1;padding:5.5rem var(--gutter) 0;
}
html.reduced .stage .stage-purpose{margin-left:0;margin-right:0}
html.reduced .stage .steps{
  position:relative;top:auto;right:auto;bottom:auto;left:auto;
  transform:none;width:auto;min-height:0;padding:1.6rem var(--gutter) 0;
}
/* the reduced column is a plain stack — undo the per-stage desktop composition */
html.reduced .stage .boards{display:block;gap:0;width:auto}
html.reduced .stage .board{padding:20px;border-radius:16px}
html.reduced .stage .board::before{display:block}
html.reduced .stage .board h3{font-size:1.15rem;margin-bottom:.5rem}
html.reduced .stage .board p{font-size:.95rem;line-height:1.5}
/* relative (not static) keeps the F5 copy-pocket ::before anchored to its own
   step in the reduced stacked column; relative stays in normal flow. */
html.reduced .stage .step{position:relative;top:auto;left:auto;width:auto;opacity:1;transform:none;margin-bottom:1.4rem}
html.reduced .stage .boards{position:static;transform:none;width:auto;min-height:0;padding:1.6rem var(--gutter) 0}
html.reduced .stage .board{position:static;width:auto;max-width:44ch;opacity:1;transform:none;margin-bottom:1rem}
html.reduced .stage .gate{position:static;transform:none;margin:1.6rem var(--gutter) 4rem}

/* ============================================================
   ITEM 2A — LOCAL COPY SCRIM (readability over the WebGL scene)
   Invisible-by-design feathered darkening behind each raw copy column that sits
   over the scene, so --dim text stays comfortable over pale furniture. Reads as
   depth-of-field / shadow, never a panel: no visible edge, no solid fill, no filter.
   Absolutely-positioned pseudo-element (z-index:-1) → zero CLS, no JS. Board cards
   keep their own scrim and are left untouched.
   ============================================================ */
.hero-inner::before,
.problem-inner::before,
.startgate-inner::before,
.stage-title::before,
.steps::before,
.outro-inner::before{
  content:"";
  position:absolute;
  inset:-4.5rem -6rem;
  z-index:-1;
  pointer-events:none;
  background:radial-gradient(ellipse 80% 74% at 50% 50%,
    rgba(8,9,11,0.55) 0%,
    rgba(8,9,11,0.53) 10%,
    rgba(8,9,11,0.50) 18%,
    rgba(8,9,11,0.45) 27%,
    rgba(8,9,11,0.38) 37%,
    rgba(8,9,11,0.30) 47%,
    rgba(8,9,11,0.22) 58%,
    rgba(8,9,11,0.14) 70%,
    rgba(8,9,11,0.08) 80%,
    rgba(8,9,11,0.03) 90%,
    rgba(8,9,11,0.00) 100%);
}
/* F3 — the Problem column's note sits at the column's bottom edge, where the
   shared ellipse falls off; extend this one scrim downward and re-centre it so
   the note stays covered. Same stops, same feathering, no visible edge. */
.problem-inner::before{
  inset:-4.5rem -6rem -6.5rem;
  background:radial-gradient(ellipse 80% 84% at 50% 54%,
    rgba(8,9,11,0.55) 0%,
    rgba(8,9,11,0.53) 10%,
    rgba(8,9,11,0.50) 18%,
    rgba(8,9,11,0.45) 27%,
    rgba(8,9,11,0.38) 37%,
    rgba(8,9,11,0.30) 47%,
    rgba(8,9,11,0.22) 58%,
    rgba(8,9,11,0.14) 70%,
    rgba(8,9,11,0.08) 80%,
    rgba(8,9,11,0.03) 90%,
    rgba(8,9,11,0.00) 100%);
}
/* faint depth shadow on the --dim copy that rides over the scene.
   .startgate-head is the one headline included: a bright fixture crosses its top
   line and measures ~2.9:1 worst-pixel without it (large-text bar is 3:1). */
.hero-sub,.problem-body,.problem-note,.stage-purpose,.step-line,.startgate-sub,
.startgate-head,.promise,.outro-reassure,.outro-email{
  text-shadow:0 1px 24px rgba(8,9,11,0.85);
}

/* ============================================================
   M1 AA REMEDIATION (2026-08-04)
   Measured with the glyph-mask method: freeze rAF, drive __wt.go/scrollToP,
   capture the real composited page twice (glyphs painted / glyphs transparent
   with their text-shadow kept), mask to the pixels the glyphs actually cover
   dilated 2px, and take the 95th-percentile background luminance inside that
   mask. 502 blocks: 9 screens x p {0.15,0.50,0.85} x desktop 1440x900 and
   mobile 375x812. Everything below measured under AA on that worst-case
   statistic. Fixes are scrim / chip-backing / text-shadow ONLY — no copy, no
   layout, no colour token moved, so CLS is untouched.

   The recurring offender is ORANGE at caption size. #ff4d00 sits at relative
   luminance 0.264: for 4.5:1 the background behind it must be <= 0.020 — it
   cannot be reached from above at all, so every orange caption needs a
   near-carbon local backing rather than a softer scrim. Stacking identical
   shadows is how that is bought: n coincident layers composite to
   1-(1-a)^n, which is effectively opaque within ~2px of the stroke and still
   fully feathered by 16px, so nothing reads as a panel over the dark room. */
.step-eyebrow{
  text-shadow:
    0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1),
    0 0 2px rgba(8,9,11,1), 
    0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1),
    0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1),
    0 0 2px rgba(8,9,11,1), 
    0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1),
    0 0 3px rgba(8,9,11,1), 0 0 5px rgba(8,9,11,1),
    0 0 10px rgba(8,9,11,0.97), 0 0 18px rgba(8,9,11,0.9),
    0 1px 3px rgba(8,9,11,1), 0 0 8px rgba(255,77,0,0.45);
}
/* The last block that would not clear AA on text-shadow alone is STEP 0n on
   Implement p≈0.50 — the construction front is the brightest surface the site
   ever puts behind copy, and the eyebrow sits at the TOP of the step block where
   the F5 pocket's ellipse (centred 50% 46%) is already falling off. Give the
   eyebrow line its own tight pocket rather than deepening the shipped one, so
   the lead/line stay exactly as tuned on 07-18. Same gradient family, ~1 line
   tall, fully feathered — invisible over the dark room like the rest. */
.step-eyebrow{position:relative}
.step-eyebrow::before{
  content:"";position:absolute;z-index:-1;pointer-events:none;
  inset:-0.42rem -0.7rem -0.34rem;
  background:radial-gradient(ellipse 74% 100% at 50% 50%,
    rgba(8,9,11,0.86) 0%,
    rgba(8,9,11,0.86) 42%,
    rgba(8,9,11,0.72) 58%,
    rgba(8,9,11,0.50) 72%,
    rgba(8,9,11,0.28) 84%,
    rgba(8,9,11,0.10) 93%,
    rgba(8,9,11,0.00) 100%);
}
.promise-num{
  text-shadow:
    0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1),
    0 0 2px rgba(8,9,11,1), 
    0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1),
    0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1),
    0 0 2px rgba(8,9,11,1), 
    0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1),
    0 0 3px rgba(8,9,11,1), 0 0 5px rgba(8,9,11,1),
    0 0 10px rgba(8,9,11,0.97), 0 0 18px rgba(8,9,11,0.9),
    0 1px 3px rgba(8,9,11,1);
}
/* the stage badge is chrome: it rides over every scene state with no column
   scrim behind it, and on mobile it sits directly over the Implement front's
   picket curtain. Fill the pill it already draws — same chip family as
   .zlabel / .pick-chip, identical box, so nothing moves. */
.stage-badge .sb-num{
  /* opaque: at 0.97 a bloom flare behind the chrome still read through the chip
     and put the orange numeral at 3.3:1 on mobile Discover */
  background:var(--carbon);
  text-shadow:0 0 3px rgba(8,9,11,1),0 0 6px rgba(8,9,11,0.95),0 1px 3px rgba(8,9,11,1);
}
.stage-badge .sb-name{text-shadow:0 0 4px rgba(8,9,11,0.98),0 1px 3px rgba(8,9,11,0.95)}
/* brand lockup — same exposure, no scrim; worst case is mobile Implement p=0.50 */
.brand-word,.brand-sub{
  text-shadow:
    0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1),
    0 0 2px rgba(8,9,11,1), 
    0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1),
    0 0 6px rgba(8,9,11,0.99), 0 0 14px rgba(8,9,11,0.92), 0 1px 3px rgba(8,9,11,0.98);
}
/* persistent CTA pill — orange label; deepen the fill it already has */
.nav-cta{background:rgba(8,9,11,0.86)}
.nav-cta{text-shadow:0 0 3px rgba(8,9,11,1),0 0 8px rgba(8,9,11,0.92)}
/* A11 2026-08-10 — paired with the guard on .nav-cta's base hover above */
@media (hover:hover){ .nav-cta:hover{text-shadow:none} }
.nav-cta:active{text-shadow:none}
/* enter screen: the wordmark is large text (needs 3:1) and the alt link is
   caption-size --faint; both sit over the light-up ramp's brightest floor edge */
.enter-word{text-shadow:0 1px 3px rgba(8,9,11,0.9),0 0 22px rgba(8,9,11,0.8)}
.enter-alt{text-shadow:0 0 4px rgba(8,9,11,0.98),0 0 14px rgba(8,9,11,0.9),0 1px 3px rgba(8,9,11,0.95)}
/* M1-R1: the lamp-warmth restore lifted the enter ramp's brightest floor edge and
   put this one block at 4.44:1 on mobile. Its siblings already carry a backing. */
.enter-sub{text-shadow:0 0 4px rgba(8,9,11,0.98),0 0 14px rgba(8,9,11,0.9),0 1px 3px rgba(8,9,11,0.95)}
/* problem headline — large text, marginal at p=0.50 where the pale partition
   crosses the second line */
.problem-head{text-shadow:0 1px 3px rgba(8,9,11,0.85),0 0 20px rgba(8,9,11,0.75)}
/* projected scene chips sit ON the model, so their own fill is the only backing
   they can have; 0.66 was transparent enough for the lit furniture to show through */
.zlabel,.hlabel{background:rgba(6,8,11,0.86)}

/* ============================================================
   NO-WEBGL / NO-JS / CDN-BLOCKED FALLBACK (2026-07-27)
   There was no try/catch around the renderer, no capability probe and no
   <noscript>: WebGL disabled, a blocklisted GPU, JS off or a corporate proxy
   blocking jsDelivr all produced a permanent "LOADING — 0%" behind a disabled
   button, with no email, no phone and no <footer> anywhere in the document.
   That IT posture describes the exact SMB audience being sold to — for them the
   site wasn't degraded, it was gone, with no way to make contact. This carries
   the proposition, the four stages, the promises and the address as plain,
   scrollable HTML that needs nothing but a browser.
   ============================================================ */
.fallback{display:none;position:relative;z-index:80;background:var(--carbon)}
html.fb-on .fallback{display:block}
html.fb-on #preloader,html.fb-on #screens,html.fb-on #chrome,
html.fb-on #scene,html.fb-on #tiltshift,html.fb-on #vignette,
html.fb-on #veil,html.fb-on #scroll-spacer,html.fb-on #menu,
html.fb-on #ra-modal{display:none!important}
html.fb-on,html.fb-on body{height:auto;overflow-y:auto}
.fb-inner{max-width:68ch;margin:0 auto;padding:clamp(2.5rem,7vh,5rem) var(--gutter) 3rem}
.fb-mark{display:flex;flex-direction:column;gap:.35rem;margin-bottom:2.6rem}
.fb-word{font-family:var(--display);font-weight:700;font-size:1.5rem;letter-spacing:0.01em;line-height:1}
.fb-sub{font-family:var(--mono);font-size:11px;letter-spacing:0.24em;text-transform:uppercase;color:var(--dim)}
.fb-head{
  font-family:var(--display);font-weight:700;font-size:clamp(2rem,5vw,3.2rem);
  line-height:1.04;letter-spacing:-0.015em;margin-bottom:1.1rem;
}
.fb-head em{font-style:normal;color:var(--accent)}
.fb-lede{font-size:1.08rem;line-height:1.6;color:var(--dim);margin-bottom:2.4rem;max-width:52ch}
.fb-h2{
  font-family:var(--mono);font-size:11px;letter-spacing:0.22em;text-transform:uppercase;
  color:var(--faint);margin-bottom:1rem;
}
.fb-stages{list-style:none;display:grid;gap:1.1rem;margin-bottom:2.4rem}
.fb-stages li{display:grid;grid-template-columns:2.6rem 1fr;column-gap:1rem;align-items:baseline}
.fb-stages b{
  font-family:var(--mono);font-size:11px;letter-spacing:0.16em;color:var(--accent);font-weight:400;
}
.fb-stages strong{display:block;font-family:var(--display);font-weight:600;font-size:1.2rem;color:var(--ink);margin-bottom:.2rem}
.fb-stages span{font-size:.98rem;line-height:1.55;color:var(--dim)}
.fb-promises{list-style:none;display:grid;gap:.85rem;margin-bottom:2.4rem}
.fb-promises li{
  font-size:1rem;line-height:1.55;color:var(--ink);
  padding-left:1.1rem;border-left:2px solid var(--line);
}
.fb-cta{
  display:inline-flex;align-items:center;gap:.8rem;min-height:56px;padding:0 1.4rem;
  background:var(--accent);color:var(--carbon);border-radius:14px;
  font-size:1.05rem;font-weight:500;
  /* Batch 6 (2026-08-11) — the fallback path had NO transition at all: its states
     snapped. It is the same object as every other pill on the site, so it gets the
     same vocabulary — light at --t-quick, weight at --t-press. */
  transition:background var(--t-quick) var(--ease),transform var(--t-quick) var(--ease);
}
/* A11 2026-08-10 — in scope, and arguably the highest-value pair of the sweep:
   the fallback is served exactly when WebGL is missing or the GPU is blocklisted,
   which skews to older Android — the likeliest surface here to meet a finger.
   Batch 6 (2026-08-11) — press vocabulary: pill-scale compression at 0.97.
   Documented limitation: this path deliberately loads no app.js, so the no-op
   `touchstart` that unlocks :active on iOS Safari cannot run here. The hover guard
   plus the :active mirror is still the right shape everywhere else. */
@media (hover:hover){ .fb-cta:hover{background:var(--accent-bright)} }
.fb-cta:active{background:var(--accent-bright);transform:scale(0.97);transition-duration:var(--t-press)}
footer.fb-foot{
  margin-top:2.6rem;padding-top:1.4rem;border-top:1px solid var(--line);
  font-size:.95rem;line-height:1.7;color:var(--dim);
}
/* Batch 6 + 7 (2026-08-11) — no transition before, and the odd idiom out. Now the
   same sweep as .outro-email / .enter-alt / .menu-info: resting --line rule as the
   lower gradient layer, accent drawn over it. The border's 1px becomes
   padding-bottom on an inline box (paint-only either way), so the rule stays on the
   same pixel row and the line box is untouched. */
footer.fb-foot a{
  color:var(--ink);
  background-image:linear-gradient(var(--accent),var(--accent)),linear-gradient(var(--line),var(--line));
  background-repeat:no-repeat;background-position:0 100%,0 100%;background-size:0% 1px,100% 1px;
  padding-bottom:1px;
  transition:color var(--t-quick) var(--ease),background-size var(--t-state) var(--ease);
}
/* A11 2026-08-10 · Batch 6 — TEXT IDIOM: tint at --t-press, no compression. */
@media (hover:hover){ footer.fb-foot a:hover{background-size:100% 1px,100% 1px} }
footer.fb-foot a:active{background-size:100% 1px,100% 1px;transition-duration:var(--t-press)}
.fb-note{margin-top:1rem;font-size:.85rem;color:var(--faint)}

/* ENQUIRY-M1 (2026-08-16) — THE CANONICAL FORM, minimally dressed.
   This tier loads no app.js at all (the boot guard shows it precisely when the
   walkthrough could not run), so everything here is native and everything here
   is styled with properties that need nothing: no transitions worth the name, no
   hover-only affordances, no scripted state. 16px inputs and 44px controls are
   floors, not targets — this path skews to older Android, which is also where a
   sub-16px input triggers iOS-style zoom on the browsers that do it.
   PRESENTATION POLISH IS M2; this is the working machine. */
/* ENQUIRY-M2 — THE FLAT TIER, DRESSED. M1 left this functional-but-plain: boxed
   inputs with a radius and a lifted ground, which is not a register this site
   speaks anywhere. The site's data grammar is a mono label over a RULE, ink sat
   straight on the dark — the same grammar the workstation screen uses, at the
   page's own scale instead of the quad's. Nothing here becomes scripted and
   nothing becomes clever: the select stays native, the submit stays a real
   button, and the whole thing still works with JavaScript switched off.
   The 1.1rem row rhythm is the fallback's own (.fb-stages, .fb-promises). */
.fb-form{display:grid;gap:1.1rem;margin-top:2.6rem;max-width:52ch}
/* F4 — the honeypot's WRAPPER is hidden, off-canvas rather than display:none, so
   no autofill and no password manager reaches the field inside it. */
.fb-hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.fb-field{display:block}
.fb-label{
  display:block;font-family:var(--mono);font-size:11px;letter-spacing:0.16em;
  text-transform:uppercase;color:var(--faint);margin-bottom:.4rem;
}
.fb-label i{font-style:normal;opacity:.7}
/* 16px is a floor, not a size: below it the browsers that zoom on focus zoom,
   and this tier skews to exactly those. 44px likewise — the padding is what
   holds the box open, since an underlined field has no border to do it. */
.fb-input{
  display:block;width:100%;min-height:44px;padding:.55rem 0;
  font-family:var(--body);font-size:16px;line-height:1.4;color:var(--ink);
  background:none;border:none;border-bottom:1px solid var(--line);border-radius:0;
  transition:border-bottom-color var(--t-quick) var(--ease);
}
/* the underline IS the focus ring — drawn to accent and doubled by a 1px shadow
   on the same row, so focus is a weight change and not a box over the page */
.fb-input:focus{outline:none;border-bottom-color:var(--accent)}
.fb-input:focus-visible{outline:none;border-bottom-color:var(--accent);box-shadow:0 1px 0 0 var(--accent)}
.fb-textarea{resize:vertical;min-height:88px;line-height:1.5}
/* a native select keeps the platform's own picker — the point of this tier. The
   only honest addition is telling the platform what ground it is drawing on, so
   the popup and the disclosure arrow come back dark instead of system-white. */
.fb-select{
  appearance:auto;-webkit-appearance:auto;
  color-scheme:dark;padding-right:.2rem;
}
.fb-privacy{font-size:.9rem;line-height:1.55;color:var(--faint)}
/* a real destination, so a real 44px box — grown by a pseudo, the site's own
   .nav-cta / .fl-privacy idiom, because raising the line box would push the
   submit down. inline-block so the two-word label cannot split across lines and
   hand the extender the union of both fragments; ±12px keeps it clear of the
   1.1rem grid gap above and below, so it steals nothing. */
.fb-privacy a{
  display:inline-block;position:relative;color:var(--ink);
  background-image:linear-gradient(var(--accent),var(--accent)),linear-gradient(var(--line),var(--line));
  background-repeat:no-repeat;background-position:0 100%,0 100%;background-size:0% 1px,100% 1px;
  padding-bottom:1px;
  transition:background-size var(--t-state) var(--ease);
}
.fb-privacy a::after{content:"";position:absolute;inset:-12px -6px}
@media (hover:hover){ .fb-privacy a:hover{background-size:100% 1px,100% 1px} }
.fb-privacy a:active{background-size:100% 1px,100% 1px;transition-duration:var(--t-press)}
.fb-submit{
  justify-self:start;
  display:inline-flex;align-items:center;min-height:56px;padding:0 1.4rem;cursor:pointer;
  background:var(--accent);color:var(--carbon);border:none;border-radius:14px;
  font-family:var(--body);font-size:1.05rem;font-weight:500;
  transition:background var(--t-quick) var(--ease),transform var(--t-quick) var(--ease);
}
@media (hover:hover){ .fb-submit:hover{background:var(--accent-bright)} }
.fb-submit:active{background:var(--accent-bright);transform:scale(0.97);transition-duration:var(--t-press)}
.fb-submit:focus-visible{outline:2px solid var(--ink);outline-offset:3px}

/* ============ RESPONSIVE ============ */
@media (max-width:640px){
  /* the badge is the only on-screen answer to "which stage is this" once the
     stage title fades at p=0.14 — it used to be display:none here, leaving 86%
     of every stage unlabelled on the majority device. Number only, so it fits
     alongside the persistent CTA. */
  .stage-badge .sb-name{display:none}
  .brand-sub{display:none}          /* buys the width the CTA + badge need */
  .nav-cta-long{display:none}
  .nav-cta-short{display:inline}
  .nav-cta{font-size:12px;padding:.42rem .8rem}
  .hero-inner,.problem-inner,.startgate-inner,.outro-inner{
    left:var(--gutter);right:var(--gutter);max-width:none;
  }
  .stub-inner{left:var(--gutter);right:var(--gutter);text-align:left}
  .problem-note,.ledger,.cta-panel{max-width:none}
  /* email wraps if needed; padding keeps the mailto tap target ≥44px on touch */
  .outro-email{font-size:clamp(1.1rem,4.4vw,1.4rem);overflow-wrap:anywhere;padding-block:.5rem}
  /* a white scene sphere bleeds through "won't" in "where AI won't help" — the
     single most trust-critical word on the page. The shared column scrim falls
     off at the ledger's right edge, so the promise rows carry their own. */
  .promise-text{text-shadow:0 1px 3px rgba(8,9,11,0.9),0 0 14px rgba(8,9,11,0.85),0 0 26px rgba(8,9,11,0.7)}
  .picker{max-width:none}

  /* ---- stage screens: reflow for narrow / touch ---- */
  .stage .scene-markers,.stage .step-dots{display:none}   /* pins/chips would collide; hide */
  .stage-title{left:var(--gutter);right:var(--gutter);top:4.6rem;bottom:auto;text-align:left;max-width:none}
  .stage-name{font-size:clamp(2.6rem,15vw,3.6rem)}
  .stage-purpose{margin-left:0;max-width:none;font-size:.98rem}
  /* ---- MOBILE STAGE STACK — FIX 2026-07-27 ----------------------------------
     .steps, .boards and .gate were three independently bottom-anchored blocks
     (15.5rem / 8.5rem / 1.8rem) whose children are position:absolute. With
     min-height:0 each container collapsed to ZERO height, so its absolute
     children rendered *downward past its own anchor point* and straight through
     whatever sat below. Measured at 375×812 on Discover: step bottom 699 vs
     board top 676 — a 23px overlap severing the last line of every step
     mid-sentence on all four stages; and at p=0.87 the gate pill (734–786)
     rendered entirely INSIDE the board card (676–824), overprinting the "Where
     AI won't help" heading with the only forward control on the screen.
     Fix: give each container a real min-height sized for its content at 375px,
     then stack the bottom offsets so the three blocks tile instead of collide.
     Budget from the bottom: gate 1.8 + 3.1 · gap 1.2 · boards 11.5 · gap 1.4 ·
     steps 10  =  29rem. Leaves ~65px clear under the title block at 812.
     (Title and steps are never both visible — the title fades out by p=0.14 and
     step 01 arrives at p=0.18 — so a static overlap on very short viewports is
     harmless. Boards/gate DO co-exist, which is why they now tile.) */
  .steps{
    left:var(--gutter);right:var(--gutter);top:auto;transform:none;width:auto;
    bottom:calc(1.8rem + 3.1rem + 1.2rem + 11.5rem + 1.4rem);min-height:10rem;
  }
  .step-lead{font-size:clamp(1.6rem,7vw,2rem)}
  .step-line{max-width:none}
  .boards{
    left:var(--gutter);right:var(--gutter);top:auto;transform:none;width:auto;
    bottom:calc(1.8rem + 3.1rem + 1.2rem);min-height:11.5rem;
  }
  .board{width:auto}
  /* F3m — the mobile camera frames the room tighter and brighter (measured scene
     luminance spikes to ~0.79 behind hero-sub, ~0.56 behind startgate-sub); the
     shared scrim profile alone leaves worst-pixels under AA there. Boost only
     these two columns, only on mobile — same feathered depth-of-field read. */
  .hero-inner::before,.startgate-inner::before{
    background:radial-gradient(ellipse 85% 80% at 50% 50%,
      rgba(8,9,11,0.72) 0%,
      rgba(8,9,11,0.72) 30%,
      rgba(8,9,11,0.65) 45%,
      rgba(8,9,11,0.55) 60%,
      rgba(8,9,11,0.40) 72%,
      rgba(8,9,11,0.25) 82%,
      rgba(8,9,11,0.12) 90%,
      rgba(8,9,11,0.04) 96%,
      rgba(8,9,11,0.00) 100%);
  }
  .gate{bottom:1.8rem}
  .gate-pill{padding:.35rem .9rem .35rem .35rem}
  .gate-ring{width:38px;height:38px}
  .gate-ring svg{width:38px;height:38px}

  /* ---- MENU OVERLAY, mobile (M1, 2026-08-12) ----
     One column, and the whole interactive mass anchored to the LOWER TWO-THIRDS
     where a thumb reaches. The survey card collapses to a compact position line
     ABOVE the ledger — eyebrow + position only: no descriptor (nothing hovers on
     a touch device to drive it) and no pin/hairline (a cyan annotation needs
     room to point at, and there is none at 390px). The stage ledger stays
     stacked; every target keeps ≥44px with ≥6px of separation; the hamburger-X
     stays top-right (Batch 4 ruling, untouched). */
  .menu-panel{
    grid-template-columns:1fr;align-content:end;row-gap:clamp(.8rem,2.5vh,1.4rem);
    padding:clamp(4rem,10vh,6rem) var(--gutter) var(--gutter);
  }
  .menu-zone-r{order:-1;justify-content:flex-end}
  .menu-zone-l{justify-content:flex-end}
  .menu-survey{
    max-width:none;background:none;border:0;border-bottom:1px solid var(--line);
    border-radius:0;-webkit-backdrop-filter:none;backdrop-filter:none;
    padding:0 0 .85rem;
  }
  .ms-desc,.menu-pin{display:none}
  .ms-position{margin:.6rem 0 0}
  .menu-stages .menu-item{font-size:clamp(1.9rem,9vw,2.6rem);padding:.45rem 0}
  /* ≥6px between adjacent hit boxes (A3). The rows are 44px boxes around type
     that is much shorter than that, so the separation is authored rather than
     inherited — on both tiers, since stacked rows otherwise abut exactly. */
  .menu-zone-l > .menu-item{margin-bottom:8px}
  .menu-stages{gap:8px}
  .menu-info{margin-top:.6rem}

  /* ---- EXPLAINER CARD, narrow (M2) ---- */
  .modal-title{padding-right:0}          /* the eyebrow already clears the × here */
  .ml-row{grid-template-columns:2rem 1fr;gap:.7rem}
}

@media (prefers-reduced-motion:reduce){
  .w,[data-reveal],.cta-pill,.gate-ring-fill,.gate-dot,#veil,.brand,
  .promise,.outro-cta,.cta-panel,.cta-panel::before,.cta-panel-label,.cta-panel-arrow,
  .outro-email,.menu,.menu-item,.menu-lead,.modal,.modal-card,
  .picker-readback,.pick-chip,.pre-bar-fill,
  /* Batch 2/3 — see the html.reduced block above for the reasoning; these two
     lists are the same authored layout reached by the media query instead. */
  .gate-pill,.gate-label,.nav-cta,.hamburger i,.copy-email,.copy-email .ce-label,
  .modal-close,.modal-close .mc-glyph,.menu-info,.enter-alt,.enter-alt .ea-line,
  /* Batch 4/5 — the same authored second layout as the html.reduced block. */
  .stage-badge,.enter-cta,.enter-dot,#preloader,
  /* Batch 6/7 — the fallback path and the converged link sweeps. This is the
     load-bearing home for .fb-cta and footer.fb-foot a: the fallback can be
     served with no JS at all, so html.reduced may never be written. */
  /* M1/M2 — the menu's and the card's authored finals, reached by the media
     query instead. */
  #scene,#screens,.menu-lead::before,.menu-cta,.menu-cta::before,.ms-desc,
  .menu-panel,.hamburger,
  /* ENQUIRY-M1 — .fb-submit joins them for the same reason: it is a control on
     the no-JS tier, so this media query is the only home it can be given. */
  /* ENQUIRY-M2 — the dressed flat tier adds two more: the field underline and
     the privacy link's sweep. Same argument, same home — with no JS there is no
     html.reduced, so this media query is the only place they can be told. */
  .fb-cta,.fb-submit,.fb-input,.fb-privacy a,footer.fb-foot a{transition:none !important}
  .ms-dot,.hamburger i{animation:none !important}
  .fb-cta:active,.fb-submit:active{transform:none !important}
  .cue,.gate:not(.is-full) .gate-ring,
  .gate.is-full .gate-dot,.pick-chip.is-land[aria-pressed="true"],
  .copy-email.done,
  .enter-dot.is-spark,.pre-bar.is-spark{animation:none !important}
  /* the cue's float is now a JS-written `translate` (see the note at the old
     gm-cue-float site) — the media-query path pins it back to rest too, so a
     setting flipped mid-session cannot leave the cue parked off its line. */
  .cue{translate:none !important;will-change:auto !important}
  .gm-draft,.menu.is-open .gm-draft,.modal.is-open .gm-draft{
    animation:none !important;clip-path:none !important;translate:none !important;
  }
  html.pre-hold .enter-word,html.pre-hold .enter-line,html.pre-hold .enter-sub,
  html.pre-hold .enter-offer,html.pre-hold .enter-cta,html.pre-hold .enter-alt,
  .enter-word,.enter-line,.enter-sub,.enter-offer,.enter-alt{
    animation:none !important;opacity:1;translate:none !important;filter:none !important;
  }
  /* the CTA keeps its own dimmed/ready opacity — only the animation goes */
  .enter-cta{animation:none !important;translate:none !important;filter:none !important}
  .gate.is-full .gate-dot{transform:scale(1.5);box-shadow:0 0 12px rgba(255,77,0,0.9)}
  .brand:active,.nav-cta:active,.cta-pill:active,.pick-chip:active,.copy-email:active,
  .gate-pill:active,.menu-cta:active,
  .modal-close:active .mc-glyph{transform:none !important}
  .hamburger:active i{scale:none !important}
  .enter-cta:active{transform:translate(-50%,-50%) !important}
  .hero-inner .cta-pill:active,.cta-panel:active{scale:none !important}
  html.state-hero .hero-head .w,
  html.state-hero .hero-sub,
  html.state-hero .hero-inner .cta-pill{animation:none !important;opacity:1;transform:none;filter:none}
}

/* ============================================================
   M3 AA REMEDIATION (2026-08-06)
   M3 gave the diorama architecture, glazing and a sky with a time of day. Where
   the void used to sit behind the fixed chrome and the Enter overture, there is
   now a lit wall, a ceiling or a daylit window — the brand lockup went from
   16.11:1 (black) to 3.55:1 in one milestone. Nothing about the copy, the
   layout or any colour token changes here: this is the SAME stroke-local carbon
   backing the M1 remediation above documents, applied to the blocks the new
   backdrop reached. n coincident shadows composite to 1-(1-a)^n, so the backing
   is effectively opaque within ~2px of the stroke and fully feathered by ~16px;
   it never reads as a panel over the dark room, and it moves nothing, so CLS is
   byte-identical (measured 0.003561 before and after).

   Measured the same way as M1: glyph-mask, p95 background luminance, 476 blocks
   (9 screens x p {0.15,0.50,0.85} x desktop 1440x900 + mobile 375x812).
   ============================================================ */
/* the M1 stack, as one reusable declaration */
.brand-word,.brand-sub,
.stage-badge .sb-num,.stage-badge .sb-name,
.enter-line,.enter-sub,.enter-offer{
  text-shadow:
    0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1),
    0 0 2px rgba(8,9,11,1),
    0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1),
    0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1),
    0 0 2px rgba(8,9,11,1),
    0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1),
    0 0 3px rgba(8,9,11,1), 0 0 5px rgba(8,9,11,1),
    0 0 10px rgba(8,9,11,0.97), 0 0 18px rgba(8,9,11,0.9),
    0 1px 3px rgba(8,9,11,1);
}
/* the badge's ring is only ~22px across, so a 2px glyph dilation crosses it onto
   whatever the scene put behind the chrome. The chip fill stays exactly as M1
   set it; the stack above is what carries the numeral's own 2px halo. */
.stage-badge .sb-num{background:var(--carbon)}
/* hero lead on mobile: the copy column overlays the model directly at 375px, and
   the curtain wall's dawn light now lands behind its first lines. The lead words
   live in .hero-head and carried NO backing at all — the void behind them was
   doing the whole job. Large text (3:1), so a lighter stack is enough. */
.hero-head .w.lead,.hero-sub{
  text-shadow:0 0 3px rgba(8,9,11,1),0 0 3px rgba(8,9,11,1),0 0 6px rgba(8,9,11,0.99),
              0 0 16px rgba(8,9,11,0.92),0 1px 3px rgba(8,9,11,0.98);
}
/* Text-shadow alone cannot carry three of these blocks, for one reason each, and
   the escalation is the M1 file's own: chip-backing, then a feathered pocket.

   .sb-num — the numeral fills its 22px pill almost edge to edge, so the glyph's
   own 2px halo lands on the ORANGE ring and then on whatever the scene put
   beyond it. The chip fill is right; it is simply 2px too small now that "beyond
   it" is a daylit window. Extend the carbon past the ring with a spread shadow:
   box-shadow takes no space, so nothing moves and CLS is untouched. */
.stage-badge .sb-num{box-shadow:0 0 0 3px var(--carbon)}

/* .brand's pocket — same feathered idea as .step-eyebrow::before, one line tall.
   Over the dark acts (hero, plan, outro) it is invisible; it earns its keep on the
   acts where the arc put daylight behind the chrome.

   M2 · ITEM 1 — THE TRUNCATION IS FIXED HERE AND THE ENTER PACK IS GONE.
   The shipped ellipse was `74% 100%`: with the gradient centred, a VERTICAL
   radius of 100% ends the ramp a full box-height from the centre — twice the
   distance to the edge — so the element's own box cut the ramp at alpha ≈0.76
   and the cut read as a hard line. Measured on the enter frame: .enter-sub top
   2.0 → 78.9 and bottom 55.5 → 2.0 in ONE CSS pixel; .enter-offer 9.8 → 83.8 and
   18.7 → 0.2. Four full-width seams plus six vertical ones on one frame.
   `closest-side` is the fix in one keyword: each radius becomes the distance to
   its own nearest edge, so the ramp reaches alpha 0 exactly where the box ends,
   from every direction. The stops are re-weighted to hold the depth the p95
   statistic needs (it only looks within 2px of a stroke) across a plateau
   instead of buying it from an over-long ramp, and the inset grows to give that
   plateau somewhere to land.
   The three ENTER lines no longer carry a pocket at all — they are covered by
   one authored column scrim (.enter-inner::before, below). */
.brand{position:relative}
.brand::before{
  content:"";position:absolute;z-index:-1;pointer-events:none;
  inset:-0.5rem -0.72rem -0.46rem;
  background:radial-gradient(ellipse closest-side at 50% 50%,
    rgba(8,9,11,0.90) 0%,
    rgba(8,9,11,0.89) 46%,
    rgba(8,9,11,0.80) 62%,
    rgba(8,9,11,0.55) 76%,
    rgba(8,9,11,0.28) 87%,
    rgba(8,9,11,0.08) 95%,
    rgba(8,9,11,0.00) 100%);
}

/* ------------------------------------------------------------
   M2 · ITEM 1 — THE ENTER COLUMN SCRIM
   One element for the whole overture instead of a pocket per line. It is the
   discipline the M1 hero scrim shipped with, stated once more: a vertical wash
   whose ramp begins at alpha 0 and returns to alpha 0 four percent before the
   box ends, masked horizontally by a ramp that is fully transparent six percent
   before its own edge. No ramp meets an edge anywhere, in either axis, so there
   is no row and no column at which the gradient can be cut — which is the entire
   mechanism behind the seams this replaces.
   It is sized off the copy block it serves (a negative inset), not off the
   viewport, so it follows the column at every width and on both compositions.
   ------------------------------------------------------------ */
.enter-inner::before{
  content:"";position:absolute;z-index:-1;pointer-events:none;
  inset:-2.6rem -2.2rem -2.2rem;
  background:linear-gradient(180deg,
    rgba(8,9,11,0.00) 0%,
    rgba(8,9,11,0.26) 7%,
    rgba(8,9,11,0.66) 15%,
    rgba(8,9,11,0.82) 24%,
    rgba(8,9,11,0.84) 50%,
    rgba(8,9,11,0.82) 74%,
    rgba(8,9,11,0.62) 84%,
    rgba(8,9,11,0.24) 92%,
    rgba(8,9,11,0.00) 96%);
  -webkit-mask-image:linear-gradient(90deg,
    rgba(0,0,0,0) 0%, #000 13%, #000 87%, rgba(0,0,0,0) 100%);
          mask-image:linear-gradient(90deg,
    rgba(0,0,0,0) 0%, #000 13%, #000 87%, rgba(0,0,0,0) 100%);
}

/* ============================================================
   M1-R2 (2026-08-06) — THE PROBLEM COLUMN AT LATE p
   Measured with the same glyph-mask method as the M1 block above (freeze rAF,
   drive __wt.go('problem') + __wt.scrollToP, film grain zeroed so noise cannot
   skew the p95, capture painted / colour-transparent-with-shadows, mask to the
   glyph pixels dilated 2px, p95 background luminance).

   At p≈0.80 the walkthrough has carried the camera into the room and a PALE
   BEIGE shelving end + partition fills the whole left third, directly behind the
   copy column — the brightest surface this screen ever puts behind that copy
   (measured p95 background luminance 0.19 at the eyebrow, vs 0.02 at p=0.50).
   The Item-2A column scrim cannot reach it: its ellipse is centred at 50% 54% of
   a box that runs well below the copy, so the eyebrow sits at radius > 1.0 and
   the lead word at ~0.9 — both in the tail, receiving essentially nothing. The
   answer is NOT to inflate the shared ellipse (a near-box-sized 0.7 field behind
   the column would read as a plate over the daylit room); it is the escalation
   this file already documents — near-carbon local backing, then a tight
   feathered pocket, applied per block. Pure darkening, so all of it stays
   invisible over the dark acts. Pseudo-elements at z-index:-1 and text-shadow
   only: no copy, no layout, no colour token moved, CLS untouched.
   ============================================================ */

/* the eyebrow is the worst block on the screen: 11px --dim caption sitting on the
   pale panel with no scrim left. Same near-carbon stack as the .brand lockup.
   The stack alone gets this block to 4.39 and no further: at 11px with 0.22em
   tracking the mono strokes are thin and widely spaced, so the p95 keeps finding
   panel between the letters. It therefore also takes a pocket — but NOT the
   tight 0.90-plateau one .brand/.step-eyebrow carry (tried first, rejected on
   sight: on a short wide isolated caption that plateau rendered as a distinct
   dark chip around the words, and this family's rule is "no panel, no visible
   edge"). This one is the Item-2A depth (0.55-ish peak) over a wider box with a
   long feather, so it reads as the same soft shadow the column already sits in. */
.problem-inner .eyebrow{
  position:relative;
  text-shadow:
    0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1),
    0 0 2px rgba(8,9,11,1),
    0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1),
    0 0 3px rgba(8,9,11,1), 0 0 5px rgba(8,9,11,1),
    0 0 10px rgba(8,9,11,0.97), 0 0 18px rgba(8,9,11,0.9),
    0 1px 3px rgba(8,9,11,1);
}
.problem-inner .eyebrow::before{
  content:"";position:absolute;z-index:-1;pointer-events:none;
  /* radii are HALF the padded box on both axes, so the gradient reaches alpha 0
     exactly at the pseudo-element's own edge — the wider ellipse fractions the
     other pockets use are fine over the dark room but leave a faint straight cut
     where this one crosses a flat pale panel. */
  inset:-1.5rem -3.4rem;
  background:radial-gradient(ellipse 50% 50% at 50% 50%,
    rgba(8,9,11,0.56) 0%,
    rgba(8,9,11,0.56) 38%,
    rgba(8,9,11,0.50) 50%,
    rgba(8,9,11,0.40) 62%,
    rgba(8,9,11,0.28) 73%,
    rgba(8,9,11,0.16) 82%,
    rgba(8,9,11,0.07) 90%,
    rgba(8,9,11,0.02) 96%,
    rgba(8,9,11,0.00) 100%);
}
/* the lead word is ORANGE, and #ff4d00 at luminance 0.264 needs the background
   under it below 0.046 even for the 3:1 large-text bar — unreachable from above,
   so it takes the same near-carbon stack as .step-eyebrow / .promise-num. It is
   one word, so the halo stays local and reads as weight, not as a box. The
   headline's other words already clear 3:1 with room and are left as shipped. */
.problem-head .w.lead{
  text-shadow:
    0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1), 0 0 2px rgba(8,9,11,1),
    0 0 2px rgba(8,9,11,1),
    0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1),
    0 0 3px rgba(8,9,11,1), 0 0 5px rgba(8,9,11,1),
    0 0 10px rgba(8,9,11,0.97), 0 0 20px rgba(8,9,11,0.9),
    0 1px 3px rgba(8,9,11,1);
}
/* body + illustrative note: --dim over the pale panel, short of AA by 0.4 and 1.0
   respectively. These are paragraphs, not captions, so they take the LIGHT stack
   (.brand-word family) rather than the opaque one — enough depth within 2px of a
   stroke, still fully feathered by 14px, no outlined-text look. The shipped broad
   0 1px 24px glow is kept as the last layer. */
.problem-body,.note-illus{
  text-shadow:
    0 0 3px rgba(8,9,11,1), 0 0 3px rgba(8,9,11,1),
    0 0 6px rgba(8,9,11,0.99), 0 0 14px rgba(8,9,11,0.92),
    0 1px 3px rgba(8,9,11,0.96), 0 1px 24px rgba(8,9,11,0.85);
}

/* ============================================================
   A11 COARSE-POINTER SWEEP (2026-08-10)
   13 of the file's 14 :hover rules were unguarded — .cta-panel (above) was the
   only one behind @media (hover:hover). On a touch device a tapped element keeps
   :hover until the next tap elsewhere, so .nav-cta stayed filled orange after a
   tap, .pick-chip became indistinguishable from its own aria-pressed state, and
   .outro-email held a full underline sweep: a false "selected" reading on the two
   screens that decide the sale.
   Pattern is .cta-panel's, already in the file: the hover rule goes inside
   @media (hover:hover), and an unguarded :active mirror keeps press feedback on
   touch. Requires the no-op touchstart listener at app.js boot, or iOS Safari
   fires no :active at all and the guard would remove feedback rather than fix it.

   EDITED IN PLACE, not appended as an override block — a deliberate departure
   from this file's convention. The dated blocks above (M1, M3, M1-R2) are all
   ADDITIVE: text-shadow stacks and ::before pockets, which append cleanly because
   a later declaration simply wins. Guarding a selector is SUBTRACTIVE — an
   appended @media (hover:hover) does not un-apply the original rule on touch. The
   only appendable equivalent would restate every base value inside
   @media (hover:none), duplicating the tokens three remediation blocks have
   worked to keep single-sourced. Each edited site carries an "A11 2026-08-10"
   marker comment. The 14 rules: .nav-cta (twice — base and the M1 AA override),
   .hamburger, .cta-pill,
   .enter-alt, .pick-chip, .outro-email, .copy-email, .menu-info, .menu-item,
   .menu-cta, .modal-close, .fb-cta, footer.fb-foot a.

   :focus-visible was NOT added per-rule: the global rule near the top of this file
   (2px --accent outline, 3px offset) already covers every focusable element, and
   doubling it with hover styling adds no legibility. .menu-item stays the single
   exception and keeps its existing pairing — its hover is a positional state
   indicator (the lead bar marks position in the list), not decoration — with the
   :focus-visible half deliberately OUTSIDE the hover query.
   Colour, type and layout untouched; CLS unaffected.
   ============================================================ */
/* ============================================================
   === OVERLAY PREVIEW (M1 · DELTA — removable per RISK-REGISTER) ===
   The ONE CSS block the demotion clause removes. It is a single state: while a
   stage entry is being previewed, the parked room racks part-way back toward
   focus — never fully sharp, so the preview stays "behind glass" and cannot
   out-shout the gate ignition (D4).
   All three filter functions are present here and in the night-depth park, in
   the same order: CSS only interpolates filter lists of matching length and
   order, so a missing function snaps the rack instead of easing it (measured on
   the P2 rig). Zero CLS by construction — filter takes no layout.
   Demotion = delete this block, delete the app.js module of the same name, bump
   the busters. No BASE rule reads html.menu-preview.
   ============================================================ */
/* THE RACK RIDES THE GLIDE (M1 tune, 2026-08-12). Entering preview the filter
   transitions over the CONDUCTOR's own duration, so the destination room
   *surfaces* across the whole assembly instead of clearing in 300ms and then
   being watched. Leaving, it drops back to the base --t-state on #scene — enter
   slow, leave quick, the estate's asymmetry rule, expressed the way .menu and
   .modal express it: the base rule carries the exit, the state rule carries the
   entrance.
   --t-glide is written onto #scene by the module below from its own GLIDE
   constant, so the duration has exactly ONE source and deleting the module takes
   it with it (the fallback keeps this rule valid meanwhile). */
html.menu-preview #scene{
  filter:blur(2px) brightness(0.8) saturate(0.85);
  transition-duration:var(--t-glide, var(--t-state));
}
/* === /OVERLAY PREVIEW (M1 · DELTA) === */

/* ============================================================================
   M1 · PORTRAIT HERO — "LEANING OVER THE BENCH"            (2026-08-13)
   The phone hero becomes one continuous column on the site gutter, read against
   a lean-over shot of the model: seven words, one per line, height-fitted, with
   the first three standing BEHIND the room's wall as in-scene type (scene.js).

   Why the sizing is written this way: the line box is derived from viewport
   height at first layout and never re-derived, so the stack reserves its full
   height on the first paint — no late font-size step, no reflow, CLS stays 0.
   The reserve (390px) is the sum of the parts the stack must not eat: the
   chrome zone at the top, the sub + CTA rhythm under it, and the cue's band at
   the bottom. Gated to portrait AND ≤900px so it lands with scene.js's own
   portrait-hero gate — the two must switch together or the in-scene words and
   the DOM words would describe different layouts.
   ============================================================================ */
@media (orientation:portrait) and (max-width:900px){
  .hero-inner{
    left:var(--gutter); right:auto; top:0; transform:none;
    width:min(62%, 22rem); max-width:none;
    /* the line box and the column's top offset are declared HERE, not on
       .hero-head, so the copy scrim below can be positioned off the same two
       numbers the type is set from. Custom properties inherit downward; the
       computed size of every rule that used them is unchanged. */
    --lh:clamp(24px, calc((100vh - 480px) / 7), 76px);
    --hpad:clamp(96px, 20.4vh, 190px);
    /* 12.56vh is not a taste number: it is where the stack has to sit for the
       room's own wall to cut "your" to ~73% at p0 — the resting state the
       occlusion arc starts from, measured with heroProbe(). Clamped so it still
       clears the chrome on short viewports. */
    padding-top:var(--hpad);
  }
  .hero-head{
    font-size:calc(var(--lh) * 0.86);
    line-height:1; letter-spacing:-0.035em; margin-bottom:0;
  }
  .hero-head .w{
    display:block; height:var(--lh); line-height:var(--lh);
    white-space:nowrap;
  }
  /* the behind-words are REAL DOM TEXT at every tier — the sentence is in the
     document, in order, for a screen reader and for a visitor whose WebGL never
     starts. Their ink is suppressed only while the scene is live, because that
     is the only case where the same three words are on screen as geometry.
     opacity (not visibility/display) keeps them in the accessibility tree and,
     just as importantly, keeps their line boxes in the layout the in-scene
     planes are measured from. */
  /* M1b · item 3 — and `transform:none`. The base .w rule parks every word at
     translateY(0.42em) for the arrival to animate off; the behind-words have that
     arrival suppressed, so they were left sitting 0.42em (18.8px at 390) BELOW
     their line. The in-scene planes are measured from these boxes, so the whole
     trio rendered 18.8px low and the gap between "best" and "people" closed to
     33px against the stack's own 52px. One rhythm across all seven lines needs
     the suppressed words to hold their line, not the arrival's start pose. */
  html:not(.fb-on) .hero-head .w.behind{opacity:0;transform:none;animation:none !important}

  .hero-sub{
    max-width:30ch; font-size:16px; line-height:1.5;
    margin:calc(var(--lh) * 0.34) 0 0;
  }
  .hero-inner .cta-pill{margin-top:1rem}
  /* THE LOWER SCRIM (M1b · item 5).
     The shared radial scrim cannot serve this shot: it sits above the canvas and
     below the DOM text, so centred on the column it darkened the three IN-SCENE
     words to about half the ink of the four DOM ones — the two halves of one
     headline stopped matching. M1 retired it outright, which left "their" and
     "week" over the whiteboard and the monitor glow.
     So it is re-cut as a one-directional wash that begins BELOW the wall line —
     its top edge is the top of word four, computed from the same --hpad/--lh the
     stack is set from, so it can never reach the plane words above it — and runs
     under the sub and the CTA. It stops at 55vw: the lit desk pools right of
     centre are untouched.
     Every ramp COMPLETES inside the box: the vertical gradient is at alpha 0 by
     4% from the bottom and starts at 0 at the top; the horizontal mask is fully
     transparent by 94% of the width. No edge of this element can be seen — the
     failure mode the enter screen's copy pockets shipped with. */
  [data-screen="hero"] .hero-inner::before{
    inset:auto auto auto calc(-1 * var(--gutter));
    top:calc(var(--hpad) + 3 * var(--lh));
    width:55vw; height:calc(4.4 * var(--lh) + 15rem);
    background:linear-gradient(180deg,
      rgba(8,9,11,0) 0%,
      rgba(8,9,11,0.30) 8%,
      rgba(8,9,11,0.58) 18%,
      rgba(8,9,11,0.62) 60%,
      rgba(8,9,11,0.44) 80%,
      rgba(8,9,11,0.16) 90%,
      rgba(8,9,11,0) 96%);
    -webkit-mask-image:linear-gradient(90deg, #000 0 58%, rgba(0,0,0,0) 94%);
            mask-image:linear-gradient(90deg, #000 0 58%, rgba(0,0,0,0) 94%);
  }
  /* the cue joins the same rail rather than centring under a left-aligned column.
     It used to need a second keyframe (gm-cue-float-rail) because the centred
     float hard-coded translateX(-50%) and an animation always beats a plain
     declaration. The float is now a JS-written `translate` (2026-08-14, perf —
     see the note at the old gm-cue-float site), which composes ahead of
     `transform` instead of replacing it, so both placements share one writer and
     the rail keyframe is gone. */
  [data-screen="hero"] .cue{
    left:var(--gutter); right:auto; transform:none; bottom:1.5rem; text-align:left;
  }

  /* ==========================================================================
     M2 · ITEM 2 — THE ENTER COLUMN, ONE FLOW
     The frame used to hang off two unrelated viewport anchors: .enter-inner at
     top 35% and .enter-cta at top 77%. Nothing tied them together, so the gap
     between the offer paragraph and the CTA was whatever the viewport made it —
     measured 149.9px at 390x700, 158.2 at 320, 210.3 at 390x844, 259.4 at 430.
     A frame that grows a hole as the phone gets taller is not composed.
     It is now ONE flex column on the screen itself (no markup change: the CTA and
     the skip link were already siblings of the lockup, kept outside it so the
     lockup's transform could not capture them — with the transform gone that
     constraint goes too, and they simply become the last two items in the flow).

     The column is anchored from the BOTTOM and holds together as one block:
       · `margin-top:auto` on the lockup collapses every pixel of slack ABOVE the
         overture, so the free height a tall phone brings lands over the room —
         which is the picture — instead of opening a hole inside the copy;
       · from there down, every gap is authored and CONSTANT: --e-gap to the CTA,
         15px to the skip link. The offer→CTA distance no longer knows how tall
         the device is (it was 149.9 → 259.4 across the range; it is now one
         number at every width);
       · the CTA and the skip link are positioned from the BOTTOM padding alone,
         so their position does not depend on how tall the copy above them turns
         out to be — which is what keeps the CTA registered on the scene, and what
         keeps a late font metric from moving it (zero CLS).
     --e-bot is derived, not chosen: the aisle's floor light pool centres at
     74.7-75.2% of viewport height at every phone viewport measured (320x720,
     390x844, 430x932, 390x700 — it is a property of enterView's camera, not of
     the layout). Solving for "CTA centre sits on the pool centre" gives
     25vh - (half the pill + the authored gap + the skip line), which is what is
     written below. The registration that used to be a coincidence at 390 is now
     the rule the column is built from.
     ========================================================================== */
  [data-screen="enter"]{
    display:flex; flex-direction:column; align-items:center;
    padding:var(--e-top) var(--gutter) var(--e-bot);
    /* a floor, not an anchor: it only bites on a viewport too short for the column */
    --e-top:clamp(56px, 9vh, 96px);
    --e-gap:7rem;
    /* 22.75px = half the pill · 15px = CTA→skip gap · 18px = the skip line box */
    --e-bot:calc(25vh - 55.75px);
    text-align:center;
  }
  [data-screen="enter"] .enter-inner{
    position:relative; left:auto; top:auto; right:auto; transform:none;
    width:min(100%, 33rem); max-width:none; flex:0 0 auto;
    margin-top:auto;                    /* every pixel of slack collapses ABOVE the copy */
    margin-bottom:var(--e-gap);
  }
  /* the column IS the frame at this width, so the scrim's horizontal ramps have
     nothing to feather against — they only put a brighter strip back at each
     screen edge. The box is widened until both ramps land outside the viewport;
     the wash then reads as one even darkening across the frame, and the vertical
     ramps (the ones that ever mattered) are untouched. Desktop keeps the mask:
     there the 33rem column really does sit in a wider room. */
  [data-screen="enter"] .enter-inner::before{inset:-2.6rem -25vw -2.2rem}
  [data-screen="enter"] .enter-cta{
    position:relative; left:auto; top:auto; transform:none;
    margin-top:0; flex:0 0 auto;
  }
  /* the press restates the WHOLE transform, and two reduced-motion rules pin the
     old centring one with !important (html.reduced and the media-query twin). In
     a flow column there is no centring left to keep, so both are answered here —
     higher specificity, later in source, and the reduced tier keeps its own
     no-compression law. */
  [data-screen="enter"] .enter-cta:active{transform:scale(0.97) !important}
  html.reduced [data-screen="enter"] .enter-cta:active{transform:none !important}
  @media (prefers-reduced-motion:reduce){
    [data-screen="enter"] .enter-cta:active{transform:none !important}
  }
  [data-screen="enter"] .enter-alt{
    position:relative; left:auto; top:auto; transform:none;
    margin-top:15px; flex:0 0 auto; max-width:100%;
  }
  /* the overture's own rhythm, authored rather than inherited from three
     unrelated margins: the lockup, the tagline, the offer. */
  .enter-mark{margin-bottom:1.15rem}
  .enter-offer{margin-top:.85rem}
}

/* ============================================================
   ENQUIRY-M1 (2026-08-16) · "THE FIRST LOOK" — THE DIEGETIC SCREEN

   No popups and no floating cards, so there is no card here. .fl-screen IS the
   workstation monitor's screen — app.js writes --fl-x/--fl-y/--fl-w/--fl-h once,
   at the instant the camera parks face-on to it, and the surface is the whole
   container. No border, no backdrop-filter, no radius but the screen's own
   corner: the material is a dark screen ground with the panel's own glow washing
   down from the top edge.

   Type is --mono throughout — this is a works order open on a workstation, and
   the mono/underline register is the site's own data grammar.

   THE SCREEN IS THE SCALE. Every size inside is a fraction of --fl-w and then
   clamped, so one authored layout serves a 390px-wide screen quad on a phone and
   an ~860px one on a desktop without a second layout — and the clamp floor is
   what guarantees a 16px rendered input at every viewport (the quad is placed in
   CSS pixels, never transform-scaled, so rendered == CSS here).

   ZERO CLS BY CONSTRUCTION: fixed root → absolute screen → absolute pages, so
   nothing here is in document flow; pages swap by [hidden]; the failure slip
   keeps its box reserved; and the screen stays visibility:hidden until the
   camera has parked and the rect has been written.

   NO NEW :root TOKENS — the :root block is mirrored into 404.html, so every
   custom property this surface needs is scoped to .fl-screen.
   ============================================================ */
.fl{
  position:fixed;inset:0;z-index:55;
  opacity:0;pointer-events:none;
  transition:opacity var(--t-quick) var(--ease);
}
.fl[hidden]{display:none}
.fl.is-open{opacity:1;pointer-events:auto;transition-duration:var(--t-state)}
/* the walk's own copy steps aside; the ROOM does not (unlike the menu park) */
html.fl-open #screens{opacity:0;transition:opacity var(--t-quick) var(--ease)}

/* ---- THE SCREEN. Registered to the projected quad; nothing else places it. ---- */
.fl-screen{
  --fl-x:0px; --fl-y:0px; --fl-w:0px; --fl-h:0px;
  /* one type scale, driven by the screen's own width */
  --fl-pad:clamp(10px,calc(var(--fl-w) * 0.028),26px);
  --fl-t-input:clamp(16px,calc(var(--fl-w) * 0.0215),20px);
  --fl-t-label:clamp(10px,calc(var(--fl-w) * 0.0125),13px);
  --fl-t-head:clamp(15px,calc(var(--fl-w) * 0.028),26px);
  --fl-t-body:clamp(11px,calc(var(--fl-w) * 0.0155),15px);
  /* the bar is a REAL 44px box, not a slim strip with a padded label: the exit
     is a control on a projected quad, and the quad is placed in CSS pixels, so
     44 here is 44 rendered (the prototype's lesson: no ::after extenders, which
     overlap at small row gaps and let the later chip steal the hit) */
  --fl-bar:44px;
  position:absolute;
  left:var(--fl-x);top:var(--fl-y);width:var(--fl-w);height:var(--fl-h);
  overflow:hidden;pointer-events:auto;
  /* 2026-08-17 — was a raw #080b0e: a THIRD near-black, matching neither
     --carbon (#08090b) nor --soft (#0e1013), with no rationale on record. The
     screen ground is the estate's ground; the cool tint this surface reads with
     is the cyan wash on the line below, which is where it is authored. Resolved
     to the token — the delta was (0,2,3)/255, so nothing on this surface moves. */
  background:var(--carbon);
  background-image:linear-gradient(rgba(168,212,228,0.055),rgba(168,212,228,0) 42%);
  color:var(--ink);
  font-family:var(--mono);
}
/* until the camera has parked and the rect is written there is nothing to show,
   and an unplaced box must never be a layout-shift source */
.fl:not(.is-open) .fl-screen{visibility:hidden}

/* ---- THE SCREEN'S HEADER BAR — exit left, section counter right ---- */
/* HIT ORDER. .fl-bar is the FIRST positioned child of .fl-screen and carries no
   z-index, so every later positioned sibling paints over it and eats the
   pointer: .fl-form is `absolute; top:0; bottom:0` and covers the whole screen
   including this strip, and in portrait .fl-page is `top:0` too (it reserves the
   shared control row with padding, which does not shrink its box). Both are
   transparent there, so the exit stays visible and looks live while never
   receiving a hit — and JS .click() verification passes it, because that skips
   hit-testing entirely. Escape and Enter-on-focus were never affected.

   The fix is the site's own .screen / .screen-inner idiom, not a box change: the
   bar strip stops taking hits and only its one real control is raised. Raising
   the whole bar instead would cover .fl-foot, which shares this row in portrait.
   Nothing here moves anything — zero CLS by construction is intact. */
.fl-bar{
  position:absolute;left:0;right:0;top:0;height:var(--fl-bar);
  display:flex;align-items:stretch;justify-content:space-between;
  padding:0 var(--fl-pad) 0 0;
  border-bottom:1px solid rgba(168,212,228,0.16);
  pointer-events:none;
}
.fl-exit{
  position:relative;z-index:2;pointer-events:auto;
  display:inline-flex;align-items:center;min-height:var(--fl-bar);
  padding:0 var(--fl-pad);
  background:none;border:none;cursor:pointer;color:var(--faint);
  font-family:var(--mono);font-size:var(--fl-t-label);letter-spacing:0.12em;text-transform:uppercase;
  transition:color var(--t-quick) var(--ease);
}
@media (hover:hover){ .fl-exit:hover{color:var(--accent)} }
.fl-exit:active{color:var(--accent);transition-duration:var(--t-press)}
/* ENQUIRY-M2 — the ruling's own words for the corner exit: mono, --faint,
   accent on focus/active. Focus carried an outline and nothing else; the ink
   now moves with it, so the control reads live to a keyboard the way it already
   did to a hand. Both exits, one grammar — there is only one control here. */
.fl-exit:focus-visible{color:var(--accent)}

/* ---- THE PORTRAIT CORNER EXIT (markup and reasoning in index.html) ----
   display:none everywhere the ruling does not apply — every landscape viewport,
   and portrait section 01, which keeps the shared row. display:none rather than
   a relocated bar is what makes the swap free: a box that is not rendered in the
   previous frame has no previous position, so no frame of this is a shift. */
.fl-strip{display:none}
/* the counter's own box is RESERVED. "03 / 03" → "BOOKED" is 8px narrower, and a
   flex-end item that changes width moves itself: measured 9.73e-06 of CLS
   charged to a successful booking. Widest label wins, right-aligned. */
.fl-count{
  display:inline-flex;align-items:center;justify-content:flex-end;
  min-width:calc(7ch + 7 * 0.16em);
  font-size:var(--fl-t-label);letter-spacing:0.16em;color:rgba(168,212,228,0.62);
}

/* ---- THE PAGES. Absolute inside the screen, so a page swap can move nothing
   that is already painted. The FOOT is always visible; only the BODY scrolls,
   which is the safety net for a short quad (portrait) and for any viewport this
   was not measured at. ---- */
.fl-form{position:absolute;left:0;right:0;top:0;bottom:0}
.fl-page{
  position:absolute;left:0;right:0;bottom:0;top:calc(var(--fl-bar) + 1px);
  display:flex;flex-direction:column;
  padding:calc(var(--fl-pad) * 0.7) var(--fl-pad) calc(var(--fl-pad) * 0.6);
}
.fl-page[hidden]{display:none}
/* The body is the only scroller on the screen. A 16:9 quad is a short box and
   section 03 does not fit it at 390x844, so the last line is masked out rather
   than guillotined — the fade is the affordance that says there is more below.
   Static mask, no animation: a promoted-but-still layer over the canvas is free
   (the cueFloat measurement), a moving one is not. */
.fl-body{
  flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;
  /* the one thing on this surface the platform would otherwise draw in its own
     colours: a classic light scrollbar down the inside of a night monitor. The
     site does not theme scrollbars anywhere else because nowhere else is a
     scroller INSIDE a rendered object. Standard properties only — thin, and in
     the screen's own two inks. */
  scrollbar-width:thin;scrollbar-color:rgba(168,212,228,0.28) transparent;
  -webkit-mask-image:linear-gradient(#000 calc(100% - 14px),transparent);
  mask-image:linear-gradient(#000 calc(100% - 14px),transparent);
}
.fl-page.is-in{animation:gm-draft-in var(--t-state) var(--ease) backwards}
.fl-page.is-out{animation:gm-draft-in var(--t-quick) var(--ease) reverse forwards}

.fl-head{
  font-family:var(--mono);font-weight:500;font-size:var(--fl-t-head);line-height:1.15;
  letter-spacing:-0.01em;color:var(--ink);margin-bottom:calc(var(--fl-pad) * 0.55);
}
.fl-field{display:block;margin-bottom:calc(var(--fl-pad) * 0.55)}
.fl-label{
  display:block;font-size:var(--fl-t-label);letter-spacing:0.2em;
  color:rgba(168,212,228,0.62);margin-bottom:.3rem;
}
.fl-label i{font-style:normal;opacity:.7}
.fl-input{
  display:block;width:100%;background:none;border:none;border-bottom:1px solid rgba(168,212,228,0.28);
  color:var(--ink);font-family:var(--mono);font-size:var(--fl-t-input);line-height:1.3;
  padding:.25rem 0 .35rem;border-radius:0;
  transition:border-bottom-color var(--t-quick) var(--ease);
}
.fl-input::placeholder{color:var(--faint)}
.fl-input:focus{outline:none;border-bottom-color:var(--accent)}
/* 2026-08-17 — the underline stays (it reads well, and it is the screen's own
   data grammar), but it is no longer the WHOLE indicator. These are the only
   fields on the site where a visitor types personal data, and they carried the
   weakest focus ring on the estate: a 1px bottom shadow against a live 3D room,
   while every other control gets the global
   :focus-visible{outline:2px solid var(--accent);outline-offset:3px}.
   Parity restored — same ring, same offset, underline kept alongside. The ring
   sits inside --fl-pad (10px floor vs 2+3px), so .fl-screen's overflow:hidden
   cannot clip it, and outline is not a layout property: CLS is untouched. */
.fl-input:focus-visible{
  outline:2px solid var(--accent);outline-offset:3px;
  border-bottom-color:var(--accent);box-shadow:0 1px 0 0 var(--accent);
}
.fl-note{resize:none;min-height:calc(var(--fl-t-input) * 2.9);line-height:1.45}
.fl-chips{display:flex;flex-wrap:wrap;gap:.4rem;margin:.25rem 0 .45rem}
.fl-page .pick-chip{
  font-family:var(--mono);font-size:var(--fl-t-body);letter-spacing:0.04em;
  background:rgba(168,212,228,0.05);border-color:rgba(168,212,228,0.22);border-radius:2px;
  padding:.4rem .7rem;min-height:44px;
}
.fl-page .pick-chip[aria-pressed="true"]{border-color:var(--accent);color:var(--accent);background:rgba(255,77,0,0.10)}
.fl-readback{
  display:block;font-size:var(--fl-t-body);line-height:1.45;color:var(--faint);
  opacity:0;transition:opacity var(--t-state) var(--ease);
}
.fl-readback.on{opacity:1}
.fl-readback b{font-weight:600;color:var(--ink)}
/* 15px, not .35rem: the link inside carries a pseudo hit-extender that reaches
   15px past its own box, and this margin is the clearance that keeps that
   extender off the interest chips above. The same clearance is paid below by
   .fl-fail's own margin. Both are static — nothing here moves at runtime. */
.fl-privacy{
  font-family:var(--mono);font-size:var(--fl-t-body);line-height:1.45;
  color:var(--faint);margin:15px 0 0;
}
/* the one inline link on the screen, and it is a real destination, so it carries
   a real 44px box. Grown by a pseudo — the site's own .nav-cta / .brand idiom —
   because raising the line box would push the whole page-03 column down.
   inline-block matters: as a bare inline the link SPLITS across two lines at
   390px, and an extender on a split inline covers the union of both fragments —
   a full-width invisible plate that would have eaten the chip row's bottom edge,
   which is exactly the hit-stealing the chip work warns about. */
.fl-privacy a{
  display:inline-block;position:relative;
  color:rgba(168,212,228,0.8);border-bottom:1px solid rgba(168,212,228,0.3);
}
.fl-privacy a::after{content:"";position:absolute;inset:-15px -6px}

.fl-foot{
  flex:none;display:flex;align-items:center;justify-content:space-between;gap:.7rem;
  min-height:44px;padding-top:calc(var(--fl-pad) * 0.4);
}
.fl-page[data-page="0"] .fl-foot,.fl-confirm .fl-foot{justify-content:flex-end}
/* SCREEN-LIFE, page 01 only: the pick restated as a works-order footer line,
   sitting on its own row directly above the control row. Empty (never `hidden`)
   when there is no pick — and an empty block box is zero-height, so the control
   row below it is in exactly the same place either way. */
.fl-concern{
  display:block;flex:none;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font-size:var(--fl-t-label);letter-spacing:0.16em;text-transform:uppercase;color:var(--faint);
}
.fl-next,.fl-submit{
  display:inline-flex;align-items:center;gap:.4rem;min-height:44px;padding:0 .95rem;cursor:pointer;
  font-family:var(--mono);font-size:var(--fl-t-label);letter-spacing:0.16em;text-transform:uppercase;
  color:var(--ink);background:none;border:1px solid rgba(168,212,228,0.32);border-radius:2px;
  transition:border-color var(--t-quick) var(--ease),color var(--t-quick) var(--ease),transform var(--t-quick) var(--ease);
}
@media (hover:hover){ .fl-next:hover,.fl-submit:hover{border-color:var(--accent);color:var(--accent)} }
.fl-next:active,.fl-submit:active{border-color:var(--accent);color:var(--accent);transform:scale(0.97);transition-duration:var(--t-press)}
.fl-next:focus-visible,.fl-submit:focus-visible,.fl-back:focus-visible,.fl-exit:focus-visible,
.fl-page .pick-chip:focus-visible,.fl-privacy a:focus-visible,.fl-email-row a:focus-visible,
.copy-email:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* ENQUIRY-M2 · the ruling — THE PRIMARY IS FILLED. Outlined, it carried exactly
   the weight of the NEXT → it had spent two sections next to, and the one moment
   on this screen that is not a step is the moment it announces least. The site's
   primary-action grammar is the .fb-cta recipe and this is that recipe sized to
   the screen: accent ground, carbon text, --accent-bright under hand, the pill
   compression at --t-press. The BOX does not change — same 44px control, same
   place in the foot row, so nothing here can move anything. */
.fl-submit{
  background:var(--accent);border-color:var(--accent);color:var(--carbon);
  transition:background var(--t-quick) var(--ease),border-color var(--t-quick) var(--ease),
             color var(--t-quick) var(--ease),transform var(--t-quick) var(--ease);
}
@media (hover:hover){
  .fl-submit:hover{background:var(--accent-bright);border-color:var(--accent-bright);color:var(--carbon)}
}
.fl-submit:active{
  background:var(--accent-bright);border-color:var(--accent-bright);color:var(--carbon);
  transform:scale(0.97);transition-duration:var(--t-press);
}
/* a filled control cannot wear the outline it is standing on: --ink here, which
   is the pairing .fb-submit — the same recipe on the no-JS tier — already ships */
.fl-submit:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
.fl-submit[aria-busy="true"]{opacity:.6;pointer-events:none}
.fl-back{
  display:inline-flex;align-items:center;min-height:44px;padding:0 .5rem;margin-left:-.5rem;
  background:none;border:none;cursor:pointer;color:var(--faint);
  font-size:var(--fl-t-label);letter-spacing:0.12em;
  transition:color var(--t-quick) var(--ease);
}
@media (hover:hover){ .fl-back:hover{color:var(--ink)} }
.fl-back:active{color:var(--ink);transition-duration:var(--t-press)}

/* ---- HONEST FAILURE — the screen dims and dashes; the mailto stays lit.
   The slip's box is RESERVED at all times (the prototype's lesson: revealing it
   cost 0.097 CLS on the phone), so failure only changes ink. ---- */
.fl-fail{
  margin-top:max(15px, calc(var(--fl-pad) * 0.5));
  min-height:calc(44px + var(--fl-t-body) * 1.5);
  opacity:0;visibility:hidden;
  transition:opacity var(--t-state) var(--ease);
}
.fl-page.is-fail .fl-fail{opacity:1;visibility:visible}
.fl-fail-line{font-family:var(--mono);font-size:var(--fl-t-body);line-height:1.45;color:var(--dim)}
.fl-page.is-fail .fl-input{border-bottom-style:dashed;border-bottom-color:rgba(168,212,228,0.22)}
/* the fill goes out with the light: a failure that leaves the primary blazing
   would be the screen saying "sent" and "not sent" in the same frame */
.fl-page.is-fail .fl-submit{background:none;border-color:rgba(168,212,228,0.22);color:var(--faint)}
.fl-email-row{display:flex;align-items:center;gap:.7rem;flex-wrap:wrap}
.fl-email-row .outro-email{
  color:var(--accent);display:inline-flex;align-items:center;min-height:44px;
  font-family:var(--mono);font-size:var(--fl-t-body);letter-spacing:0.02em;
}
.fl-screen .copy-email{min-height:44px}
.fl-screen .ce-label{font-family:var(--mono);font-size:var(--fl-t-label)}

/* ---- CONFIRMATION — the power-on grammar, on the screen ---- */
.fl-eyebrow{
  display:inline-flex;align-items:center;gap:.5rem;
  font-size:var(--fl-t-label);letter-spacing:0.2em;color:var(--accent);
  margin-bottom:calc(var(--fl-pad) * 0.5);
}
.fl-eyebrow-dot{width:6px;height:6px;border-radius:50%;background:var(--accent);opacity:.5}
.fl-confirm-line{
  font-family:var(--mono);font-size:var(--fl-t-body);line-height:1.55;color:var(--dim);
  margin-bottom:calc(var(--fl-pad) * 0.7);
}
.fl-confirm.is-in .fl-rise{animation:gm-enter-rise var(--t-enter) var(--ease) backwards}
.fl-confirm.is-in .fl-head.fl-rise{animation-delay:var(--stagger)}
.fl-confirm.is-in .fl-confirm-line{animation-delay:calc(var(--stagger) * 2)}
.fl-confirm.is-in .fl-email-row{animation-delay:calc(var(--stagger) * 3)}
.fl-confirm.is-in .fl-foot{animation-delay:calc(var(--stagger) * 4)}
.fl-ignite{opacity:1;box-shadow:0 0 12px rgba(255,77,0,0.9)}
.fl-confirm.is-in .fl-ignite{
  --spark-peak:0 0 16px rgba(255,77,0,1);
  --spark-rest:0 0 12px rgba(255,77,0,0.9);
  animation:gm-spark var(--t-spark) var(--ease-decay) var(--t-enter);
}

/* ---- PORTRAIT POSTURE (390x844) ------------------------------------------
   A 16:9 monitor quad framed at 94% of a 390px-wide portrait viewport is
   366 x 197 CSS px, and that is a hard ceiling: framing CLOSER does not help,
   because the overhang would have to be paid back as side padding and the
   content would only get taller. So the height is bought back from the chrome
   instead — the screen's exit row and the page's own foot share ONE 44px control
   row along the bottom of the screen, rather than costing 44px each at opposite
   ends. Same controls, same real 44px boxes, 44px of screen reclaimed. The
   decorative section counter is dropped here (the sr-only status line is what
   actually announces the step). --------------------------------------------- */
/* ENQUIRY-M2 · the ruling — and the shared row survives ONLY where it is still
   unambiguous. From section 02 on, "← BACK TO THE ROOM" and "← BACK" sat side by
   side in that one row: two arrows pointing left, one of which left the room.
   So from 02 the room-exit moves to a strip of its own along the top and becomes
   a corner close, and the foot row carries only the step-back and the primary.
   Section 01 keeps the shared row exactly as it was — it has no step-back, so
   nothing there is ambiguous, and it is also the only section that still gets to
   spend its reclaimed 44px on content.
   ENQUIRY-M3 · and the ruling's cost is now paid back: the corner exit FLOATS
   over the top-right instead of reserving a band. Sections 02/03/confirmation
   keep their full quad height, and the one thing a floating control can break —
   text running underneath it — is answered with clearance on the leading line
   rather than with a row nobody else gets to use. -------------------------- */
@media (orientation:portrait){
  .fl-bar{
    top:auto;bottom:0;justify-content:flex-start;padding:0;
    border-bottom:none;border-top:1px solid rgba(168,212,228,0.16);
  }
  .fl-count{display:none}
  /* NO RESERVED ROW anywhere in portrait now. Section 01's foot shares the
     bottom bar (padding, which does not shrink the box); 02/03/confirmation
     have the exit floating over them and give it clearance below. */
  .fl-page{top:0}
  .fl-page[data-page="0"]{padding-bottom:var(--fl-bar)}
  .fl-page[data-page="0"] .fl-foot{
    position:absolute;right:var(--fl-pad);bottom:0;left:auto;
    height:var(--fl-bar);padding-top:0;justify-content:flex-end;
  }
  /* ---- SECTIONS 02, 03 AND THE CONFIRMATION ----
     ENQUIRY-M3. The strip is a FLOATING corner control, not a reserved band: it
     is shrink-wrapped to its one 44px button at the top-right, and .fl-page runs
     the full height of the quad underneath it. Two things follow, and both are
     handled here rather than hoped for.

     1. TEXT. Only the leading line of each section reaches this corner, so only
        the leading line pays: --fl-corner of right-clearance on the head (and on
        the confirmation's eyebrow, which sits above its head). Everything below
        the control's 44px is clear of it by construction.
     2. HITS. The r3 lesson, unchanged and now load-bearing in both directions:
        the strip takes NO pointer events and only its one real control is raised
        (.fl-exit's z-index:2), so it cannot steal from the chips or fields it
        now overlaps — and .fl-form, which paints later, cannot steal from IT.
        Re-hit-tested on every section at both viewports.

     Still two boxes with exactly one of them ever rendered, so still zero shift:
     an element display:none in the previous frame has no previous position. */
  .fl[data-fl-page="1"] .fl-bar,
  .fl[data-fl-page="2"] .fl-bar,
  .fl[data-fl-page="3"] .fl-bar{display:none}
  .fl[data-fl-page="1"] .fl-strip,
  .fl[data-fl-page="2"] .fl-strip,
  .fl[data-fl-page="3"] .fl-strip{
    display:flex;align-items:stretch;justify-content:flex-end;
    position:absolute;left:auto;right:0;top:0;height:var(--fl-bar);
    border-bottom:none;
    pointer-events:none;
  }
  /* THE SCROLLER'S GUTTER. Dropping the reserved band let .fl-body run the full
     height of the quad, and its scrollbar with it — so the overlay thumb was
     drawn straight through the last letter of ROOM (measured: thumb at 361–368,
     label ink ending at 368.0). The control keeps its 44px box anchored in the
     corner, where a thumb reaches for it; only the INK steps in by the gutter. */
  .fl-screen{--fl-gutter:12px}
  .fl-exit-top{padding-right:calc(var(--fl-pad) + var(--fl-gutter))}
  /* The clearance is the CONTROL'S OWN WIDTH, derived rather than guessed: the
     exit is a real 44px-tall box whose width is its six mono glyphs at
     --fl-t-label (0.6em advance + 0.12em tracking ≈ 4.32em, rounded up to 4.6
     for the slack a fallback face needs), the screen's padding either side, and
     the gutter above. It lands at ~78px on a 366px quad; a flat 52 would have
     left the last letters of ROOM sitting on "What we'd look at first". */
  .fl-screen{--fl-corner:calc(var(--fl-t-label) * 4.6 + var(--fl-pad) * 2 + var(--fl-gutter))}
  .fl[data-fl-page="1"] .fl-head,
  .fl[data-fl-page="2"] .fl-head,
  .fl[data-fl-page="3"] .fl-head,
  .fl[data-fl-page="3"] .fl-eyebrow{padding-right:var(--fl-corner)}
  .fl-head{font-size:var(--fl-t-input);margin-bottom:calc(var(--fl-pad) * 0.3)}
  .fl-note{min-height:calc(var(--fl-t-input) * 2.4)}
  .fl-chips{gap:.35rem;margin:.2rem 0 .3rem}
  .fl-page .pick-chip{padding:.35rem .55rem}
}

/* ============================================================
   REDUCED MOTION — a COMPLETE TIER, not a stub. The camera does not move at all:
   the room cuts straight to the parked screen-face pose with the screen already
   lit (a state, not a motion), and every page change is instant. The form is the
   same form on the same screen — this tier loses the walk, not the room, because
   the room is where the enquiry lives. Both halves are authored, because
   html.reduced is app.js's and the media query is the browser's.
   ============================================================ */
html.reduced .fl-page,
html.reduced .fl-confirm.is-in .fl-rise,
html.reduced .fl-confirm.is-in .fl-ignite{animation:none !important;clip-path:none !important;translate:none !important}
html.reduced .fl-page.is-out{opacity:1;translate:none}
/* .fl.is-open is (0,2,0) and carries its own transition-duration, so a bare `.fl`
   here would not reach it — the open fade would still run at --t-state on a tier
   that asked for no motion. Both selectors, in both halves. */
html.reduced .fl,html.reduced .fl.is-open,
html.reduced .fl-readback,html.reduced .fl-fail,
/* ENQUIRY-M2 — the screen's own CONTROLS were in neither half: their colour and
   border transitions ran at --t-quick on a tier that asked for none, and the
   press compression was pinned by the media query alone. The filled primary made
   that visible (a 200ms accent wash is not a state change), so both halves now
   carry the whole control set, not just the one that changed. */
html.reduced .fl-exit,html.reduced .fl-next,html.reduced .fl-submit,
html.reduced .fl-back,html.reduced .fl-input,
html.reduced.fl-open #screens{transition:none}
html.reduced .fl-next:active,html.reduced .fl-submit:active{transform:none !important}
html.reduced #scene{transition:none}
@media (prefers-reduced-motion:reduce){
  .fl-page,.fl-confirm.is-in .fl-rise,.fl-confirm.is-in .fl-ignite{animation:none !important;clip-path:none !important;translate:none !important}
  .fl,.fl.is-open,.fl-readback,.fl-fail,
  .fl-exit,.fl-next,.fl-submit,.fl-back,.fl-input,
  html.fl-open #screens{transition:none}
  .fl-next:active,.fl-submit:active{transform:none !important}
}

/* ============================================================================
   SHORT-VIEWPORT SCROLL FALLBACK — THE MISSING AXIS            (2026-08-17)

   THE HOLE. Every screen box is fixed/absolute, vertically centred and
   overflow:visible, and the walk is scroll-DRIVEN — the wheel advances the
   screen, it never scrolls a panel — so content taller than its box is not
   clipped, it is UNREACHABLE. And every breakpoint in this file was width-only
   or portrait-only: the one-column menu is gated (max-width:640px), the compact
   hero (orientation:portrait) and (max-width:900px). A landscape phone at
   844x390 is wider than 640, narrower than 900 and not portrait, so NEITHER
   fires and the full desktop centred layout renders into a 390px viewport.
   Measured before, at 844x390: menu content 464px in 390 — "Start" at top -42,
   the CTA cut at 359..404, "What is responsible AI?" wholly below the fold at
   420..464, wheel-scroll a confirmed no-op; .hero-inner 626px, CTA at 466..512;
   .outro-inner 493px, address and COPY ADDRESS below the fold; .problem-inner
   449px, last paragraph gone. At 640x400 @dsf2 (200% zoom) the first three menu
   entries sat at -200, -148 and -66. Fixed at the CONTAINER CONTRACT, not at the
   five call sites, on the axis the file never had. Nothing is keyed on width, so
   no existing breakpoint changes behaviour.

   WHY 560px — measured, not taste. Sweeping 400..760h: at 640h every column
   still clears (hero 7..633, outro 71..569, menu 78..591), which is exactly
   where the outro's own height-fit note says it was last measured, and the
   file's lowest authored height step is max-height:700px. The first column to
   stop fitting is .hero-inner, whose headline clamp floors at 56px so its height
   parks at 626px; the menu ledger breaks at 520h (844 wide) / 600h (1280 wide).
   560 is the round step below every viewport the design was measured good at and
   above the whole real short band (landscape phones ~330-430h, 200% zoom on any
   laptop), so the gate can never reach a designed moment. RESIDUAL, stated not
   hidden: 561-626h still clips .hero-inner by up to 26px; closing it needs the
   gate at 640, which would re-anchor .outro-inner at a height its own comment
   records as measured and cleared — a look change on a working viewport.

   overscroll-behavior:contain on every scroller: a flick that reaches the end
   cannot chain into the document and steal the walk's own wheel.

   THE SCRIM. overflow-y:auto also clips overflow-x (a non-visible value on one
   axis computes the other to auto), and the copy scrims deliberately sit OUTSIDE
   their column at inset:-4.5rem -6rem — clipped to the box the gradient would be
   cut at ~0.36 alpha dead on the text edge, the one thing that scrim's own note
   forbids ("no visible edge"). Refitted INSIDE the box for this band only: same
   gradient, same stop list, drawn into a centred 62.5% painting area so the
   ellipse's 0-alpha radius (80% of that area) lands exactly on the column edge.
   Geometry refit forced by the clip; no colour or stop is authored here.

   .fl / #firstlook needs nothing — .fl-body is already flex:1 + overflow-y:auto,
   which is this same contract, already honoured.
   ============================================================================ */
@media (max-height:560px){
  /* THE SCROLLBAR. The estate's rule is already written, at .fl-body: "the site
     does not theme scrollbars anywhere else because nowhere else is a scroller
     INSIDE a rendered object". These new scrollers are exactly that case — the
     columns sit over the live room — and measured unthemed they drew a 15px
     full-height WHITE system track straight across the diorama at 640x400.
     Same treatment as .fl-body, in the estate's ink instead of the screen's:
     thin, transparent track, existing token for the thumb. No new colour. */
  .hero-inner,.problem-inner,.startgate-inner,.outro-inner,.menu,.modal{
    scrollbar-width:thin;scrollbar-color:var(--faint) transparent;
  }
  .hero-inner,.problem-inner,.startgate-inner,.outro-inner{
    top:0;transform:none;
    max-height:100vh;max-height:100dvh;      /* dvh where supported, vh fallback */
    overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;
    padding-block:1rem;                      /* nothing sits flush to the edge */
  }
  .hero-inner::before,.problem-inner::before,
  .startgate-inner::before,.outro-inner::before{
    inset:0;background-repeat:no-repeat;background-position:center;
    background-size:62.5% 62.5%;
  }
  /* THE MENU. The scroller is .menu, not .menu-panel: the panel is
     pointer-events:none by design (the backdrop-click handler depends on it), so
     a touch drag on it would never reach a scroller of its own. .menu is
     pointer-events:auto while open and is the panel's containing block.
     align-items/align-content go to START because CENTRED overflow is the whole
     bug: scrollable overflow only ever extends down, so content pushed above the
     top edge by centring is unreachable however far you scroll — measured at
     640x400, where the panel's scrollHeight equalled its clientHeight while three
     entries sat at negative tops. */
  .menu{
    align-items:flex-start;
    overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;
  }
  .menu-panel{align-content:start}
  /* the explainer card, same contract: stop centring it, let the backdrop scroll */
  .modal{
    align-items:flex-start;
    overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;
  }
}

/* ============================================================================
   THE HERO RESIDUAL — closed 2026-08-17 (Phase 7 clean sweep, finding F-S32)
   ----------------------------------------------------------------------------
   The 560px gate above left a stated residual, and the Phase 7 sweep measured it
   BOTH wrong and worse than the comment claimed. The comment says the clipping
   band is 561-626h; the CTA's real clipped depth was measured at 1024 wide as
   h570 -> 34.5px, h600 -> 19.4, h620 -> 9.4, h628 -> 5.4, h632 -> 3.4,
   h635 -> 2.0, and only h640 -> -0.6 (clear). So the true band is ~561-636h,
   and inside all of it the hero's primary CTA "Start with a conversation" is
   below the fold with NO scroll affordance at all, because the gate is
   deliberately inactive there. That is not a landscape-phone edge case: a laptop
   window ~700px tall, once browser chrome is taken out, lands squarely in it.

   Why this is a SEPARATE block rather than moving the gate to 640: the original
   note's objection was sound — firing the whole contract at 640 would re-anchor
   .outro-inner at a height its own comment records as measured and cleared, i.e.
   a look change on a working viewport. Only .hero-inner fails in this band (its
   headline clamp floors at 56px, parking the column at 626px), so only
   .hero-inner is given the scroll fallback here. min-height:561px keeps this
   strictly above the main gate, so the two never both apply.
   ============================================================================ */
@media (min-height:561px) and (max-height:640px){
  .hero-inner{
    top:0;transform:none;
    max-height:100vh;max-height:100dvh;
    overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;
    padding-block:1rem;
    scrollbar-width:thin;scrollbar-color:var(--faint) transparent;
  }
  /* the scrim must be refitted inside the box for the same reason as above:
     overflow-y:auto computes overflow-x to auto, which would guillotine a
     gradient authored to sit outside the column. */
  .hero-inner::before{
    inset:0;background-repeat:no-repeat;background-position:center;
    background-size:62.5% 62.5%;
  }
}
