/* ============================================================================
   verso pre-launch site · shared stylesheet
   Mirrors the Next app's split: globals.css tokens + h9c.css components, flattened
   into one file because there is no build step here. Values are the port's
   token-for-token. sim.html is deliberately NOT on this sheet, it is a
   self-contained snapshot that carries its own copy.
   ========================================================================== */

/* Both faces self-hosted, so these pages make ZERO third-party requests, matching
   production (next/font pulls Plex at build time and serves it from our origin).
   Plex is the variable face, one file covers both weights we use. */
@font-face { font-family: "IBM Plex Sans"; src: url("IBMPlexSans-var.woff2") format("woff2");
  font-weight: 400 500; font-style: normal; font-display: swap; }
@font-face { font-family: "Satoshi"; src: url("Satoshi-Regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "Satoshi"; src: url("Satoshi-Medium.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap; }

/* ───────── VISTA TOKENS · light default ─────────
   The port defines dark on bare :root and light under [data-theme="light"]. This
   is inverted because there is no server render to stamp an attribute before
   paint. Same rendered result, and the anti-flash script only ever writes "dark". */
:root {
  --paper: #F5F6F5; --paper-elev: #FFFFFF; --paper-deep: #E8EAE9; --plain-bg: #FFFFFF;
  --ink: #1B1E1F; --ink-soft: #5F6562; --secondary: #6B6E69; --whisper: #8C9298;
  --hairline: #E4E6E4; --rule: #D4D6D3;
  --hero-accent: #3F6385; --hero-accent-soft: #4F7596; --hero-tint: #DCE5EE;
  --problem-accent: #A72E1B; --problem-accent-soft: #E0563E; --problem-tint: #F7E0DA;
  --problem-red: #A72E1B;
  --process-accent: #5F7A66; --process-accent-soft: #7A9482; --process-tint: #DDE4DD;
  /* mesh-only greys: the site's atmosphere is steel + graphite, not steel + sage */
  --mesh-graphite: #7B828A; --mesh-graphite-soft: #99A1A9;
  --mesh-steel: var(--hero-accent); --mesh-steel-soft: var(--hero-accent-soft);
  --error: #8F3032; --on-accent: #FFFFFF; --pulse-min: 0.70;

  /* ── GLASS TUNING · two multipliers over every frosted surface on the site ──
     Enzo's call 2026-08-23: almost every box on both sites is glass now, but the
     page behind them has always been near-blank paper, so the level was never
     actually testable. The contour field is the first real backdrop, which makes
     this the first honest look at it.

     MULTIPLIERS, NOT ABSOLUTE VALUES, and that is the whole design of this knob.
     The surfaces do NOT share one number: light runs 76% cards over a 46% pane,
     dark runs 16% cards over a 38% pane, and dark's ladder (backdrop darkest,
     then pane, card, row, each lighter) is a locked rule in CLAUDE.md. One
     absolute slider would flatten all of that into a single value and destroy the
     ladder on the first drag. A multiplier moves them together and keeps every
     relationship the design already has.

     LOCKED 2026-08-23 at Enzo's tuning, eyeballed against the contour field:
     x0.15 opacity, x0.60 blur. The tuner bar that found them has been removed
     from index.html; rebuild it from that session if these ever reopen.

     What x0.15 resolves to, and it is a big move: the light pane goes 46% -> 6.9,
     light cards 76 -> 11.4, dark cards 16 -> 2.4, buttons 55 -> 8.3, the gate
     72 -> 10.8, and the feature card body 100 -> 15. These surfaces are now
     barely there, which is the point: the site finally has a real backdrop to be
     glass over, and the level that reads as glass over a contour field is far
     lower than the level that read as glass over blank paper. */
  --glass-k: 0.15;      /* surface opacity, x1 = the pre-2026-08-23 values */
  --glass-blur-k: 0.6;  /* backdrop blur, x1 = the pre-2026-08-23 values */

  --glass: color-mix(in srgb, #EDF0EF clamp(0%, calc(46% * var(--glass-k)), 100%), transparent);
  --glass-border: color-mix(in srgb, #fff 55%, var(--rule));
  --glass-inset: inset 0 1px 0 color-mix(in srgb, #fff 60%, transparent);
  --card-glass: color-mix(in srgb, var(--paper-elev) clamp(0%, calc(76% * var(--glass-k)), 100%), transparent);
  --card-glass-hi: color-mix(in srgb, var(--paper-elev) clamp(0%, calc(76% * var(--glass-k)), 100%), transparent);
  --card-glass-line: var(--hairline);
  --card-glass-text: var(--card-glass); --card-lift-text: 1;
  --pane-lift: 1; --card-lift: 1; --pane-bake: rgba(255,255,255,0);

  /* --shadow-card USED TO LEAD with `0 -2px 0 var(--hairline)`, a hard unblurred
     line offset upward as a light catch. In light mode that line is the same colour
     as the card's own border, so the two stack into a ~2.7px grey lip that reads as a
     second panel behind every card. A light catch has to be BRIGHTER than the
     surface to read as light, and there is no headroom above a near-white card on
     pale paper, so light mode drops the layer instead of inverting it.

     DARK DROPPED IT TOO, 2026-08-21, ON ENZO'S CALL. The old note here said dark
     was correct as-is because #4D5054 genuinely is brighter than the near-black
     behind it. That is true about the COLOUR and wrong about the RESULT: a hard
     unblurred 2px line offset upward from a 20px radius does not read as a light
     catch, it reads as a thin rounded chip sitting on top of the box, and it is
     most obvious on the full-width renewals band where the run is longest.
     Enzo: "that weird thin pill chip band ... it shouldn't be there, it makes the
     box look weird. Anywhere on the website where you see a box do that, remove
     it." Both themes now carry the ambient layer only.
     THE NEXT APP DROPPED IT TOO, same day, same call, in app/globals.css. The old
     note here said it had not; that was true when written and is not any more. */
  --shadow-card: 0 -10px 36px rgba(28,26,23,0.05);
  --shadow-card-hover: 0 8px 24px rgba(28,26,23,0.10);
  --shadow-card-lg: 0 18px 48px rgba(28,26,23,0.16);
  --shadow-btn: 0 4px 0 -2px rgba(28,26,23,0.08), 0 4px 18px rgba(28,26,23,0.10);
  --shadow-btn-hover: 0 8px 24px rgba(28,26,23,0.14);
  --shadow-nav-cta: 0 2px 6px rgba(28,26,23,0.06);
  --shadow-nav-cta-hover: 0 6px 16px rgba(28,26,23,0.10);
  --nav-bg: rgba(245,246,245,0.85);

  --ease-out-strong: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out-strong: cubic-bezier(0.77, 0, 0.175, 1);
  --ease-soft: cubic-bezier(0.16, 1, 0.3, 1);
  --dur-press: 160ms; --dur-fast: 200ms; --dur-base: 260ms;
  --dur-slow: 340ms; --dur-panel: 420ms;
  --press-scale: 0.97;

  --sans: "IBM Plex Sans", system-ui, -apple-system, sans-serif;
  --display: "Satoshi", "IBM Plex Sans", sans-serif;

  --nav-h: 68px;
}
:root[data-theme="dark"] {
  --paper: #191C1F; --paper-elev: #3A3D40; --paper-deep: #141619; --plain-bg: #141619;
  --ink: #ECEAE4; --ink-soft: #BAB8B2; --secondary: #AEACA6; --whisper: #6E747A;
  --hairline: #4D5054; --rule: #4A4F54;
  --hero-accent: #88A4C0; --hero-accent-soft: #A2BCD2; --hero-tint: #232C33;
  --problem-accent: #E85C49; --problem-accent-soft: #F07A6A; --problem-tint: #2A1A17;
  --problem-red: #E85C49;
  --process-accent: #8DB39B; --process-accent-soft: #A8C8B6; --process-tint: #1E2622;
  /* dark mesh steel is the DEEP steel, not --hero-accent: the UI steel #88A4C0
     blurs to the same grey as graphite and the two scenes stop reading apart. */
  --mesh-graphite: #828A92; --mesh-graphite-soft: #9AA2AA;
  --mesh-steel: #4F7596; --mesh-steel-soft: #5E82A3;
  --error: #D45656; --on-accent: #16191C; --pulse-min: 0.55;

  --glass: color-mix(in srgb, var(--paper-elev) clamp(0%, calc(38% * var(--glass-k)), 100%), transparent);
  --glass-border: rgba(255,255,255,0.10);
  --glass-inset: inset 0 1px 0 rgba(255,255,255,0.05);
  --card-glass: color-mix(in srgb, #F2F5F7 clamp(0%, calc(16% * var(--glass-k)), 100%), transparent);
  --card-glass-hi: color-mix(in srgb, #F2F5F7 clamp(0%, calc(5% * var(--glass-k)), 100%), transparent);
  --card-glass-line: rgba(255,255,255,0.12);
  --card-glass-text: color-mix(in srgb, var(--paper-elev) clamp(0%, calc(62% * var(--glass-k)), 100%), transparent);
  --card-lift-text: 1.15;
  /* THE DARK BRIGHTNESS LIFT IS OFF, and this is a determinism fix as much as a
     look call. Enzo, 2026-08-26.

     It used to be `--pane-lift: 1.35; --card-lift: 1.5`, and those numbers were set
     when the surfaces were opaque enough to carry themselves. After the glass went to
     x0.15 on 08-23 the lift became MOST of what a dark box looks like: measured on the
     page backdrop rgb(25,28,31) it moves a card +13,+14,+16 while the card's own tint
     at x0.15 moves it only +5,+5,+5. A uniform multiply over a near-flat backdrop
     paints a solid rectangle, so dark read as though the 08-23 tuning had never
     landed. It had; it was buried.

     THE REASON IT IS 1 AND NOT A SMALLER NUMBER: `brightness()` lived INSIDE
     `backdrop-filter`, and Chrome SKIPS that whole property under layer pressure
     rather than degrading it (this page asks for 19 live filter layers). So the lift
     was present or absent depending on what the compositor decided that load, and the
     boxes rendered two different ways on one machine, which is the "features coming in
     and out of existence" Enzo had been chasing for weeks. At 1 the filter is just a
     blur, and a dropped blur is nearly invisible next to a dropped brightness, so both
     compositor states now look the same. See [[project_background_exploration]].

     To bring a lift back, do NOT put it back in the filter. Paint it as a flat layer,
     the way --pane-bake already did, or it starts flickering again. */
  --pane-lift: 1; --card-lift: 1; --pane-bake: rgba(255,255,255,0);

  --shadow-card: 0 -10px 36px rgba(9,13,18,0.35);
  --shadow-card-hover: 0 8px 24px rgba(9,13,18,0.4);
  --shadow-card-lg: 0 18px 48px rgba(9,13,18,0.50);
  --shadow-btn: 0 4px 0 -2px rgba(9,13,18,0.5), 0 4px 18px rgba(9,13,18,0.4);
  --shadow-btn-hover: 0 8px 24px rgba(9,13,18,0.5);
  --shadow-nav-cta: 0 2px 6px rgba(9,13,18,0.20);
  --shadow-nav-cta-hover: 0 6px 16px rgba(9,13,18,0.30);
  --nav-bg: rgba(25,28,31,0.85);
}

/* ───────── base ───────── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; scroll-padding-top: calc(var(--nav-h) + 24px); }
/* Lenis owns scroll physics once it boots; these are its required hooks. */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: clip; }

body { background: var(--paper); color: var(--ink); font-family: var(--sans);
  font-weight: 400; line-height: 1.55; -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility; overflow-x: hidden; }
a { color: inherit; text-decoration: none; }
::selection { background: var(--hero-tint); color: var(--ink); }
a:focus-visible, button:focus-visible, input:focus-visible {
  outline: 3px solid var(--hero-accent); outline-offset: 2px; border-radius: 4px; }
/* on steel-filled controls the steel ring vanishes; use the bone ink ring */
.btn:focus-visible, .nav-cta:focus-visible { outline-color: var(--ink); }
h1, h2, h3 { font-family: var(--display); font-weight: 500; letter-spacing: -0.02em;
  line-height: 1.06; text-wrap: balance; }
p { text-wrap: pretty; }
main:focus { outline: none; }

html.theme-transitioning, html.theme-transitioning * {
  transition: background-color var(--dur-base) var(--ease-out-strong),
              color var(--dur-base) var(--ease-out-strong),
              border-color var(--dur-base) var(--ease-out-strong),
              fill var(--dur-base) var(--ease-out-strong),
              stroke var(--dur-base) var(--ease-out-strong) !important; }

/* ───────── atmospheric mesh · RETIRED 2026-08-16, kept as the revert path ─────────
   The hero warp below replaced this, matching the port. The mesh markup is gone from
   all five pages and the scene-morph observers are out of verso.js, so every rule in
   this block is currently dead. It stays because reverting is then two divs per page
   plus the observer, rather than rebuilding a tuned effect from memory.
   NOTE if you do revert: the per-section cool/warm morph went with it, on Enzo's
   call. `[data-scene]` attributes are still on the sections but nothing reads them. */
.mesh { position: fixed; inset: 0; z-index: -1; overflow: hidden; pointer-events: none;
  transition: opacity 1.15s var(--ease-soft); }
.mesh .blob { position: absolute; border-radius: 50%; filter: blur(70px); will-change: transform, opacity; }
.mesh-warm { opacity: 0; }
:root[data-scene="warm"] .mesh-cool { opacity: 0.18; }
:root[data-scene="warm"] .mesh-warm { opacity: 1; }
:root[data-theme="dark"][data-scene="warm"] .mesh-warm { opacity: 0.6; }
.mesh-cool .b1 { width: 60vw; height: 60vw; left: -12vw; top: -14vw;
  background: radial-gradient(circle, color-mix(in srgb, var(--mesh-steel) 60%, transparent) 0%, transparent 64%);
  animation: drift1 30s var(--ease-soft) infinite alternate, breathe 7s ease-in-out infinite; }
.mesh-cool .b2 { width: 56vw; height: 56vw; right: -14vw; top: -6vw;
  background: radial-gradient(circle, color-mix(in srgb, var(--mesh-steel-soft) 52%, transparent) 0%, transparent 62%);
  animation: drift2 36s var(--ease-soft) infinite alternate, breathe 8.5s ease-in-out -2s infinite; }
.mesh-cool .b3 { width: 52vw; height: 52vw; left: 14vw; bottom: -16vw;
  background: radial-gradient(circle, color-mix(in srgb, var(--mesh-steel) 40%, transparent) 0%, transparent 60%);
  animation: drift3 33s var(--ease-soft) infinite alternate, breathe 6.5s ease-in-out -1s infinite; }
/* b4 exists so glass cards mid-column have something to frost against */
.mesh-cool .b4 { width: 44vw; height: 44vw; left: 30vw; top: 32vh;
  background: radial-gradient(circle, color-mix(in srgb, var(--mesh-steel-soft) 28%, transparent) 0%, transparent 62%);
  animation: drift2 38s var(--ease-soft) infinite alternate, breathe 8s ease-in-out -2.5s infinite; }
.mesh-warm .b1 { width: 58vw; height: 58vw; left: -10vw; top: -10vw;
  background: radial-gradient(circle, color-mix(in srgb, var(--mesh-graphite) 42%, transparent) 0%, transparent 64%);
  animation: drift2 32s var(--ease-soft) infinite alternate, breathe 7.5s ease-in-out infinite; }
.mesh-warm .b2 { width: 54vw; height: 54vw; right: -12vw; top: 4vh;
  background: radial-gradient(circle, color-mix(in srgb, var(--mesh-graphite-soft) 38%, transparent) 0%, transparent 62%);
  animation: drift1 35s var(--ease-soft) infinite alternate, breathe 9s ease-in-out -3s infinite; }
.mesh-warm .b3 { width: 50vw; height: 50vw; left: 18vw; bottom: -14vw;
  background: radial-gradient(circle, color-mix(in srgb, var(--mesh-steel) 36%, transparent) 0%, transparent 60%);
  animation: drift3 31s var(--ease-soft) infinite alternate, breathe 6.8s ease-in-out -1.5s infinite; }
.mesh-warm .b4 { width: 44vw; height: 44vw; right: 24vw; top: 30vh;
  background: radial-gradient(circle, color-mix(in srgb, var(--mesh-graphite) 20%, transparent) 0%, transparent 62%);
  animation: drift1 37s var(--ease-soft) infinite alternate, breathe 8.6s ease-in-out -1.8s infinite; }
/* only the visible scene animates; once the plain zone covers the mesh, freeze both */
.mesh-warm .blob { animation-play-state: paused; }
:root[data-scene="warm"] .mesh-warm .blob { animation-play-state: running; }
:root[data-scene="warm"] .mesh-cool .blob { animation-play-state: paused; }
:root.mesh-idle .mesh .blob { animation-play-state: paused; }
@keyframes drift1 { from { transform: translate(0,0) scale(1); } to { transform: translate(7vw,5vh) scale(1.12); } }
@keyframes drift2 { from { transform: translate(0,0) scale(1); } to { transform: translate(-6vw,7vh) scale(1.1); } }
@keyframes drift3 { from { transform: translate(0,0) scale(1.05); } to { transform: translate(5vw,-5vh) scale(1); } }
@keyframes breathe { 0%,100% { opacity: 0.72; } 50% { opacity: 1; } }

/* ════════════════════ BACKGROUND · THE CONTOUR FIELD ════════════════════
   Two masses of topographic contour lines, one running in from the left, one its
   mirror from the right, with a channel between them that the copy sits in. From
   `_design-exploration/hero-space-lab/index.html`, Enzo's tuning 2026-08-23.

   IT IS FIXED, AND THAT IS WHY THE PLAIN ZONE STILL MATTERS. The field covers the
   viewport and never scrolls, exactly as the retired mesh did, so it sits behind
   the hero, the what, the pain and the features and then STOPS, because
   .plain-zone below is opaque and paints over it. The cut is at the surface
   change, not at the bottom of the hero. Two consequences worth knowing:
     · adding a background to any section above .plain-zone hides the field there
     · the geometry is viewport-sized, so Reach and Tilt mean the same thing here
       as they do in the lab. An absolute, page-tall version would stretch the
       waves and every tuned number would have to be found again.

   THE TWO KNOBS ARE DELIBERATELY STILL LOOSE. --topo-tilt and --topo-reach are
   read fresh on every draw and `topo(tilt, reach)` in the console redraws live,
   because the lab could only guess at what clears the nav, the wordmark and the
   feature cards on a real page. When they lock, they become numbers here and the
   console hook goes. */
:root {
  --topo-ink: #1B1E1F;
  --topo-op: 0.85;
  --topo-n: 31;         /* lines per mass */
  --topo-amp: 180;      /* how far a line swings, px */
  --topo-reach: 17;     /* inner edge of each mass, % of viewport width */
  --topo-tilt: -19;     /* degrees, rotates both masses about the centre */
}
/* BELOW THE HERO'S OWN COLLAPSE the masses have to pull back. At 1000px the hero
   drops to a single left-aligned column (see the media query near the bottom of
   this file) and the copy stops being a narrow band down the middle, so the
   channel that clears it at 1440 no longer exists. Amp already scales with the
   viewport in verso.js, which fixes the swing; this fixes the footprint. A
   starting value, not a tuned one, and it is the second thing to settle after the
   desktop pair. */
@media (max-width: 1000px) { :root { --topo-reach: 4; } }
:root[data-theme="dark"] { --topo-ink: #ECEAE4; }
/* width/height are explicit because inset alone does not size a replaced element
   that has no intrinsic ratio, and an <svg> has none until its viewBox is set */
.topo { position: fixed; inset: 0; width: 100%; height: 100%;
  z-index: -1; pointer-events: none; opacity: var(--topo-op);
  /* THE NAV BAND IS KEPT CLEAR, and it is a mask rather than a change to the
     geometry on purpose. The nav is transparent by design and both right-hand
     items, `About us` and `Join the waitlist`, are small unbacked type that the
     right mass runs straight through. Three other fixes were available and each
     costs more than it saves: giving the nav a bar background contradicts the one
     rule that section of the stylesheet is built on; a radial hole punched at the
     top right reads as a smudge in a field of lines; and pulling Reach back until
     the mass clears the corner throws away the composition to protect two links.

     A mask is the only one that is independent of the tuning. It holds the field
     fully out of the nav's own line box, then ramps it in over the 52px below, so
     the field appears to emerge from behind the chrome instead of being cut. It
     tracks --nav-h, so it keeps working at any Tilt or Reach, which matters while
     those two are still moving. */
  --topo-clear: calc(var(--nav-h) * 0.9);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, transparent var(--topo-clear), #000 calc(var(--nav-h) + 64px));
          mask-image: linear-gradient(to bottom, transparent 0, transparent var(--topo-clear), #000 calc(var(--nav-h) + 64px)); }
.topo path { fill: none; stroke: var(--topo-ink); stroke-linecap: round; }

/* The plain zone used to cover the mesh from here down, so the page read as two
   halves: atmospheric up top where the pitch lives, flat and quiet below where the
   detail does. The mesh is gone and the contour field above took over that job, so
   this rule is load-bearing again: --plain-bg is opaque and differs from --paper,
   which is both what keeps the lower half a distinct surface AND what cuts the
   field off. */
.plain-zone { position: relative; z-index: 1; background: var(--plain-bg); }

/* ════════════════════ HERO REFRACTION WARP ════════════════════
   Ported from the port's `app/styles/warp.css` + `HeroWarp.tsx`, 2026-08-16. One
   masked lens sweeps diagonally across the hero once, refracting whatever it
   crosses, then gets out of the way. It replaces the mesh as the hero's atmosphere.

   Source of truth for every number: `_design-exploration/background/d3.html` and its
   README, which carries the measurements and three post-mortems. THE SETTINGS ARE
   ENZO'S, hand-tuned and locked 08-16. The numeric ones live in verso.js so the JS
   and CSS cannot drift.

   FOUR THINGS THAT LOOK WRONG AND ARE NOT:
   1. THE LENS SITS ON TOP (z-index 50), not behind. `backdrop-filter` repaints what
      is painted BEHIND the element, so at the mesh's z-index -1 it would sample the
      page background and refract nothing. Under the nav (200) so chrome stays crisp.
   2. THE COLOUR IS 100% DERIVED FROM THE CONTENT. Measured in the lab: with content
      hidden the effect produces exactly 0 fringe pixels. Nothing to retint.
   3. NO WAVE BODY. Enzo's tuning runs the lab's grey lobe layer at strength 0, and
      the oil renders at `oil x strength`, so it shows nothing. Not ported.
   4. THE ROTATION NEEDS ITS OWN ELEMENT. The rise wants transform-origin 50% 100%,
      a rotation wants 50% 50%, and an element has one origin. Rotating is also what
      keeps the band perpendicular to travel: a horizontal band sliding right is a
      rising band that slides, not a diagonal sweep. */
:root {
  /* --warp-travel and --warp-reach are DERIVED at runtime by verso.js from the angle
     and the live viewport. Speed is the tuned constant and duration falls out of it,
     so a change of angle or viewport never silently changes the rate of the pass.
     820 px/s since 2026-08-18, up from the 510 that Enzo's own pass and the presence
     lab had independently converged on; he called that slow.
     The values here are only the pre-script fallback. */
  --warp-travel: 1894ms;
  --warp-reach: 1;
  --warp-angle: 22deg;
  --warp-op: 1;
  --warp-spread: 1.55;
  /* THE BLUR IS LOAD-BEARING and the obvious measurement lies about it. Dropping it
     toward 0.12vw DOUBLES the measured fringe and looks worse: hard cyan/magenta
     ringing that reads as print misregistration. Blur fuses the three channel copies
     into one material. More coloured pixels is not more glass. */
  --warp-blur: 0.27vw;
  /* 1, not the presence lab's 1.11: on pale paper 1.11 clips to white, blowing out
     4.23% of the crop, eating 18% of the headline's ink AND cutting the fringe,
     because clipping destroys the channel differences that ARE the colour. */
  --warp-bright: 1;
}
/* display:none at rest, not opacity 0. A permanently mounted backdrop-filter is a
   compositing layer carried on every scroll frame for an effect that fires twice a
   session. The cost is that the filter compiles on first fire, which is why the load
   fire is at 0ms: the stall lands inside the hero's entrance choreography. */
.warp-rot { position: fixed; inset: 0; z-index: 50; display: none; pointer-events: none;
  transform: rotate(var(--warp-angle)); transform-origin: 50% 50%; }
:root.warp-on .warp-rot { display: block; }
.warp-lens { position: absolute; inset: 0; opacity: 0; pointer-events: none;
  transform-origin: 50% 100%;
  -webkit-mask-image: radial-gradient(62% 30% at 50% 76%, #000 6%, transparent 74%);
  mask-image: radial-gradient(62% 30% at 50% 76%, #000 6%, transparent 74%);
  -webkit-backdrop-filter: blur(var(--warp-blur)) brightness(var(--warp-bright)) url(#warpRefract);
  backdrop-filter: blur(var(--warp-blur)) brightness(var(--warp-bright)) url(#warpRefract); }
:root.warp-on .warp-lens { animation: warpRise var(--warp-travel) linear forwards; }
/* THE TRAILING PASS IS GONE, removed 2026-08-18 on measured performance. It was a
   second full-viewport backdrop-filter layer at 0.55 opacity, staggered 370ms so the
   two bands fused into one thicker pass. At 4x CPU throttle on the scroll-back
   re-fire it cost 18 and 12 frames over 32ms against 1 and 8 with one lens, and a
   hybrid keeping the layer without its SVG filter did not help at all. The cost is
   two full-viewport backdrop-filters existing at once, not the filter.
   Full measurement and the do-not-restore note live in app/styles/warp.css. */
/* Born small and low, widens as it climbs, drains out the top. That growth is why it
   does not read as a printer scan line: full width from frame one IS a scan line. */
@keyframes warpRise {
  0%   { opacity: 0;               transform: translate3d(0, calc(6%    * var(--warp-reach)), 0) scale(0.55); }
  12%  { opacity: var(--warp-op);  transform: translate3d(0, calc(1%    * var(--warp-reach)), 0) scale(0.72); }
  58%  { opacity: var(--warp-op);  transform: translate3d(0, calc(-34%  * var(--warp-reach)), 0) scale(1.00); }
  100% { opacity: 0;               transform: translate3d(0, calc(-104% * var(--warp-reach)), 0) scale(var(--warp-spread)); }
}
/* Someone who asked for less motion should not get the page bent at them, and a
   frozen refraction is worse than none. Off, not reduced. verso.js also bails before
   registering a listener, so this is the belt to that braces. */
@media (prefers-reduced-motion: reduce) {
  .warp-rot { display: none !important; }
}

/* ───────── shared layout ───────── */
.wrap { max-width: 1080px; margin: 0 auto; padding: 0 22px; }
section { padding: clamp(64px, 11vh, 128px) 0; }
/* no leading dot: dot + tiny uppercase + wide tracking is the template/AI eyebrow
   pattern. The wordmark keeps its dot, so that mark stays uniquely the brand's. */
.eyebrow { display: inline-flex; align-items: center; font-size: 11px; font-weight: 500;
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--secondary); margin-bottom: 18px; }
.eyebrow.steel { color: var(--hero-accent); }
/* light-mode AA (4.5:1) nudges for small accent text, same fixes the port applies */
.eyebrow.red { color: color-mix(in srgb, var(--problem-red) 88%, black); }
:root[data-theme="dark"] .eyebrow.red { color: var(--problem-red); }
.eyebrow.green { color: color-mix(in srgb, var(--process-accent) 80%, black); }
:root[data-theme="dark"] .eyebrow.green { color: var(--process-accent); }
.lede { font-size: 17px; line-height: 1.6; color: var(--ink-soft); max-width: 56ch; }

.reveal { opacity: 0; transform: translateY(22px);
  transition: opacity 0.85s var(--ease-soft), transform 0.85s var(--ease-soft); }
.reveal.in { opacity: 1; transform: none; }

/* ───────── wordmark ───────── */
/* 28px, 2026-08-18, up from 20px, and the reason is READABILITY not hierarchy. Enzo's
   father, an older reader, could not pick the mark out at 20px, and the audience here is
   agency principals who skew the same way. The bump is deliberately large, +40%, because
   a change small enough to argue about is one that will have to be made again. Measured
   clear at every width down to Enzo's 763px laptop: 190px of air still sits between the
   wordmark and the centre pill, and the 68px --nav-h holds the 43px line box fine. */
/* ONE KNOB. Change --wm and the whole lockup follows: the letters, the mark, the
   trademark and the gap between them are all ratios of it, so the proportions tuned
   below cannot drift apart. Nothing else in the nav is sized off it.
   The word's THICKNESS is font-weight, and it has exactly two legal values, 400 or
   500 (CLAUDE.md). The MARK's stroke thickness is not adjustable here at all: it is
   artwork, a masked PNG, so changing it means redrawing it in Figma. */
.wordmark { --wm: 24px;
  display: inline-flex; align-items: center; gap: calc(var(--wm) * 0.571); position: relative;
  font-family: var(--display); font-weight: 500; font-size: var(--wm); letter-spacing: -0.015em;
  color: var(--ink);
  transform-origin: left center;
  transition: transform var(--dur-fast) var(--ease-out-strong); }

/* THE MARK REPLACES THE DOT (2026-08-16), matching the port. The steel dot was a
   stand-in from before a logo existed; now there is one, so the slot holds the real
   thing. Enzo's artwork, keyed out of his Figma screenshot, split by flood fill into
   two layers so the middle bar can move on its own.

   TWO PSEUDOS INSTEAD OF MARKUP: this site is five hand-written pages plus a footer
   wordmark, and a real span would mean six edits that a seventh page would forget.
   ::before is a flex item and reserves the space; ::after is absolutely positioned
   directly over it, so they stack in register while only one of them occupies layout.
   Centred with margin-top rather than translateY, deliberately: the launch animation
   below owns `transform`, and a centring transform would fight it.

   SIZED AGAINST THE LETTERS: "verso" is all x-height, so its visible ink is 15.4px at
   28px Satoshi. 21px tall is cap height plus optical compensation for a mark that is
   mostly negative space. Same numbers as the port, both scaled by 1.4 off the 20px
   wordmark they were first drawn against. */
/* THE BOX CARRIES THE ARTWORK'S OWN ASPECT, 184:171 = 1.0760, so `contain` fills
   it exactly and neither dimension letterboxes. Both sites use the same two
   ratios for this reason: they drifted apart on 2026-08-18 (0.857/0.786 here
   against 0.786/0.750 on the pre-launch site, inherited from an old 17x16 vs
   16x15) and the narrower box silently painted a 7% smaller mark, which read as
   the logo being thinner on one site than the other. Same art, same dilation,
   different box. */
.wordmark::before, .wordmark::after {
  content: ""; width: calc(var(--wm) * 0.846); height: calc(var(--wm) * 0.786);
  background: currentColor; }
.wordmark::before {
  -webkit-mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAACrCAYAAADVcVbyAAAFCElEQVR42u3dMY4cVRQF0Fsl2AYhgQmnA1tI3oZJ3BIdshoCO/VekIUh8IRsBgflwCBhbPBopqv7//fOXcG46/r066r/6y/btm3plTdJvo+0yNKw4B/9+1Vgl4zSqWV1IfJWH8/6eY4E5rb8Bfjm2tC8iNofZf3HxV2S3LpAUumz+/eIcmhedCUv9pktX/iNuRlXZOZ+rHe40IsLJ5N+Rne+i9JxbPlVh/8zz2e6D043o0pJve8yonTX/J0uf5JvZnuSaValeEm97yt499lcJso5HtUfipfco/xJ9T7HiNJlZDGmTFrwVRGkarn3KLiSS7kZXMnr5/dZx8hl5/0O75J8ZQY3nlzrGu294eHrAhf3RfNyv57gb3y214OeDndX6D3xNVqVRCafvZ/t+ai+uuS3+fAgi96TfsOuvu7zpae09J5U72sIPpMM9C7w++har414Se/Y0LCz3tcUPPmwiOmG3vTec9xdrvxiq82tQQXfE6BVkT774Sl3kfFxhFe3nczeshdAyyDv3tyMJ/Te4/qsSqXcVfUeqeAjjir0LnDrdhns9eAbvRX8nLduV7fnJMkvVR+8rW7T+Y+V5Gm12XvUEeVaX5dLc72fVl2TP+oRJpf8wfktvWvqPbLgl1Sc3oV3VI18CNUlFD/Su67eowt+CcU76z3yas6zXZ/RjxE8WVS1W26q6z2D4Hsq3lnvH5O86vA2gxkOgj3R++x51UHvWQTfQ3GP5Zu8i2a1Ik25K1+fWQQ/54Whd6M3ia30Vu7K16eb4PRu9h7ItdFLMOnd8Nt1JsEfepHo3fAtvmukQ950/W00W8Ff0vteeZKmr8mbbUS571ft0lzvJ10PGehQcLN34xM0zOBm79KpLji9m59/tHoaSe84CHaoHGM7WgrcOVmMKA/76rWhQcHL/sg82tCg3JUF9+NSwcsKblGVcpcWnN4KXlZweit3acHpreBlBae3cpcQ/FGSP+it4FUF/4nen+Q35a692IreCl52sRW9lbvcXZS/X/1762Riele8i3JQ7in0/sGGB6E3weUzeR0rOglO796H+hI8025ooDfB6d39SHaCS+ndVASnd+nDvQguqbyik+D0Ln24F8Gl9Hp8gtO79NGMBJfSu6kUPBZVZeAX2Ct4jzymt4LTm94KTu9ebzJQcHqX1VvB6V3+PTQKPm5+pnc86IkHO9fS+0BwSdENDQenrAm9FVwmLPjilDWprHccBCv0JrjQm+AKTm+CF8mf9CY4velN8FhURW+C07u63gSnd/mT6whO71Q+NZrg9C4dgtM7lU+NJngsiSW40JvgQm+CC70JLqLg9Ka3ggvBhd5F9VZwIbikw9HbS9wmFOMJwSXTLapa4kGP0JvgQm+C01vBCU5v5SY4vRWc4LEktm+5CU5vgsu98mjgv+0YGx6E3gSX+XKMLWtCb4ILvQlOb3oTXOIlmgoeGxrS4uBWI4rxpPTBrQSPRVX0VvBL57HZW8HpTW8Fpze9FZze9FZweouCp+iGBnrHffC4763gMl/BF5fGiFJZbyE4vQku9Ca4gtOb4MpNb4IrOL0JHhsa6E1wegvBU2NRFb0JTm+Cy//lBb0JTm96E1zoTXCC05vg9BaCVxWc3gSntyj4XXOyFc2IYkwxmhCc4spN8HHzNsmNchO8ag5X+JGn3ApesuS3yq3gVUt+crfEDJ6Cd1fcBiT4kGtCliTfPVDsRbn3yXtnJsuv0zFctQAAAABJRU5ErkJggg==) center / contain no-repeat;
  mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAACrCAYAAADVcVbyAAAFCElEQVR42u3dMY4cVRQF0Fsl2AYhgQmnA1tI3oZJ3BIdshoCO/VekIUh8IRsBgflwCBhbPBopqv7//fOXcG46/r066r/6y/btm3plTdJvo+0yNKw4B/9+1Vgl4zSqWV1IfJWH8/6eY4E5rb8Bfjm2tC8iNofZf3HxV2S3LpAUumz+/eIcmhedCUv9pktX/iNuRlXZOZ+rHe40IsLJ5N+Rne+i9JxbPlVh/8zz2e6D043o0pJve8yonTX/J0uf5JvZnuSaValeEm97yt499lcJso5HtUfipfco/xJ9T7HiNJlZDGmTFrwVRGkarn3KLiSS7kZXMnr5/dZx8hl5/0O75J8ZQY3nlzrGu294eHrAhf3RfNyv57gb3y214OeDndX6D3xNVqVRCafvZ/t+ai+uuS3+fAgi96TfsOuvu7zpae09J5U72sIPpMM9C7w++har414Se/Y0LCz3tcUPPmwiOmG3vTec9xdrvxiq82tQQXfE6BVkT774Sl3kfFxhFe3nczeshdAyyDv3tyMJ/Te4/qsSqXcVfUeqeAjjir0LnDrdhns9eAbvRX8nLduV7fnJMkvVR+8rW7T+Y+V5Gm12XvUEeVaX5dLc72fVl2TP+oRJpf8wfktvWvqPbLgl1Sc3oV3VI18CNUlFD/Su67eowt+CcU76z3yas6zXZ/RjxE8WVS1W26q6z2D4Hsq3lnvH5O86vA2gxkOgj3R++x51UHvWQTfQ3GP5Zu8i2a1Ik25K1+fWQQ/54Whd6M3ia30Vu7K16eb4PRu9h7ItdFLMOnd8Nt1JsEfepHo3fAtvmukQ950/W00W8Ff0vteeZKmr8mbbUS571ft0lzvJ10PGehQcLN34xM0zOBm79KpLji9m59/tHoaSe84CHaoHGM7WgrcOVmMKA/76rWhQcHL/sg82tCg3JUF9+NSwcsKblGVcpcWnN4KXlZweit3acHpreBlBae3cpcQ/FGSP+it4FUF/4nen+Q35a692IreCl52sRW9lbvcXZS/X/1762Riele8i3JQ7in0/sGGB6E3weUzeR0rOglO796H+hI8025ooDfB6d39SHaCS+ndVASnd+nDvQguqbyik+D0Ln24F8Gl9Hp8gtO79NGMBJfSu6kUPBZVZeAX2Ct4jzymt4LTm94KTu9ebzJQcHqX1VvB6V3+PTQKPm5+pnc86IkHO9fS+0BwSdENDQenrAm9FVwmLPjilDWprHccBCv0JrjQm+AKTm+CF8mf9CY4velN8FhURW+C07u63gSnd/mT6whO71Q+NZrg9C4dgtM7lU+NJngsiSW40JvgQm+CC70JLqLg9Ka3ggvBhd5F9VZwIbikw9HbS9wmFOMJwSXTLapa4kGP0JvgQm+C01vBCU5v5SY4vRWc4LEktm+5CU5vgsu98mjgv+0YGx6E3gSX+XKMLWtCb4ILvQlOb3oTXOIlmgoeGxrS4uBWI4rxpPTBrQSPRVX0VvBL57HZW8HpTW8Fpze9FZze9FZweouCp+iGBnrHffC4763gMl/BF5fGiFJZbyE4vQku9Ca4gtOb4MpNb4IrOL0JHhsa6E1wegvBU2NRFb0JTm+Cy//lBb0JTm96E1zoTXCC05vg9BaCVxWc3gSntyj4XXOyFc2IYkwxmhCc4spN8HHzNsmNchO8ag5X+JGn3ApesuS3yq3gVUt+crfEDJ6Cd1fcBiT4kGtCliTfPVDsRbn3yXtnJsuv0zFctQAAAABJRU5ErkJggg==) center / contain no-repeat; }
.wordmark::after {
  position: absolute; left: 0; top: 50%; margin-top: calc(var(--wm) * -0.393);
  -webkit-mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAACrCAYAAADVcVbyAAADfElEQVR42u3dPW4TQRzG4f9GOQlFitROyFlQYmk5U6gpchJAspPCZQoKzjL0NAj8oZl3nt8JovXLo/GusZfWWkmpXbsE3dSLNEvSRb2yqy6G3fxDM/C0XjseUzNwHTvujx3/fZ9SLvTiTaZxJ5/DCW7csXoT3NnWXRRNNe5D2kUnuIHH6k1w447Wm+AGHq03wetid07oTXB605vgo/Wd3gSnN70JTm96E5ze9CY4vWfRm+D0LoLrX3uiN8HpTW+Ci94Epze9Ca4Z9SY4vQkuehNc9Ca46G3gI7Yb4G/czPriOKLkH08OMw+c4PQmuOhNcHrTm+D0dmuQ4PQ2boLT28AJfp5W4yY4vQ2c4DJuA6e3DFz0NnB6G7jobeD0loGL3gY+RnuXwIMexxN6E5zeIji96U1weovg9BbB6U1wetOb4OU/NIjg9D5B26p68TIZuOOJHFEG1FsEp7cITm+CGzi9CW7c9Ca4gdOb4OWxfPn5EYLTWwSntwhOb4LTm94Epze9Cd5Tz/QmOL3pTfDyHxroTXB605vgojfB6S2C01sTCk5vgoveBl4ey5+8jSka+DE90NvA6U1vA6c3vQ2c3vQ2cHqbn4HTWwZOb8028JXeqsp9VN8619vADfy/u62qd585UerAfahKPmxl3AZObxm46G3g9JaB01sGXn56WzPeJmz0Vqrg9Fa04PRWrOD0VrTg9Fas4PRWtOD0Vqzgq5dOyYLTWx7VS1f0preBSwY+vd5bc/Im0/FEBKe3ZhSc3ooVfE9vJQtOb8UKTm9FC05vxQq+9xXIShac3ooVnN6KFpzeihV8pbeSBae3fFyW3hpRcHqL4PTWiILTWwSnt0YUnN4iOL014sB/lJ/dVvARpfnZbaUKvqe3kgf+4Oyt1IHTW9EDp7diB05vRQ+c3ood+EpvVfB9cPe9ZeA+c6IRjyjN2VvJgtNbsYI3l1w+LktvDThweovg9NaIA//mUiv5Loo7J4oVfOcyK1lweitWcHorWnB6K1ZweitacHorVvDVpVWy4PSWR/XSiAN/oreSB37vsip54Hf0VvKbzJuq+mngchfFuOUuylF99hIb+Dl6rD6+BuKrl9gRpQIf1fuGKp39iLIYt9LP4ItxK/1N5qVG/sW49WfXF7xV19wKVPJtwqWq3ur0twGNW919ffKHqvp15LDdAtRf+w3EJ23OU3yx6gAAAABJRU5ErkJggg==) center / contain no-repeat;
  mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAACrCAYAAADVcVbyAAADfElEQVR42u3dPW4TQRzG4f9GOQlFitROyFlQYmk5U6gpchJAspPCZQoKzjL0NAj8oZl3nt8JovXLo/GusZfWWkmpXbsE3dSLNEvSRb2yqy6G3fxDM/C0XjseUzNwHTvujx3/fZ9SLvTiTaZxJ5/DCW7csXoT3NnWXRRNNe5D2kUnuIHH6k1w447Wm+AGHq03wetid07oTXB605vgo/Wd3gSnN70JTm96E5ze9CY4vWfRm+D0LoLrX3uiN8HpTW+Ci94Epze9Ca4Z9SY4vQkuehNc9Ca46G3gI7Yb4G/czPriOKLkH08OMw+c4PQmuOhNcHrTm+D0dmuQ4PQ2boLT28AJfp5W4yY4vQ2c4DJuA6e3DFz0NnB6G7jobeD0loGL3gY+RnuXwIMexxN6E5zeIji96U1weovg9BbB6U1wetOb4OU/NIjg9D5B26p68TIZuOOJHFEG1FsEp7cITm+CGzi9CW7c9Ca4gdOb4OWxfPn5EYLTWwSntwhOb4LTm94Epze9Cd5Tz/QmOL3pTfDyHxroTXB605vgojfB6S2C01sTCk5vgoveBl4ey5+8jSka+DE90NvA6U1vA6c3vQ2c3vQ2cHqbn4HTWwZOb8028JXeqsp9VN8619vADfy/u62qd585UerAfahKPmxl3AZObxm46G3g9JaB01sGXn56WzPeJmz0Vqrg9Fa04PRWrOD0VrTg9Fas4PRWtOD0Vqzgq5dOyYLTWx7VS1f0preBSwY+vd5bc/Im0/FEBKe3ZhSc3ooVfE9vJQtOb8UKTm9FC05vxQq+9xXIShac3ooVnN6KFpzeihV8pbeSBae3fFyW3hpRcHqL4PTWiILTWwSnt0YUnN4iOL014sB/lJ/dVvARpfnZbaUKvqe3kgf+4Oyt1IHTW9EDp7diB05vRQ+c3ood+EpvVfB9cPe9ZeA+c6IRjyjN2VvJgtNbsYI3l1w+LktvDThweovg9NaIA//mUiv5Loo7J4oVfOcyK1lweitWcHorWnB6K1ZweitacHorVvDVpVWy4PSWR/XSiAN/oreSB37vsip54Hf0VvKbzJuq+mngchfFuOUuylF99hIb+Dl6rD6+BuKrl9gRpQIf1fuGKp39iLIYt9LP4ItxK/1N5qVG/sW49WfXF7xV19wKVPJtwqWq3ur0twGNW919ffKHqvp15LDdAtRf+w3EJ23OU3yx6gAAAABJRU5ErkJggg==) center / contain no-repeat; }


/* THE TRADEMARK. Pinned to the top-right corner of the MARK, not the word, so it
   reads as a mark on the logo rather than punctuation after "verso".

   THE OFFSETS ARE MEASURED OFF SATOSHI, not eyeballed. Its trademark glyph is a real
   superscript: ink runs 443 to 723 of a 1000 em, advance 642. At 15px that is 9.63px
   wide with 4.2px of ink, and with line-height 1 the ink top lands 0.1618em below the
   box top, which is the 2.43px below. Left edge is the mark's 22px plus 1.5px of air.
   Vertically it hangs off the mark's own top, found the same way ::after finds the
   mark's centre: 50% less half the 21px mark, then the 2.43px correction. */
.tm { position: absolute; left: calc(var(--wm) * 0.90); top: 50%;
  margin-top: calc(var(--wm) * -0.393 - 0.1618em);
  font-size: calc(var(--wm) * 0.536); line-height: 1; opacity: 0.6; }
/* THE LAUNCH, ported from the port. The middle bar leaves down its OWN slant, fades,
   and returns from the top. The exit vector is measured off the artwork, not guessed:
   the bar's centre runs 61.7px left across 166px of height, 20.38deg from vertical,
   which in percentages of this 16x15 box is -0.35x the y value. Opacity crosses zero
   at the turnaround, so the reposition is never seen. 172ms out on an accelerating
   curve, 258ms back on ease-out-strong. Fires once per hover, re-arms on re-entry. */
@keyframes wmBarLaunch {
  0%     { transform: none; opacity: 1; animation-timing-function: cubic-bezier(0.5, 0, 1, 0.6); }
  40%    { transform: translate(-45%, 130%); opacity: 0; }
  40.01% { transform: translate(45%, -130%); opacity: 0; animation-timing-function: var(--ease-out-strong); }
  100%   { transform: none; opacity: 1; }
}
@media (hover: hover) and (pointer: fine) {
  a.wordmark:hover { transform: scale(1.06); }
  a.wordmark:hover::after { animation: wmBarLaunch 430ms both; }
}
@media (prefers-reduced-motion: reduce) {
  a.wordmark:hover { transform: none; }
  a.wordmark:hover::after { animation: none; }
}
/* still used by the waitlist status dot, which is why it outlived the wordmark dot */
@keyframes dotpulse { 0%,100% { opacity: var(--pulse-min); } 50% { opacity: 1; } }

/* ── THE MARK IN THE CTA · Handle's move (usehandle.ai), same as the port ──
   A slot opens from width 0 while the logo fades and scales up into it on an 80ms
   delay, so the button makes room first and the logo arrives second. One element,
   not their two: both pseudos here are spent on the swirl, so the slot IS the mark,
   a box whose background is currentColor pulled through a fixed-size mask. A fixed
   mask in a box growing from zero reveals from the centre out, which reads the same
   as their centred SVG in a growing window.
   The negative margin cancels the flex `gap`, which applies to a zero-width child
   too and would otherwise push every label sideways permanently. */
.btn { --mark-gap: 9px; }
.btn-sm { --mark-gap: 8px; }
.btn-mark {
  flex: none; width: 0; height: 0.94em;
  margin-right: calc(-1 * var(--mark-gap));
  background: currentColor;
  -webkit-mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAACrCAYAAADVcVbyAAAGqklEQVR42u3dy24cRRTG8a9aymuwQcrCLD2QGWH8GqAgR5pZkpdhYW8nUgILHiOM8ATJswQJHgWQikWC5JjbXPpS5zv/WkZx4uo+/etTp+tSaq1VudqtpE9FS9FKwgB/r/8N/S7V6Jo005cu+QNeJd018DtUkwetub50jSk2RTuf6Ka8aSwYTg3yJvvS3XstFUm75JqPGdxPGr4WTx2C+68c/J/+/O6dbOTl+YL70GvQ9Bju33LwWeLUpSYP7qcuwf1fgofrSBDJq1G/Q/SlO6DT2fLzHxIG9773+CpSHdzxRrWoOHpP0JfuyB/Movnv6P239kG0L5nuN44ve0n1Plbw7Lm5jqicuOgdci5KH/+Oc918p7dlU/QO2Je+5qI4181PeXC/R28Pwd3z8oLeMfvSmU9BnbKht6ngjpKX5HqHnT/TDfwf/oHeFno/iXojhl7w8MggWK+P+JnPAvRr38rQJkBfPp8iRXFJVw5NT64kvTQqfYZOtbqEax+Hbi/lo/ebyHqPKXhUyY/5yFPRu503bJd4FXuf0kUKiDR6TyF4tNl16B18fDTVthE36N10n64c9J5ScKntCVrobVLdKhNvbFVNSoPVrNxp87B2bJ2WYn7GLmmqpRa2blsFz71vDccTNhPESiN7b1bSE3LvIe5Pxy6vJ/0e6N14+tjS7rKrgBd8Qe7d9sNaGtsevAbTe0HlpO21saXB/e8ruffofXot6dJx2WBHmc429z6kT5euS+tKoyeYVPRG7z6KEK0eYTLmgPPxgX9/id5x3uil4TOoKnqj96kl5JYPoRpD8WdS6j1cLt23tSiNnyJY0XuwPkXYbq+0vqq+ZcV3yfU+z7ApUQlwDmxF7977tJS0znD4QISDYFfo3XtArLNsKVeCnORd0TvVZ/kSZWcrNpwkuCe9P1EE7/PGHKrDVtKcAI85dbpD7/9tBHfgt2s2wdE72eG7XaLTztA74dgokuCn3iT0znd0esoqSka9b7NWtqIF+M2Ieju1RdaF0dFSlGNftW4fdkritaOkKCdevKXQ22bLEHJwhZun8cxs7WjYYwRbSFEcj/+jcmIu+C7xxvzonUDwM0k/7RkMr9A7r95RBf95z7/3KrHebgPlVILvE4zojd7WVZTMetPMBXc8Xwe9EVzHfvbdsgIJwaMIjt7obS04etNsBUdv9LYQ/Ay9ac4B/lWS5Wgzo4e1kKKc9io+9AJeS3pudB4N6YnxZKtj9H4u9E5xRGQXtFKyO2FguTTLveei2VVRpLf7W8+Sz/dufeX/F5K+I8DzHhtO7k2KkmpBwyHBvRETxBAcvfPqnU1wJ72X6I3g6J1c70yCb7Pu7JR9cUYWwdE7od5ZBEfvxBPEMgiO3kn1ziA4eief3usuOHon1ttdcPRmcYZ1gM+NAoKHlQBXpC99s8QPKzk4uXeoKbGFAFeTu89GCQinh5UATzCpqqA3Obg4L4jcmwCPp/ch7WsqJ6QokQKc3BvB0VueZU4ER2/0RnClOXobvQlwRak2uOktAhy90ZsAR2/0JsDRG71TB7iT3r+hNwHurPcj9CbAXfXeojcfevTgS9+awSUzBl0FX6M3emc9yhu90Tu04BW9adlOWUNv9LYQvBrtqroUDcHRG72zCI7eNGvB0Ru9bQXfcqIBzVlw9EZvW8HRm2YtOHqjt63gzNOgWQvuJN5G0gV6I7ir3heiIbjxpKo5eiO4Anzp25mtPkJw9EZvBM/R0JsAt9Z7ZjRQLgQ4Db0JcPQOOFAuYpBJgJ+4Jzaf5RHcVu8zMUEMwdEbvbMIvmFfPvR2Fhy90dtW8C16o7ez4OiN3raCozeLM6wDfG4UEAyUCXBrvS/QmwB31ZtUiwBXlHkas+QPq6iiiMoJCxoI8IgBXpI/rKQo5nqTeyM4eqM3gkf80pd9oIzg6I3eBHieeRqVygkpSoiHPPlAmQDvob3mUpN7O6co5N7k3raC33KZ0dtZcPRGb1vB0Ru9rQN8YaT3hsoJKcpDvRekJ9S9XQV3Cu4teiO4HszTWKM3ersK7hTc1+iN4JQG0TuN4FcEBHo7B/gnXFZR9zYO8I/RG72dc/DHkn4xCvDKw4rg99uvBAR6s+BhnLZq5N9gQpVZmfBLSd8YBUTlTYTg99u3ZtqtCG4Eb0W+oV7ld5LOCW4En/LmDJmnziYY5BHcAQaZY92kmxEGYWMF+Y7gZl+UKaUbMl1ZSXpBaMYrExZJPw4QDGWiT+VlILUJ7sB18Pm7m/hhT4H9ooHqRpH0UQ99ocY9QPsTx2t/geV1cBQAAAAASUVORK5CYII=) center / auto 0.94em no-repeat;
  mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAACrCAYAAADVcVbyAAAGqklEQVR42u3dy24cRRTG8a9aymuwQcrCLD2QGWH8GqAgR5pZkpdhYW8nUgILHiOM8ATJswQJHgWQikWC5JjbXPpS5zv/WkZx4uo+/etTp+tSaq1VudqtpE9FS9FKwgB/r/8N/S7V6Jo005cu+QNeJd018DtUkwetub50jSk2RTuf6Ka8aSwYTg3yJvvS3XstFUm75JqPGdxPGr4WTx2C+68c/J/+/O6dbOTl+YL70GvQ9Bju33LwWeLUpSYP7qcuwf1fgofrSBDJq1G/Q/SlO6DT2fLzHxIG9773+CpSHdzxRrWoOHpP0JfuyB/Movnv6P239kG0L5nuN44ve0n1Plbw7Lm5jqicuOgdci5KH/+Oc918p7dlU/QO2Je+5qI4181PeXC/R28Pwd3z8oLeMfvSmU9BnbKht6ngjpKX5HqHnT/TDfwf/oHeFno/iXojhl7w8MggWK+P+JnPAvRr38rQJkBfPp8iRXFJVw5NT64kvTQqfYZOtbqEax+Hbi/lo/ebyHqPKXhUyY/5yFPRu503bJd4FXuf0kUKiDR6TyF4tNl16B18fDTVthE36N10n64c9J5ScKntCVrobVLdKhNvbFVNSoPVrNxp87B2bJ2WYn7GLmmqpRa2blsFz71vDccTNhPESiN7b1bSE3LvIe5Pxy6vJ/0e6N14+tjS7rKrgBd8Qe7d9sNaGtsevAbTe0HlpO21saXB/e8ruffofXot6dJx2WBHmc429z6kT5euS+tKoyeYVPRG7z6KEK0eYTLmgPPxgX9/id5x3uil4TOoKnqj96kl5JYPoRpD8WdS6j1cLt23tSiNnyJY0XuwPkXYbq+0vqq+ZcV3yfU+z7ApUQlwDmxF7977tJS0znD4QISDYFfo3XtArLNsKVeCnORd0TvVZ/kSZWcrNpwkuCe9P1EE7/PGHKrDVtKcAI85dbpD7/9tBHfgt2s2wdE72eG7XaLTztA74dgokuCn3iT0znd0esoqSka9b7NWtqIF+M2Ieju1RdaF0dFSlGNftW4fdkritaOkKCdevKXQ22bLEHJwhZun8cxs7WjYYwRbSFEcj/+jcmIu+C7xxvzonUDwM0k/7RkMr9A7r95RBf95z7/3KrHebgPlVILvE4zojd7WVZTMetPMBXc8Xwe9EVzHfvbdsgIJwaMIjt7obS04etNsBUdv9LYQ/Ay9ac4B/lWS5Wgzo4e1kKKc9io+9AJeS3pudB4N6YnxZKtj9H4u9E5xRGQXtFKyO2FguTTLveei2VVRpLf7W8+Sz/dufeX/F5K+I8DzHhtO7k2KkmpBwyHBvRETxBAcvfPqnU1wJ72X6I3g6J1c70yCb7Pu7JR9cUYWwdE7od5ZBEfvxBPEMgiO3kn1ziA4eief3usuOHon1ttdcPRmcYZ1gM+NAoKHlQBXpC99s8QPKzk4uXeoKbGFAFeTu89GCQinh5UATzCpqqA3Obg4L4jcmwCPp/ch7WsqJ6QokQKc3BvB0VueZU4ER2/0RnClOXobvQlwRak2uOktAhy90ZsAR2/0JsDRG71TB7iT3r+hNwHurPcj9CbAXfXeojcfevTgS9+awSUzBl0FX6M3emc9yhu90Tu04BW9adlOWUNv9LYQvBrtqroUDcHRG72zCI7eNGvB0Ru9bQXfcqIBzVlw9EZvW8HRm2YtOHqjt63gzNOgWQvuJN5G0gV6I7ir3heiIbjxpKo5eiO4Anzp25mtPkJw9EZvBM/R0JsAt9Z7ZjRQLgQ4Db0JcPQOOFAuYpBJgJ+4Jzaf5RHcVu8zMUEMwdEbvbMIvmFfPvR2Fhy90dtW8C16o7ez4OiN3raCozeLM6wDfG4UEAyUCXBrvS/QmwB31ZtUiwBXlHkas+QPq6iiiMoJCxoI8IgBXpI/rKQo5nqTeyM4eqM3gkf80pd9oIzg6I3eBHieeRqVygkpSoiHPPlAmQDvob3mUpN7O6co5N7k3raC33KZ0dtZcPRGb1vB0Ru9rQN8YaT3hsoJKcpDvRekJ9S9XQV3Cu4teiO4HszTWKM3ersK7hTc1+iN4JQG0TuN4FcEBHo7B/gnXFZR9zYO8I/RG72dc/DHkn4xCvDKw4rg99uvBAR6s+BhnLZq5N9gQpVZmfBLSd8YBUTlTYTg99u3ZtqtCG4Eb0W+oV7ld5LOCW4En/LmDJmnziYY5BHcAQaZY92kmxEGYWMF+Y7gZl+UKaUbMl1ZSXpBaMYrExZJPw4QDGWiT+VlILUJ7sB18Pm7m/hhT4H9ooHqRpH0UQ99ocY9QPsTx2t/geV1cBQAAAAASUVORK5CYII=) center / auto 0.94em no-repeat;
  opacity: 0;
  transform: scale(0.75);
  /* Handle's own curve. Deliberately not --ease-out-strong: that curve is tuned for
     transforms, and on a width transition, which is layout, its late rush snaps. */
  transition: width 300ms cubic-bezier(0, 0, 0.2, 1),
              margin-right 300ms cubic-bezier(0, 0, 0.2, 1),
              opacity 260ms cubic-bezier(0, 0, 0.2, 1) 80ms,
              transform 260ms cubic-bezier(0, 0, 0.2, 1) 80ms;
}
@media (hover: hover) and (pointer: fine) {
  .btn:hover .btn-mark, .btn:focus-visible .btn-mark {
    width: 1.01em; margin-right: 0; opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .btn-mark { transition: opacity 160ms linear; transform: none; }
}

/* ───────── nav ─────────
   Persistent and TRANSPARENT, floating over the page, exactly as the port has it:
   "Logo left, links in a standing glass pill (centered), CTA right." There is no
   bar background and no border, at any scroll position. The only frosted surface
   is the centre pill itself. */
/* EDGE ALIGNED AT EVERY SCROLL POSITION. Enzo, 2026-08-29: "push the CTA and the
   wordmark off to the sides rather than have them get there through scrolling."
   The nav used to hold the 1080px centre column at rest and spread to the edges
   past 60px of scroll, animating its own padding to get there. Two problems with
   that, and the second is why it had to go:
     - the spread fired on any scroll, including the involuntary one, so the
       wordmark drifted sideways for no reason the reader had caused;
     - it collided with the opener. The nav slides DOWN as the splash lifts, and
       if the padding is also travelling the wordmark moves on both axes at once,
       which is what read as broken.
   Holding the edge position permanently removes the whole class of problem: there
   is no second state to get to, so there is nothing to get there wrongly. */
.nav { position: fixed; top: 0; left: 0; right: 0; z-index: 200; height: var(--nav-h);
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  padding: 0 clamp(20px, 2.4vw, 36px); background: transparent; }
/* AT THE TOP the bar holds the 1080px centre column, in line with .wrap, because
   that is the hero's own measure and the nav should read as part of it. Once the
   page moves it spreads to the viewport edges the way the main site's nav sits
   (h9c.css `.nav`, same clamp), so the wordmark and the two CTAs stop sitting over
   the content sliding under them.

   PADDING, NOT max-width, and the resting value is deliberately the long form
   rather than `max-width: 1080px; margin: 0 auto; padding: 0 22px`. Percentage
   padding on a fixed element resolves against the initial containing block, which
   is the same box max-width was capping, so the two are pixel-identical at rest:
   measured 230.33px against .wrap's own 230.33px at 1497px of ICB. The difference
   is that this is one animatable property instead of a box width and a padding
   moving against each other, and it never has to name a full-width value, so the
   scrollbar never enters the arithmetic. `max-width: 1080px -> 100%` was the
   obvious alternative and it is worse on both counts.

   The centre pill is justify-self:center in both states, so only the outer two
   move: the pill holds still as the anchor they move away from. Measured travel
   for the wordmark: 398px at 1920 wide, 194px at 1512, 84px at 1280, and 2px on
   Enzo's 763px laptop, where there is no middle left to sit in and the two states
   are meant to converge. Between roughly 1080 and 1150 the clamp's 2.4vw runs a
   couple of px WIDER than the resting 22px, so the bar creeps 2px inward there
   instead of outward. Known, measured, and left alone: capping it costs a
   min(max(), clamp()) nobody will be able to read for 2px in a 70px-wide band. */
/* .nav.wide retired 2026-08-29: its padding is now the nav's only padding. */
.nav-left { justify-self: start; }
.nav-mid { justify-self: center; }
.nav-right { justify-self: end; display: flex; align-items: center; gap: 18px; }
.nav-link { font-size: 14px; font-weight: 500; color: var(--ink-soft);
  transition: color var(--dur-fast) ease; white-space: nowrap; }
.nav-link:hover, .nav-link[aria-current="page"] { color: var(--ink); }

/* ───────── buttons ───────── */
/* pill, per the port. NOT --shadow-btn or --shadow-card: both lead with a hard
   `0 4px 0 -2px` / `0 -2px 0` line, which on a 999px pill renders as a pale slab
   hugging the curve rather than an edge highlight. */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  padding: 14px 26px; border: 0; border-radius: 999px; cursor: pointer;
  font: 500 15px var(--sans); background: var(--hero-accent); color: var(--on-accent);
  box-shadow: var(--shadow-nav-cta); white-space: nowrap;
  transition: transform var(--dur-fast) var(--ease-out-strong), background-color var(--dur-fast) ease; }
.btn .arrow { transition: transform var(--dur-fast) ease; }
@media (hover: hover) {
  .btn:hover { background: var(--hero-accent-soft); transform: translateY(-2px); }
  .btn:hover .arrow { transform: translateX(4px); }
}
.btn:active { transform: scale(0.98); }
.btn-sm { padding: 10px 18px; font-size: 14px; gap: 8px; }

/* secondary: outlined, for the waitlist slot so it never competes with the demo CTA */
.btn-quiet { background: transparent; color: var(--ink);
  border: 1px solid var(--rule); box-shadow: none; }
@media (hover: hover) {
  .btn-quiet:hover { background: transparent; border-color: var(--hero-accent);
    color: var(--hero-accent); transform: translateY(-1px); }
}

/* ============================================================================
   THE DEMO CTA · frosted glass + the presence swirl on hover

   The surface is the port's standing nav pill, token for token: translucent
   paper-elev, a light-catch border, and a real backdrop blur. Ink text, not
   on-accent, because there is no accent fill under it any more.

   The hover state is the sim's presence signal, the same one the verso mark wears
   while it is working. Ported rather than reinvented, including its reasoning:

   - TWO counter-rotating conic gradients. One alone is a pinwheel and reads as a
     loading spinner; two turning against each other never repeat and read liquid.
   - Palette is red / yellow / orange / blue ONLY (Enzo, 2026-08-08). No green, no
     purple, no full spectrum. The near-chromaless `nil` stops exist because every
     hue path from red to blue runs through green or purple, so the two families
     never blend into each other, they fade out and the other rises from the base.
   - oklch, so the warm run blends perceptually instead of dipping dark.
   - 1.62 speed ratio, deliberately not an integer, or the two layers would fall
     back into the same relative position and the loop would become visible.
   - The pseudos exist at ALL times at opacity 0. Scoped to :hover they would stop
     matching on mouse-out and vanish with nothing to animate, which is exactly the
     abrupt disappearance to avoid.
   - Transitions, not keyframes, so a re-trigger retargets from wherever it is
     instead of restarting from zero.
   - Paused at rest rather than absent, so the next hover picks the angle up where
     the last one left it instead of snapping back to zero mid-fade. Longhand only:
     an `animation:` shorthand silently resets play-state.
   ========================================================================== */
.btn-glass, .card-swirl {
  --ntl-a: 0.50;        /* the colours' own alpha: light landing on a transparent
                           solid, not paint filling the pill */
  /* SECOND LAYER RUNS WEAKER, and this is a scale fix the sim does not need.
     The `nil` crossings stop green appearing inside one layer's hue path, but two
     layers at equal alpha still stack, and translucent yellow over translucent cyan
     composites to green. On a 20px tile the blur buries it. On a 150px pill it
     resolves into a visible mint patch, and green is banned in this palette. At
     0.26 layer B tints layer A rather than mixing with it as an equal, so the
     counter-rotation still reads liquid but the overlap never lands on a hue. */
  --ntl-a2: 0.26;
  --ntl-chroma: 1;
  --ntl-soft: 9px;      /* blur. Also what kills the conic's hard 0deg seam, which
                           is otherwise a visible spoke. Well above the sim's 3px
                           because this surface is far larger than a 20px tile. */
  --ntl-speed: 9s;
  /* ENTER FAST, LEAVE SLOWER, inverting the usual rule. That rule protects a user
     waiting on a dismissal; nothing waits on this. Arriving is news, so it is
     prompt. Leaving wants an even drain: measured, a strong ease-out on the way
     out dropped opacity 1.00 to 0.30 in the first 100ms then crawled, which reads
     as a snap with a long dead tail. So strong ease-out in, even curve out. */
  --ntl-in: 220ms;
  --ntl-out: 400ms;
  --ntl-ease: cubic-bezier(0.23, 1, 0.32, 1);
  --ntl-ease-leave: cubic-bezier(0.4, 0, 0.2, 1);
  /* Alpha 0, not a low-alpha grey: a grey crossing stop washes the surface toward
     mud. At 0 the crossing is genuinely the base showing through. Chroma stays ~0
     so interpolation passes through grey and can never put green or purple on screen. */
  --ntl-nil: oklch(0.76 0.010  90 / 0);
  --ntl-yel: oklch(0.90 calc(0.170 * var(--ntl-chroma))  96 / var(--ntl-a));
  --ntl-orn: oklch(0.79 calc(0.175 * var(--ntl-chroma))  62 / var(--ntl-a));
  --ntl-red: oklch(0.63 calc(0.205 * var(--ntl-chroma))  28 / var(--ntl-a));
  --ntl-blu: oklch(0.55 calc(0.175 * var(--ntl-chroma)) 258 / var(--ntl-a));
  --ntl-cyn: oklch(0.74 calc(0.145 * var(--ntl-chroma)) 226 / var(--ntl-a));

  position: relative; overflow: hidden; isolation: isolate;
}
/* THE PILLAR CARDS BORROW THE SAME SIGNAL, re-aimed at the text half.
   A filler child rather than .card-body's own pseudos: the pseudos are free, but
   the token block is not, and duplicating it is how two copies of one signal drift
   apart. Sharing the selector keeps ONE definition, so tuning the CTA tunes these. */
.card-swirl { position: absolute; inset: 0; z-index: -1; pointer-events: none;
  /* the two knobs a card body needs that a 150px pill does not: more blur, because
     the conic's 0deg seam is a visible spoke once the surface is this wide, and a
     slower turn, because motion under a paragraph reads busy at the pill's speed */
  --ntl-soft: 26px; --ntl-speed: 14s; }
/* DARK ONLY. Light keeps brightness(1) and chroma 1, so the look Enzo approved is
   untouched byte for byte. In dark the swirl has to sit BELOW the card instead of
   above it: dark's colours are mid-lightness and lift a dark card UP toward the
   bone text on it rather than away from it.

   RETUNED 2026-08-23 to `dim 1.00 / chroma 1.16 / alpha 0.83`. ENZO'S CALL, MADE
   AGAINST THE MEASUREMENT AND REAFFIRMED. He is the decision maker on this and it
   is his hover effect; the numbers are recorded here so nobody re-derives them or
   "fixes" this rule by accident.

   Worst pixel inside the hovered card-body paragraph, worst of 8 frozen rotation
   phases, measured on real rendered pixels:

       dim /chroma/alpha    pre-launch    next port
       0.45 / 1.00 / 0.50      9.15          6.93     what shipped before today
       0.45 / 1.16 / 0.83      9.03          6.29
       0.50 / 1.16 / 0.83      8.25          5.51
       0.60 / 1.16 / 0.83      6.82          4.30 FAIL
       0.70 / 1.16 / 0.83      5.72          3.38 FAIL
       1.00 / 1.16 / 0.83      3.44 FAIL     1.81 FAIL   <- SHIPPED

   So this ships BELOW AA on the hovered card body: 3.44 on the pre-launch site and
   1.81 in the port, where 25% of the paragraph is under 4.5. It is a hover state on
   body copy, not a resting one, and the text collapses to --ink while it shows,
   which is what keeps it as high as it is.

   Chroma and alpha are nearly free on contrast; brightness is the expensive lever.
   Same finding as 2026-08-21 and 2026-08-19 before it. If this ever needs to come
   back under AA without losing the look, MOVE THE DIM, not the colour: 0.50 clears
   both sites at this exact chroma and alpha.

   THE PORT IS THE BINDING SITE, and it only became so today. Its card body was
   opaque paper until the glass dropped to x0.15; at 15% the swirl composites over
   the contour field instead of over solid paper, which cost it roughly a full stop.
   A value measured on the pre-launch site alone reads a stop better than the truth.

   MEASURE WITH THE ANIMATION FROZEN. The swirl spins, so a single screenshot
   samples one rotation phase: dim 0.70 on the port read 4.66, then 3.40, then 3.86
   on three passes of the same code before the phases were pinned. Every number in
   project_vista_todos item 15 was taken that way AND predates the contour field and
   the x0.15 glass, so all of them are stale. The rig that does it properly is
   scratchpad/swirl-final.js from this session. */
:root[data-theme="dark"] .card-swirl {
  --ntl-dim: 1.00;
  --ntl-chroma: 1.16;
  --ntl-a: 0.83; }

/* ── THE SWIRL'S CONTRAST DEBT, PAID · added 2026-08-19 ──────────────────────
   A swirl behind body copy drops it under AA while it shows. Measured against
   the worst hue patch of the two layers: light --ink-soft fell 5.96 -> 2.26 and
   the steel link to 2.39; dark was worse, --ink-soft 1.28 and even the bone
   HEADING 2.12, because the swirl's colours are mid-lightness and lift a dark
   card UP toward bone text instead of away from it.

   A mask was the first idea and it does not survive contact: the words cover
   ~60%% of the body with 21-27px of clearance, so masking them out leaves a 24px
   rim, which is a border glow and not the effect Enzo approved.

   So: while the swirl is up, every piece of text in the body collapses to --ink,
   the highest-contrast token each theme owns. Done by re-pointing the tokens on
   the body, so heading, copy, tag and link all follow without a rule each.
   Light lands at 6.37 and needs nothing else. Dark still fails at 6.24-on-paper
   because there is no text lighter than bone to reach for, which is why the
   swirl itself has to move there, see --ntl-dim below.
   ──────────────────────────────────────────────────────────────────────────── */
.card:hover .card-body, .card:focus-visible .card-body {
  --ink-soft: var(--ink);
  --hero-accent: var(--ink);
  --hero-accent-soft: var(--ink);
}
/* The swirl resolves in over --ntl-in; a hard colour snap under a fading wash
   reads as a glitch. Symmetric here on purpose: the swirl's asymmetry protects
   its opacity drain, and a two-token colour step has nothing to drain. */
.card-body h3, .card-body p, .card-body .card-tag, .card-body .card-cta {
  transition: color 220ms cubic-bezier(0.23, 1, 0.32, 1);
}
.btn-glass {
  background: color-mix(in srgb, var(--paper-elev) clamp(0%, calc(55% * var(--glass-k)), 100%), transparent);
  border: 1px solid var(--glass-border);
  color: var(--ink);
  box-shadow: none;
  -webkit-backdrop-filter: blur(calc(12px * var(--glass-blur-k))) brightness(var(--card-lift));
  backdrop-filter: blur(calc(12px * var(--glass-blur-k))) brightness(var(--card-lift));
}
/* isolation makes .btn-glass the stacking context, so a z-index:-1 pseudo paints
   ABOVE the glass background but BELOW the inline label. That is the layer we want:
   glass, then swirl, then text. */
.btn-glass::before,
.btn-glass::after,
.card-swirl::before,
.card-swirl::after {
  content: ''; position: absolute; pointer-events: none; z-index: -1;
  /* The hub has to sit INSIDE the pill for all 360 degrees of the palette to be
     present at once. Pushed far out, the surface samples one enormous band and goes
     flat yellow. 200% keeps the wheel covering the whole shape. */
  width: 200%; height: 200%; inset: auto;
  /* RESTING = gone: transparent, slightly small, over-blurred. Not scale(0),
     because nothing in the real world appears out of nothing; 0.86 still has a
     shape. The extra blur means the swirl RESOLVES INTO FOCUS rather than cutting in. */
  opacity: 0;
  scale: 0.86;
  filter: blur(calc(var(--ntl-soft) * 2.6)) brightness(var(--ntl-dim, 1));
  transition: opacity var(--ntl-out) var(--ntl-ease-leave),
              scale   var(--ntl-out) var(--ntl-ease-leave),
              filter  var(--ntl-out) var(--ntl-ease-leave);
  animation: ntlSpin var(--ntl-speed) linear infinite;
  animation-play-state: paused;
}
.btn-glass::before,
.card-swirl::before {
  left: -70%; top: -65%;    /* hub at (30%, 35%) of the pill */
  /* Angles are explicit because even spacing gives every colour an equal 45deg
     wedge, and equal wedges are exactly what reads as chunks: the warm family wants
     a long smooth sweep, the crossings want to be short. */
  background: conic-gradient(from 0deg in oklch,
    var(--ntl-yel)   0deg, var(--ntl-orn)  34deg, var(--ntl-red)  72deg,
    var(--ntl-nil) 104deg, var(--ntl-nil) 126deg,
    var(--ntl-blu) 162deg, var(--ntl-cyn) 200deg, var(--ntl-blu) 238deg,
    var(--ntl-nil) 268deg, var(--ntl-nil) 292deg,
    var(--ntl-red) 322deg, var(--ntl-orn) 344deg, var(--ntl-yel) 360deg);
}
.btn-glass::after,
.card-swirl::after {
  left: -30%; top: -35%;    /* hub at the opposite corner, (70%, 65%) */
  /* Different start angle and the families swapped round the wheel, so the two
     layers cannot fall into step and beat against each other. No blend mode: two
     translucent sheets simply stack, which is what light through layered glass
     does. `screen` bleaches to white, `overlay` makes mud and olive, and olive is
     a green this palette is not allowed to have. */
  background: conic-gradient(from 128deg in oklch,
    oklch(0.55 calc(0.175 * var(--ntl-chroma)) 258 / var(--ntl-a2))   0deg,
    oklch(0.74 calc(0.145 * var(--ntl-chroma)) 226 / var(--ntl-a2))  40deg,
    oklch(0.55 calc(0.175 * var(--ntl-chroma)) 258 / var(--ntl-a2))  78deg,
    var(--ntl-nil) 108deg, var(--ntl-nil) 130deg,
    oklch(0.63 calc(0.205 * var(--ntl-chroma))  28 / var(--ntl-a2)) 166deg,
    oklch(0.79 calc(0.175 * var(--ntl-chroma))  62 / var(--ntl-a2)) 204deg,
    oklch(0.90 calc(0.170 * var(--ntl-chroma))  96 / var(--ntl-a2)) 242deg,
    var(--ntl-nil) 272deg, var(--ntl-nil) 296deg,
    oklch(0.74 calc(0.145 * var(--ntl-chroma)) 226 / var(--ntl-a2)) 324deg,
    oklch(0.55 calc(0.175 * var(--ntl-chroma)) 258 / var(--ntl-a2)) 360deg);
  animation-duration: calc(var(--ntl-speed) * 1.62);
  animation-direction: reverse;
}
/* :focus-visible as well as :hover, so the signal is reachable from the keyboard */
@media (hover: hover) {
  .btn-glass:hover { background: color-mix(in srgb, var(--paper-elev) clamp(0%, calc(62% * var(--glass-k)), 100%), transparent);
    transform: translateY(-2px); }
}
.btn-glass:hover::before, .btn-glass:hover::after,
.btn-glass:focus-visible::before, .btn-glass:focus-visible::after,
/* the card is the trigger, not the body: hovering the illustration half
   lights the text half, so the whole card answers as one surface. */
.card:hover .card-swirl::before, .card:hover .card-swirl::after,
.card:focus-visible .card-swirl::before, .card:focus-visible .card-swirl::after {
  opacity: 1;
  scale: 1;
  filter: blur(var(--ntl-soft)) brightness(var(--ntl-dim, 1));
  animation-play-state: running;
  transition: opacity var(--ntl-in) var(--ntl-ease),
              scale   var(--ntl-in) var(--ntl-ease),
              filter  var(--ntl-in) var(--ntl-ease);
}
.btn-glass:active { transform: scale(0.98); }
/* `rotate`, the independent transform property, NOT `transform: rotate()`: the
   entrance transitions `scale` on these same elements, and one `transform` cannot
   be driven by a keyframe and a transition at once. Independent properties compose. */
@keyframes ntlSpin { to { rotate: 360deg; } }

/* ───────── glass card ───────── */
.card-glass { background: var(--card-glass); border: 1px solid var(--card-glass-line);
  border-radius: 14px; padding: 26px 24px; box-shadow: var(--shadow-card);
  -webkit-backdrop-filter: blur(calc(14px * var(--glass-blur-k))) brightness(var(--card-lift));
  backdrop-filter: blur(calc(14px * var(--glass-blur-k))) brightness(var(--card-lift)); }

/* ───────── footer ───────── */
footer { padding: clamp(64px, 10vh, 110px) 0 40px; border-top: 1px solid var(--hairline); }
.foot-line { font-family: var(--display); font-weight: 500; letter-spacing: -0.03em;
  font-size: clamp(34px, 6vw, 64px); line-height: 1.05; margin-bottom: 48px; }
.foot-meta { display: flex; align-items: center; justify-content: space-between; gap: 16px 22px;
  font-size: 13px; color: var(--secondary); flex-wrap: wrap; }
.foot-links { display: flex; gap: 20px; flex-wrap: wrap; }
.foot-links a { color: var(--secondary); transition: color var(--dur-fast) ease; }
.foot-links a:hover { color: var(--ink); }

/* ───────── theme toggle ─────────
   Two aria-pressed buttons rather than one relabelled control, so the current mode
   is announced rather than inferred. Same component the port ships. */
.theme-toggle { display: inline-flex; align-items: center; gap: 2px; padding: 3px;
  background: color-mix(in srgb, var(--paper-elev) 60%, transparent);
  border: 1px solid var(--hairline); border-radius: 999px; }
.theme-toggle-btn { display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border: none; background: none; color: var(--secondary);
  border-radius: 999px; cursor: pointer;
  transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease; }
.theme-toggle-btn:hover { color: var(--ink); }
.theme-toggle-btn.active { background: var(--hero-accent); color: var(--on-accent); }
.theme-toggle-btn svg { stroke: currentColor; }

/* ───────── interior page head (about / waitlist / legal / demo) ───────── */
.page-head { padding: calc(var(--nav-h) + clamp(48px, 9vh, 96px)) 0 clamp(24px, 4vh, 40px); }
.page-head h1 { font-size: clamp(34px, 5vw, 58px); max-width: 17ch; }
.page-head .lede { margin-top: 20px; font-size: 18px; max-width: 60ch; }

@media (max-width: 700px) {
  .nav { grid-template-columns: auto 1fr; row-gap: 0; }
  .nav-mid { justify-self: end; grid-column: 2; }
  /* below 700 the right cluster would overflow; the demo CTA is the one that has to
     survive, so About and the waitlist fall back to the footer links. */
  .nav-right { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .mesh .blob, .hero-cue, .hero-cue::after, .wordmark::before { animation: none !important; }
  .btn-glow::before, .btn-glow::after { animation: none !important; }
  .co, .hero h1 .ln span { animation: none; opacity: 1; transform: none; }
  .reveal { transition: none; opacity: 1; transform: none; }
  /* the nav has no transition left to kill since it stopped moving on scroll
     2026-08-29; this stays as a guard in case one is ever added back */
  .nav { transition: none; }
  html { scroll-behavior: auto; }
}

/* ============================================================================
   PAGE SECTIONS
   ========================================================================== */

/* ───────── hero ───────── */
.hero { min-height: 100dvh; display: flex; flex-direction: column; position: relative;
  padding: var(--nav-h) 0 0; }
.hero > .wrap { width: 100%; } /* flex column + margin auto shrink-wraps otherwise */
.hero-core { flex: 1; display: flex; flex-direction: column; justify-content: center;
  align-items: center; text-align: center; padding: 40px 0 96px; }

/* ───────── early-access line ─────────
   PILL AND CHIP BOXES ARE BANNED HERE (Enzo, 2026-08-18): a bordered rounded
   capsule floating over a hero is the generic agent-coded website signature, and
   this site is not allowed to look like one. The first version of this element
   was exactly that and was cut on sight.

   So: plain display type in ink, no surface, no border, no dot, no tracking. It
   reads as a subheading standing over the headline rather than as a badge
   decorating it. Satoshi and --ink to match the h1 it sits above, sized well
   under it so the headline still leads. */
.hero-early { font-family: var(--display); font-weight: 500; color: var(--ink);
  font-size: clamp(15px, 1.5vw, 18px); letter-spacing: -0.01em; margin-bottom: 16px; }
.hero h1 { font-size: clamp(36px, 6.5vw, 76px); letter-spacing: -0.03em; }
.hero h1 .ln { display: block; overflow: hidden; }
.hero h1 .ln span { display: block; }
.hero-sub { margin-top: 26px; font-size: clamp(16px, 1.6vw, 18px); line-height: 1.7;
  color: var(--ink-soft); max-width: 54ch; }
.hero-sub .nm, .gate .nm { color: var(--ink); font-weight: 500; }
.hero-actions { margin-top: 38px; display: flex; align-items: center; gap: 20px;
  flex-wrap: wrap; justify-content: center; }
.hero-note { font-size: 13px; color: var(--secondary); }

/* scroll cue: dotted thread drifting down, gone once you move */
.hero-cue { position: absolute; bottom: 26px; left: 50%; transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 10px; z-index: 2;
  font-family: var(--sans); font-size: 9px; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--ink-soft); transition: opacity 0.45s ease; pointer-events: none; }
.hero-cue::after { content: ""; width: 1px; height: 34px;
  background: repeating-linear-gradient(180deg, currentColor 0 2px, transparent 2px 7px);
  animation: cue-drift 2.8s linear infinite; }
.hero-cue.gone { opacity: 0; }
@keyframes cue-drift { from { background-position: 0 0; } to { background-position: 0 14px; } }
.hero-cue { animation: cuein 1.2s var(--ease-out-strong) 1.9s backwards; }
@keyframes cuein { from { opacity: 0; } to { opacity: 1; } }

/* cold-open choreography */
.co { opacity: 0; animation: rise 0.9s var(--ease-out-strong) forwards; }
.co-1 { animation-delay: 0.15s; }
.co-2 { animation-delay: 0.45s; }
.co-5 { animation-delay: 1.35s; }
.co-6 { animation-delay: 1.75s; }
@keyframes rise { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
.hero h1 .ln span { transform: translateY(110%); animation: lift 0.95s var(--ease-out-strong) forwards; }
.hero h1 .ln:nth-child(1) span { animation-delay: 0.65s; }
.hero h1 .ln:nth-child(2) span { animation-delay: 0.78s; }
@keyframes lift { to { transform: none; } }

/* ───────── what it is ───────── */
/* CENTRED, and it is the only left-to-centre exception on the page, deliberately.
   Every other left-aligned section (.pain, .feat) opens with this exact shape and
   then fills the 1080 with a grid. This one has nothing under it, so left-aligning
   it promised a structure that never arrived and read as a block shoved off-axis.
   Centred, it continues the hero's axis and .pain going left becomes the signal
   that the page is getting into the detail. */
.what { text-align: center;
  /* asymmetric ON PURPOSE: the top follows the hero this section continues, so it
     is tight. The bottom precedes a real topic change, so it keeps the section
     default. Uniform padding is what made the lightest section read as the
     emptiest. */
  padding: clamp(40px, 6vh, 72px) 0 clamp(64px, 11vh, 128px); }
/* ONE column, not two max-widths. The h2 was 24ch of a vw-scaled display face and
   the lede 56ch of a fixed 17px body face, so their right edges CROSSED near 1000px
   wide: h2 wider on desktop, lede wider on a laptop. There was no stable edge at any
   width. Both now share the wrap, and the lede's cap is px so it can never overtake. */
.what .wrap { max-width: 720px; }
.what h2 { font-size: clamp(26px, 3.4vw, 40px); margin: 0 auto 18px; }
.what .lede { max-width: 620px; margin-inline: auto; }

/* ───────── the pain ───────── */
.pain h2 { font-size: clamp(26px, 3.4vw, 40px); margin-bottom: 40px; max-width: 22ch; }
.pain-grid { display: grid; grid-template-columns: 1fr; gap: 14px; }
.pain-ico { width: 40px; height: 40px; border-radius: 10px; background: var(--problem-tint);
  display: grid; place-items: center; margin-bottom: 18px; }
.pain-ico svg { stroke: var(--problem-accent); }
.pain-card h3 { font-size: 18px; margin-bottom: 8px; }
.pain-card p { font-size: 15px; line-height: 1.65; color: var(--ink-soft); }

/* ============================================================================
   WHAT IT DOES · illustrated card grid
   Ported from the main site's bento. Replaces the five-row text list that used
   to live here: same content, but each stage now has something to look at, which
   is the whole point. Every illustration below is pure CSS/HTML, no JS.
   ========================================================================== */
.feat h2 { font-size: clamp(28px, 4.3vw, 50px); line-height: 1.14; max-width: 20ch;
  margin-bottom: 12px; }
.feat .lede { margin-bottom: 44px; }
.bento { display: grid; grid-template-columns: 1fr; gap: 16px; }
.card { position: relative; border: 1px solid var(--glass-border); border-radius: 20px;
  overflow: hidden; background: transparent; box-shadow: var(--shadow-card);
  min-height: 340px; display: flex; flex-direction: column; }
/* isolation + the swirl's z-index -1 = the swirl paints ABOVE this background
   and BELOW the words, the same layer the CTA gives its own. */
/* THE ONE BOX THAT IS NOT GLASS, and the one most worth testing as glass, which
   is why it is wired to the tuner despite starting at 100%. It is the opaque half
   of every feature card and it is deliberate: the pane above it is a light 46%
   smoked plate, safe because nothing but illustration sits on it, and the body
   copy gets the solid recipe instead. At x1 this resolves to the identical solid
   colour, so nothing about the live site changes.

   THE BACKDROP-FILTER IS NOW PERMANENT, AND IT WAS NOT FREE. While the tuner
   existed this filter was scoped to [data-glass-test] so the live site never paid
   for it, because this project has MEASURED backdrop-filter as a GPU cliff rather
   than a slope (the warp: 2.7fps on a software rasteriser against 60.8 with the
   effect off, see project_background_exploration). Enzo picked 15% on 2026-08-23,
   and 15% without a blur is a see-through hole rather than glass, so the filter
   had to come out of the gate with it. That is 5 more full-time compositing layers
   on the features grid. If the landing page's Lighthouse or a real slow machine
   regresses, THIS IS THE FIRST THING TO SUSPECT, ahead of the contour field, which
   is a static one-time paint. */
.card-body { position: relative; isolation: isolate; overflow: hidden;
  padding: 24px 24px 26px; margin-top: auto;
  background: color-mix(in srgb, var(--paper-elev) clamp(0%, calc(100% * var(--glass-k)), 100%), transparent);
  -webkit-backdrop-filter: blur(calc(14px * var(--glass-blur-k))) brightness(var(--card-lift));
          backdrop-filter: blur(calc(14px * var(--glass-blur-k))) brightness(var(--card-lift)); }
.card-body h3 { font-size: 20px; margin-bottom: 8px; }
.card-body p { font-size: 15px; line-height: 1.65; color: var(--ink-soft); }
.card-tag { display: block; font-size: 11px; font-weight: 500; text-transform: uppercase;
  letter-spacing: 0.14em; color: var(--hero-accent); margin-bottom: 8px; }
:root[data-theme="dark"] .card-tag { color: var(--hero-accent-soft); }

/* shared illustration panel · the lighter smoked pane, safe because no body copy
   sits on it (body copy gets the more opaque card recipe instead) */
.panel { position: relative; flex: 1; min-height: 184px; overflow: hidden;
  background: var(--glass);
  -webkit-backdrop-filter: blur(calc(22px * var(--glass-blur-k))) saturate(1.2) brightness(var(--pane-lift));
  backdrop-filter: blur(calc(22px * var(--glass-blur-k))) saturate(1.2) brightness(var(--pane-lift)); }

/* renewals · the loop that reruns the four stages from the client file */
.card-renewals { grid-column: 1 / -1; flex-direction: row; align-items: stretch; min-height: 240px; }
/* TUNED IN THE LAB 2026-08-19. The band's text half was reading too wide: the
   longest line stopped ~93px short of the box, so the right side was dead air.
   The art half takes the space instead, and the inset carries the words with it.
   padding-left overrides the .card-body shorthand above, so it stays after it. */
.card-renewals .panel { flex: 0 0 38%; min-height: unset; }
.card-renewals .card-body { flex: 1; display: flex; flex-direction: column;
  justify-content: center; margin-top: 0;
  /* SYMMETRIC AND PROPORTIONAL, and both halves of that matter.

     Symmetric: the inset used to be padding-LEFT only, against the body's 24px
     right padding. The paragraph runs to its measure, so it ate whatever the
     inset left and sat 78px off the panel with 26px to the card edge. Pushed
     right, but with nothing to breathe into, which is the awkwardness. Equal
     padding centres the block in its half instead, which is what "scoot it over
     and shrink the box" actually wants.

     Proportional: 78px was tuned against the lab's fixed 1036px card, this
     page's WIDEST case (.wrap is 1080 less 44 of padding). The real card is
     fluid and lands at 704 on a 763px laptop, where a constant would be 11.1%
     of the card instead of 7.5%. Percentage padding on a flex item resolves
     against the flex container, so the % IS the card and the proportion holds
     at every width. The 24px floor is the card's own gutter, and the stacked
     rule below overrides both sides outright. */
  padding-left: clamp(24px, 5%, 52px);
  padding-right: clamp(24px, 5%, 52px); }
.card-renewals .card-body p { max-width: 64ch; }
/* ───────── the approval gate callout ───────── */
.gate { margin-top: 40px; border-radius: 14px;
  background: color-mix(in srgb, var(--process-tint) clamp(0%, calc(72% * var(--glass-k)), 100%), transparent);
  border: 1px solid color-mix(in srgb, var(--process-accent) 30%, transparent);
  -webkit-backdrop-filter: blur(calc(14px * var(--glass-blur-k))) brightness(var(--card-lift));
  backdrop-filter: blur(calc(14px * var(--glass-blur-k))) brightness(var(--card-lift));
  padding: 24px 26px; display: flex; gap: 16px; align-items: flex-start; }
.gate svg { flex: none; stroke: var(--process-accent); margin-top: 2px; }
.gate p { font-size: 16.5px; line-height: 1.6; color: var(--ink); max-width: 52ch; }

/* ───────── demo CTA band ───────── */
.demo-cta { text-align: center; }
.demo-cta h2 { font-size: clamp(28px, 4.2vw, 50px); max-width: 18ch; margin: 0 auto 18px; }
.demo-cta .lede { margin: 0 auto 34px; text-align: center; }
.demo-cta .hero-note { display: block; margin-top: 18px; }

/* ───────── impact ───────── */
.impact-grid { display: grid; grid-template-columns: 1fr; gap: 36px; text-align: center; }
.impact-num { font-family: var(--display); font-weight: 500; font-size: clamp(48px, 5.4vw, 72px);
  line-height: 1; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
/* .unit clamps to ~18px on narrow screens, under the large-text threshold, so it
   needs the light-mode nudge even though it passes at desktop size */
.impact-num .unit { font-size: 0.38em; margin-left: 4px; letter-spacing: -0.01em;
  color: color-mix(in srgb, var(--process-accent) 80%, black); }
:root[data-theme="dark"] .impact-num .unit { color: var(--process-accent); }
.impact-cap { margin-top: 12px; font-size: 13.5px; line-height: 1.55; color: var(--ink-soft);
  max-width: 24ch; margin-inline: auto; }

/* ───────── proof ───────── */
.proof { padding: clamp(40px, 7vh, 80px) 0; }
.proof-band { display: flex; align-items: center; gap: 22px; max-width: 720px; margin: 0 auto;
  border-radius: 16px; padding: 24px 28px; }
.proof-logo { width: 64px; height: 64px; flex: none; border-radius: 14px; overflow: hidden; }
.proof-logo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.proof-copy .eyebrow { margin-bottom: 8px; }
.proof-copy p { font-size: 15.5px; line-height: 1.5; color: var(--ink-soft); max-width: 48ch; }
.proof-copy .nm { color: var(--ink); }

/* ───────── founders (about page) ───────── */
.team-grid { display: grid; grid-template-columns: 1fr; gap: 14px; }
.team-card { display: flex; flex-direction: column;
  transition: transform var(--dur-fast) var(--ease-out-strong), border-color var(--dur-fast) ease; }
@media (hover: hover) {
  .team-card:hover { transform: translateY(-2px);
    border-color: color-mix(in srgb, var(--hero-accent) 40%, var(--card-glass-line)); }
}
.team-card:focus-within { border-color: color-mix(in srgb, var(--hero-accent) 40%, var(--card-glass-line)); }
.team-card h3 { font-size: 18px; margin-bottom: 5px; }
.team-role { font-size: 11px; font-weight: 500; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--secondary); margin-bottom: 14px; }
.team-card p { font-size: 15px; line-height: 1.65; color: var(--ink-soft); }
/* auto top margin so both links share a baseline when the bios differ in length */
.team-link { display: inline-flex; align-items: center; gap: 7px; margin-top: auto;
  padding-top: 16px; align-self: flex-start;
  font-size: 14px; font-weight: 500; color: var(--hero-accent); }
/* dark --hero-accent #88A4C0 is 4.09:1 on the elevated glass card and fails AA;
   the soft accent clears it. Same fix the port applies to .card-cta / .card-tag. */
:root[data-theme="dark"] .team-link { color: var(--hero-accent-soft); }
.team-link .arrow { transition: transform var(--dur-fast) ease; }
.team-link:hover .arrow { transform: translateX(3px); }

/* ───────── prose (about, legal) ───────── */
.prose { max-width: 68ch; }
.prose h2 { font-size: clamp(24px, 3vw, 34px); margin-bottom: 14px; }
.prose h3 { font-size: clamp(17px, 2vw, 20px); margin: 32px 0 10px; }
.prose p { font-size: 16.5px; line-height: 1.7; color: var(--ink-soft); margin-bottom: 16px; }
.prose ul { margin: 0 0 18px 20px; }
.prose li { font-size: 16.5px; line-height: 1.7; color: var(--ink-soft); margin-bottom: 8px; }
.prose a { color: var(--hero-accent); font-weight: 500; text-decoration: underline;
  text-underline-offset: 3px; }
:root[data-theme="dark"] .prose a { color: var(--hero-accent-soft); }
.prose strong { font-weight: 500; color: var(--ink); }
.doc-stamp { font-size: 13px; color: var(--secondary); margin-bottom: 26px; }
.doc-rule { border: 0; border-top: 1px solid var(--hairline); margin: clamp(40px, 6vh, 72px) 0; }

/* ───────── FAQ ───────── */
.faq-list { border-top: 1px solid var(--hairline); max-width: 68ch; }
.faq-item { border-bottom: 1px solid var(--hairline); }
.faq-q { width: 100%; display: flex; align-items: flex-start; justify-content: space-between;
  gap: 20px; padding: 20px 0; background: none; border: 0; cursor: pointer;
  font: 500 17px var(--display); color: var(--ink); text-align: left; line-height: 1.35; }
.faq-q .chev { flex: none; margin-top: 3px; color: var(--secondary);
  transition: transform var(--dur-base) var(--ease-out-strong); }
.faq-q[aria-expanded="true"] .chev { transform: rotate(180deg); }
.faq-a { overflow: hidden; max-height: 0; transition: max-height var(--dur-base) var(--ease-soft); }
.faq-a p { font-size: 15.5px; line-height: 1.7; color: var(--ink-soft);
  padding: 0 0 22px; max-width: 60ch; }

/* ───────── waitlist ───────── */
/* Steel, not green: green means "your go" in this system and a closed list is not
   a go. Circle, not the wordmark's rounded square, so the mark keeps that shape. */
.join-status { display: inline-flex; align-items: center; gap: 9px; font-size: 11px;
  font-weight: 500; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--hero-accent); margin-bottom: 20px; }
.join-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: currentColor; animation: dotpulse 2.6s ease-in-out infinite; }
:root[data-theme="dark"] .join-status { color: var(--hero-accent-soft); }
.join-card { max-width: 560px; border-radius: 16px; padding: 30px 28px; }
.join-card h2 { font-size: 24px; margin-bottom: 10px; }
.join-card > p { font-size: 15.5px; line-height: 1.65; color: var(--ink-soft); }
.join-hint { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--hairline);
  font-size: 13.5px; line-height: 1.6; color: var(--secondary); }

/* ============================================================================
   THE SIM (demo page)
   The frame is expanded, not the iframe: same FLIP the port runs on .sim-frame,
   but here the iframe just rides along inside it. Cross-document isolation means
   the sim's own wheel events never reach Lenis, so no data-lenis-prevent needed.
   ========================================================================== */
.wrap-wide { max-width: 1320px; margin: 0 auto; padding: 0 22px; }
.sim-frame { position: relative; width: min(1320px, 100%);
  height: clamp(560px, 72vh, 760px); margin: 0 auto;
  border: 1px solid var(--glass-border); border-radius: 16px; overflow: hidden;
  box-shadow: var(--shadow-card-lg); background: var(--paper-deep); }
.sim-frame iframe { width: 100%; height: 100%; border: 0; display: block; }
.sim-frame.sim-fs-anim {
  transition: top var(--dur-panel) var(--ease-out-strong), left var(--dur-panel) var(--ease-out-strong),
              width var(--dur-panel) var(--ease-out-strong), height var(--dur-panel) var(--ease-out-strong),
              border-radius var(--dur-panel) var(--ease-out-strong); }
/* native scroll chaining would still reach the page behind the open sim */
html.sim-fs-open { overflow: hidden; }
/* right: 92px clears the sim's own titlebar controls, which sit under this chip
   in the iframe: 16px titlebar padding + 26px avatar + 12px gap + 28px theme
   button + 10px breathing room. At right:14px the chip covered the sun/moon. */
.sim-chip { position: absolute; top: 12px; right: 92px; z-index: 90;
  display: inline-flex; align-items: center; gap: 7px; padding: 8px 14px; border-radius: 999px;
  border: 1px solid var(--glass-border);
  background: color-mix(in srgb, var(--paper-elev) clamp(0%, calc(55% * var(--glass-k)), 100%), transparent);
  -webkit-backdrop-filter: blur(calc(12px * var(--glass-blur-k))) brightness(var(--card-lift));
  backdrop-filter: blur(calc(12px * var(--glass-blur-k))) brightness(var(--card-lift));
  color: var(--ink); font-family: var(--sans); font-size: 12px; font-weight: 500;
  cursor: pointer; transition: transform var(--dur-press) var(--ease-out-strong),
  background-color var(--dur-fast) ease; }
.sim-chip svg { width: 13px; height: 13px; }
/* WCAG 2.4.1 bypass blocks: the sim holds 30+ tab stops. Without this a keyboard
   user has to traverse the entire demo to reach anything after it. */
.skip-demo { position: absolute; left: -9999px; }
.skip-demo:focus { position: static; display: inline-block; margin-bottom: 14px;
  padding: 10px 16px; border-radius: 8px; background: var(--paper-elev); color: var(--ink);
  border: 1px solid var(--hairline); font-size: 14px; font-weight: 500; }
#after-demo:focus { outline: none; }
@media (hover: hover) {
  .sim-chip:hover { background: color-mix(in srgb, var(--paper-elev) 75%, transparent); }
}
.sim-chip:active { transform: scale(var(--press-scale)); }

/* ───────── responsive ─────────
   THREE breakpoints, not one, because the three grids fail at different widths.
   A single 760 switch put Enzo's 763px laptop 3px inside the desktop layout and
   every grid inherited a width it was never sized for. Each number below is
   derived from the content, not picked. */
@media (min-width: 760px) {
  .impact-grid { grid-template-columns: repeat(3, 1fr); }
  .impact-grid > div { border-left: 1px solid var(--hairline); }
  .impact-grid > div:first-child { border-left: none; }
  .team-grid { grid-template-columns: repeat(2, 1fr); }
  /* the pain cards take TWO columns through the middle band. Three of them here
     gives a 23ch measure, a third of the readable minimum, and the blurbs ran six
     ragged lines. Two lands at ~39ch, the same measure they get at full desktop. */
  .pain-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 950px) {
  .pain-grid { grid-template-columns: repeat(3, 1fr); }
}
/* 1084 IS A MEASURED VALUE, DO NOT ROUND IT. At 1084 the 1080 wrap less its 44px
   padding and the 16px gap leaves a 510px card, which is the first width where the
   plate still holds its 508px design size. Below it a two-column plate shrinks, and
   because every scene is positioned in tuned pixel offsets against 508 (see the
   .scb-l-* block, note the negative lefts), a shrinking plate does not scale the
   art, it slides real words under the overflow clip. It was cutting "Premier Metal
   Fabrication LL" and "General liability section" on a 763px screen. */
@media (min-width: 1084px) {
  .bento { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 1083px) {
  /* the renewals band goes back to a stacked card below the grid breakpoint */
  .card-renewals { flex-direction: column; min-height: 340px; }
  .card-renewals .panel { flex: 1; min-height: 184px; }
  /* Single column makes the card wider than the art, so hold the plate AT its
     design width and centre it. Letting it stretch is the same bug in reverse:
     the tuned overhang stops reaching and the scene drifts off its framing. */
  .bento .panel { width: 100%; max-width: 508px; margin-inline: auto; }
  /* the 78px inset is a HORIZONTAL BAND value: it buys the art half room and
     carries the words right. Stacked, the art is above and there is nothing to
     clear, so it would just read as a lopsided 78/24 gutter. */
  .card-renewals .card-body { padding-left: 24px; padding-right: 24px; margin-top: auto; }
}

/* Reduced motion, restated last so it wins over every section rule above. */
@media (prefers-reduced-motion: reduce) {
  .doc, .mesh .blob, .hero-cue, .hero-cue::after, .wordmark::before,
  .join-status::before { animation: none !important; }
  /* The swirl still appears on hover, it just stops turning and stops fading:
     the signal survives, the motion does not. */
  .btn-glass::before, .btn-glass::after,
  .card-swirl::before, .card-swirl::after {
    animation: none !important; transition: none !important; scale: 1; }
  .co, .hero h1 .ln span { animation: none; opacity: 1; transform: none; }
  .reveal { transition: none; opacity: 1; transform: none; }
  .sim-frame.sim-fs-anim { transition: none; }
  html { scroll-behavior: auto; }
}

/* Set by verso.js on a machine measured to be unable to afford the motion. It
   is NOT a second reduced-motion: the reveals, the scroll and the entrance all
   still run, because measurement cleared them. Only the swirl's rotation and its
   blur transition are pulled, which is where the frames actually went (10-15fps
   spinning, 32-39 static, 60 with the transition off too, same hover, same
   machine). The swirl still appears and still carries its colour.
   The hero warp is handled in JS on the same signal: it never fires there at all.
   Numbers: tools/perf-probe.mjs, 2026-08-20. */
:root.perf-lite .btn-glass::before, :root.perf-lite .btn-glass::after,
:root.perf-lite .card-swirl::before, :root.perf-lite .card-swirl::after {
  animation: none !important; transition: none !important; scale: 1;
}

/* The sim's fullscreen FLIP is a 420ms transition on a large element containing
   an iframe full of glass, which is the single most expensive thing a software
   compositor can be asked to do. Enzo: "the initial sim opening especially feels
   very slow and laggy", on a machine where the rest of the demo then runs fine.
   So it snaps instead of animating, which is again exactly what the
   reduced-motion block above already does with this same selector.
   NOT the warp, which was the obvious suspect: demo.html has no .hero, so the
   warp is never armed on that page at all. */
:root.perf-lite .sim-frame.sim-fs-anim { transition: none; }

/* ============================================================
   NARROW PLATES ZOOM, THEY DO NOT CROP.

   THIS BLOCK MUST STAY ABOVE THE SIM ILLUSTRATION KIT BANNER. sync.py replaces
   everything from that banner to the end of the file, so anything written below
   it is deleted on the next run.

   Every scene in the kit is drawn in tuned pixel offsets against a 508px plate.
   Above 1084 the grid guarantees that width; from 1084 down the plate is pinned
   to 508 and centred. Below roughly 570 the CARD is narrower than 508, the pin
   runs out of room, and the crop starts taking real words: at 390 wide it was
   showing "Premier Metal Fabrication LL", "GL + Proper", "operty section". Same
   failure the 1084 breakpoint fixed at the desktop end, arriving from the other
   side.

   --fit is the plate's share of its 508px design width, measured by verso.js and
   clamped to 1. Scaling the whole scene is what keeps the framing: every size,
   offset and deliberate bleed in the kit is px * --k, so a uniform scale is the
   desktop composition at a smaller zoom rather than a re-tune.

   THE HEIGHT AND THE ORIGIN ARE THE WHOLE TRICK, do not simplify them away. The
   scene box is the crop window, and its edges are what the offsets are measured
   from: .scb-body hangs off the TOP, .scb-foot off the BOTTOM. Scaling the box
   about its centre pulls both edges inward, which put 21px of dead glass above
   the intake module and unhooked closing's foot from the plate floor. So the box
   is first grown to plate-height / fit, then scaled from its top edge: after the
   transform it covers the plate exactly, every edge lands where it lands on
   desktop, and only the zoom changed. Growing the box needs bottom:auto, since
   the kit sets inset:0 and a box with both edges pinned ignores height.

   The selector carries a fourth class ONLY for specificity. The kit's own
   .panel:not(.panel-wide) .scb is also three classes and sits LATER in the
   file, so a three-class rule here loses the tie and the scene keeps the
   kit's unscaled transform.

   AND THE MEDIA QUERY IS LOAD-BEARING, it is not just "phones only". --fit is a
   share of 508, which is the plate's design width ONLY below 1084. Above it the
   renewals band's plate is 393 wide by design (flex 0 0 38%), so verso.js writes
   it a --fit of 0.77 that means nothing. Harmless while this rule is scoped, and
   a 23% shrink of that band the moment someone unscopes it.
   ============================================================ */
@media (max-width: 1083px) {
  .bento .card .panel .scb {
    bottom: auto;
    height: calc(100% / var(--fit, 1));
    transform-origin: top center;
    transform: translateX(-50%) scale(var(--fit, 1));
  }
}


/* ============================================================
   SIM ILLUSTRATION KIT
   Generated by _design-exploration/pillar-shots/sync.py.
   Tune framings in that preview and re-run it.
   Landing framings only. No hero bands (this site has no pillar pages)
   and no Next card framings (its plate is 508x209, not 271x184).
   ============================================================ */
/* ============================================================
   VARIANT B, the window. Sits beside variant A (sim-cards.css), does not
   replace it.

   A was a composition: elements picked, tidied and centred on the plate. It
   reads as marketing because a real screen is never that neat. B is a WINDOW:
   the plate is treated as a viewport onto the verso panel, elements sit at
   their real size in their real order, and the card edge crops whatever runs
   past it.

   WHY THIS CAN BE A LITERAL TRANSCRIPTION

   The sim is built on Vista, the same palette as the site, and the surfaces
   match token for token in BOTH themes:

     sim --card       == site --card-glass
     sim --card-hi    == site --card-glass-hi
     sim --card-line  == site --card-glass-line
     sim --glass      == site --glass          (the panel IS the card plate)
     sim --steel      == site --hero-accent
     sim --green      == site --process-accent
     sim --secondary  == site --secondary

   So "draw the sim's panel" and "draw the site's plate" are the same
   instruction, and the glass survives by construction rather than by
   compromise. Every measurement below is the sim's own, in sim pixels,
   multiplied by --k. Changing --k changes the zoom and nothing else.

   NO SCRIM. The sim dims everything behind an open document with
   rgba(18,26,38,0.55). That wash is what made the raw Assembly and Closing
   crops read muddy. Here the layer behind a document stays at full brightness
   and depth comes from the document's own shadow, which is the same cue the
   sim uses once the wash is discounted.
   ============================================================ */

.scb {
  --k: 0.8;                      /* Enzo's pick: matches how his crops displayed */
  position: absolute; inset: 0;
  font-family: inherit; line-height: 1.4;
}

/* A card scene is a 400px slice of the sim's panel column. The Next bento
   plate is 271px so the slice all but fills it, but the landing page's plate
   is 510px wide at the same 184px height, and left-anchoring there dumped
   every pixel of slack on one side. Centring the column fixes that without
   touching a single tuned offset, because the offsets are relative to .scb.
   The plate keeps the clipping (.panel is overflow:hidden), so anything that
   bleeds still bleeds, now symmetrically.
   Bands are exempt: their panel hangs off the RIGHT edge by design. */
.panel:not(.panel-wide) .scb {
  left: 50%; right: auto; width: calc(400px * var(--k)); transform: translateX(-50%);
}

/* Every inline SVG gets a size by default. Without this an icon with no rule
   of its own expands to fill its flex parent, which is exactly what happened
   to the cue arrow: 13px in the sim, 210px here, and it shoved the entire
   panel foot off the top of the card. */
.scb svg { width: calc(15px * var(--k)); height: calc(15px * var(--k)); flex: none; display: block; }
.scb-cue svg { width: calc(13px * var(--k)); height: calc(13px * var(--k)); color: var(--hero-accent); }

/* the panel body: the sim's 400px pane, its 20px padding, anchored top-left.
   Content column lands at 358 * k and runs off the right edge, which is the
   whole point. */
.scb-body {
  position: absolute; top: 0; left: calc(-8px * var(--k));
  width: calc(400px * var(--k));
  padding: calc(20px * var(--k));
}

/* ---------- module ---------- */
.scb-mod {
  background: var(--card-glass); border: 1px solid var(--card-glass-line);
  border-radius: calc(12px * var(--k)); padding: calc(16px * var(--k));
  margin-bottom: calc(10px * var(--k));
}
.scb-modh {
  display: flex; align-items: center; gap: calc(10px * var(--k));
  margin-bottom: calc(14px * var(--k));
}
.scb-modh .lbl {
  font-size: calc(12px * var(--k)); font-weight: 500; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--secondary); white-space: nowrap;
}
.scb-ico {
  width: calc(28px * var(--k)); height: calc(28px * var(--k));
  border-radius: calc(9px * var(--k)); display: grid; place-items: center; flex: none;
  background: var(--hero-tint); color: var(--hero-accent);
}
.scb-ico.green { background: var(--process-tint); color: var(--process-accent); }
.scb-ico svg { width: calc(15px * var(--k)); height: calc(15px * var(--k)); display: block; }

/* rows carry NO separator lines in the sim. Variant A drew them, and that one
   detail is a large part of why A reads as a table someone made rather than a
   panel someone shipped. */
.scb-row { display: flex; align-items: baseline; justify-content: space-between; gap: calc(14px * var(--k)); }
.scb-row + .scb-row { margin-top: calc(9px * var(--k)); }
.scb-row .k { font-size: calc(12px * var(--k)); color: var(--secondary); flex: none; }
.scb-row .v { font-size: calc(14px * var(--k)); font-weight: 500; color: var(--ink); text-align: right; white-space: nowrap; }

/* ---------- stat trio ---------- */
.scb-stats { display: flex; gap: calc(9px * var(--k)); margin-bottom: calc(10px * var(--k)); }
.scb-stat {
  flex: 1; background: var(--card-glass); border: 1px solid var(--card-glass-line);
  border-radius: calc(12px * var(--k)); padding: calc(10px * var(--k)) calc(12px * var(--k));
}
.scb-stat .v { font-size: calc(20px * var(--k)); font-weight: 500; letter-spacing: -0.01em; color: var(--ink); display: block; }
.scb-stat .k { font-size: calc(12px * var(--k)); color: var(--secondary); margin-top: calc(2px * var(--k)); display: block; }

/* ---------- document button ---------- */
.scb-doc {
  display: flex; align-items: center; gap: calc(11px * var(--k));
  padding: calc(11px * var(--k)) calc(12px * var(--k));
  border: 1px solid var(--card-glass-line); border-radius: calc(8px * var(--k));
  background: var(--card-glass-hi); margin-bottom: calc(10px * var(--k));
}
.scb-doc .tt { display: block; font-size: calc(14px * var(--k)); font-weight: 500; color: var(--ink); white-space: nowrap; }
.scb-doc .ss { display: block; font-size: calc(12px * var(--k)); color: var(--secondary); white-space: nowrap; }
.scb-doc .go { margin-left: auto; color: var(--secondary); flex: none; }
.scb-doc .go svg { width: calc(14px * var(--k)); height: calc(14px * var(--k)); display: block; }

/* ---------- callout ---------- */
.scb-callout {
  display: flex; gap: calc(9px * var(--k));
  padding: calc(12px * var(--k)) calc(13px * var(--k));
  border-radius: calc(12px * var(--k)); font-size: calc(14px * var(--k));
  line-height: 1.45; margin-bottom: calc(10px * var(--k));
}
.scb-callout svg { width: calc(15px * var(--k)); height: calc(15px * var(--k)); flex: none; margin-top: calc(2px * var(--k)); }
.scb-callout.steel { background: color-mix(in srgb, var(--hero-tint) 72%, transparent); color: var(--hero-accent); }
.scb-callout.green { background: color-mix(in srgb, var(--process-tint) 72%, transparent); color: var(--process-accent); }

/* ---------- market picker ---------- */
.scb-pick {
  display: flex; align-items: center; gap: calc(11px * var(--k));
  padding: calc(11px * var(--k)) calc(12px * var(--k));
  min-height: calc(40px * var(--k));
  border: 1px solid var(--card-glass-line); border-radius: calc(8px * var(--k));
}
.scb-pick + .scb-pick { margin-top: calc(8px * var(--k)); }
.scb-pick .rd {
  width: calc(16px * var(--k)); height: calc(16px * var(--k)); border-radius: 50%;
  border: calc(1.5px * var(--k)) solid var(--rule); flex: none; background: var(--paper-elev);
}
.scb-pick .nm { font-size: calc(14px * var(--k)); font-weight: 500; color: var(--ink); white-space: nowrap; }
.scb-pick .nm span { display: block; font-size: calc(12px * var(--k)); font-weight: 400; color: var(--secondary); }
.scb-pick .mt { margin-left: auto; font-size: calc(12px * var(--k)); color: var(--hero-accent); font-weight: 500; flex: none; }
.scb-pick.off { border-style: dashed; }
.scb-pick.off .nm, .scb-pick.off .nm span { color: var(--secondary); }

/* ---------- panel foot: cue, actions, the approval line ---------- */
.scb-foot {
  position: absolute; left: 0; bottom: 0; width: calc(400px * var(--k));
  padding: calc(14px * var(--k)) calc(18px * var(--k));
  border-top: 1px solid var(--card-glass-line);
}
.scb-cue {
  font-size: calc(12px * var(--k)); font-weight: 500; color: var(--secondary);
  margin-bottom: calc(9px * var(--k)); display: flex; align-items: center; gap: calc(7px * var(--k));
}
.scb-acts { display: flex; gap: calc(8px * var(--k)); }
.scb-btn {
  flex: 1; display: flex; align-items: center; justify-content: center; gap: calc(8px * var(--k));
  font-size: calc(16px * var(--k)); font-weight: 500;
  padding: calc(12px * var(--k)) calc(16px * var(--k));
  border-radius: calc(8px * var(--k)); border: 1px solid transparent;
  background: var(--process-accent); color: var(--on-accent); white-space: nowrap;
}
.scb-btn svg { width: calc(16px * var(--k)); height: calc(16px * var(--k)); flex: none; }
.scb-btn.ghost {
  flex: none; background: color-mix(in srgb, var(--paper-elev) 30%, transparent);
  color: var(--ink-soft); border-color: var(--glass-border);
}
.scb-hitl {
  font-size: calc(12px * var(--k)); font-weight: 500; color: var(--hero-accent);
  text-align: center; margin-top: calc(10px * var(--k));
}

/* ---------- paper: an open document, floating over the panel ----------
   The sim's .docwin is 720px wide. At any usable zoom most of it is off the
   card, and that is correct: you are seeing a document open over a panel, not
   a picture of a document. */
.scb-paper {
  position: absolute; background: #FFFFFF; color: #111;
  border: 1px solid rgba(16,22,30,0.10); border-radius: calc(10px * var(--k));
  font-family: Arial, Helvetica, sans-serif;
  box-shadow: 0 calc(24px * var(--k)) calc(60px * var(--k)) calc(-16px * var(--k)) rgba(16,22,30,0.42),
              0 calc(4px * var(--k)) calc(12px * var(--k)) calc(-4px * var(--k)) rgba(16,22,30,0.18);
  overflow: hidden;
}
:root[data-theme="dark"] .scb-paper {
  box-shadow: 0 calc(26px * var(--k)) calc(64px * var(--k)) calc(-16px * var(--k)) rgba(0,0,0,0.75),
              0 calc(4px * var(--k)) calc(14px * var(--k)) calc(-4px * var(--k)) rgba(0,0,0,0.55);
}
.scb-paper .dh {
  padding: calc(14px * var(--k)) calc(18px * var(--k));
  font-size: calc(14px * var(--k)); font-weight: 500; color: #1B1E1F;
  border-bottom: 1px solid #E2E6EA; white-space: nowrap;
}
.scb-sheet { padding: calc(16px * var(--k)) calc(18px * var(--k)); }
.scb-fhead { display: flex; align-items: center; gap: calc(14px * var(--k)); margin-bottom: calc(10px * var(--k)); }
.scb-fbadge {
  border: 1px solid #111; border-radius: 999px; flex: none;
  padding: calc(3px * var(--k)) calc(12px * var(--k));
  font-size: calc(12px * var(--k)); font-weight: 700; letter-spacing: 0.02em;
}
.scb-ftitle { font-size: calc(12.5px * var(--k)); font-weight: 700; letter-spacing: 0.02em; white-space: nowrap; }
.scb-ftable { border: 1px solid #9AA2AA; font-size: calc(8.9px * var(--k)); line-height: 1.3; }
.scb-ftable .r { display: flex; border-top: 1px solid #9AA2AA; }
.scb-ftable .r:first-child { border-top: 0; }
.scb-ftable .c { padding: calc(3px * var(--k)) calc(5px * var(--k)); border-left: 1px solid #9AA2AA; white-space: nowrap; }
.scb-ftable .c:first-child { border-left: 0; }
.scb-ftable .lbl { display: block; font-size: calc(6.6px * var(--k)); letter-spacing: 0.03em; color: #333; }
.scb-legend {
  border: 1px solid #B33A2B; color: #A72E1B; font-weight: 700;
  padding: calc(5px * var(--k)) calc(7px * var(--k));
  font-size: calc(7.6px * var(--k)); line-height: 1.3;
  margin-bottom: calc(10px * var(--k)); white-space: nowrap;
}
.scb-sign { font-family: "Segoe Script", "Brush Script MT", cursive;
  font-size: calc(15px * var(--k)); color: #1B2A44;
  border-bottom: 1px solid #111; display: inline-block;
  padding: 0 calc(40px * var(--k)) calc(2px * var(--k)) calc(2px * var(--k)); }
.scb-signcap { display: block; font-size: calc(8px * var(--k)); color: #444; margin-top: calc(3px * var(--k)); }

/* ---------- mail: list column and reading pane ---------- */
/* The sim floats the mail list and the reading pane as separate rounded
   islands on the canvas, with a gap between them. Filling the plate edge to
   edge instead made the pane's white run straight into the card body's white
   and the plate boundary disappeared entirely. The gap IS the sim. */
.scb-list {
  position: absolute; top: calc(10px * var(--k)); bottom: calc(10px * var(--k));
  left: calc(10px * var(--k)); width: calc(250px * var(--k));
  background: var(--paper-elev); border: 1px solid var(--card-glass-line);
  border-radius: calc(10px * var(--k)); overflow: hidden;
}
.scb-mrow { display: flex; gap: calc(11px * var(--k)); padding: calc(11px * var(--k)) calc(13px * var(--k)); align-items: flex-start; }
.scb-mrow.on { background: color-mix(in srgb, var(--hero-tint) 55%, transparent); }
.scb-av {
  width: calc(30px * var(--k)); height: calc(30px * var(--k)); border-radius: 50%; flex: none;
  display: grid; place-items: center; background: var(--hero-accent); color: #fff;
  font-size: calc(11px * var(--k)); font-weight: 500;
}
.scb-mrow .mb { min-width: 0; }
.scb-mrow .mf { display: block; font-size: calc(13px * var(--k)); font-weight: 500; color: var(--ink); white-space: nowrap; }
.scb-mrow .mp { display: block; font-size: calc(12px * var(--k)); color: var(--secondary); white-space: nowrap; }
.scb-read {
  position: absolute; top: calc(10px * var(--k)); bottom: calc(10px * var(--k));
  left: calc(270px * var(--k)); right: calc(-300px * var(--k));
  background: var(--paper-elev); border: 1px solid var(--card-glass-line);
  border-radius: calc(10px * var(--k)); overflow: hidden;
  padding: calc(18px * var(--k)) calc(20px * var(--k));
}
.scb-subj { font-size: calc(20px * var(--k)); font-weight: 500; color: var(--ink); line-height: 1.25; margin-bottom: calc(12px * var(--k)); }
.scb-from { display: flex; gap: calc(11px * var(--k)); align-items: center; margin-bottom: calc(14px * var(--k)); }
.scb-from .nm { font-size: calc(13px * var(--k)); font-weight: 500; color: var(--ink); display: block; white-space: nowrap; }
.scb-from .or { font-size: calc(12px * var(--k)); color: var(--secondary); display: block; white-space: nowrap; }
.scb-para { font-size: calc(13px * var(--k)); color: var(--ink-soft); line-height: 1.6; }
.scb-para b { color: var(--ink); font-weight: 500; }

/* ============================================================
   FRAMING, one per pillar, copied off Enzo's crops.
   Each names the edges it runs off. Nothing bleeds on all four, and every
   scene keeps one element whole as the thing that carries the message.
   ============================================================ */



/* ============================================================
   LANDING PAGE, its own framing.

   Its plate is 508 x 209 against the Next bento's 271 x 184, near enough
   double the width and half again the height, so one --k cannot serve both:
   at the Next values the art sat in the middle of all that room showing the
   same small crop. These run around --k 1.0, roughly 45% larger, which is
   what the extra room is for.

   The scene classes are DISTINCT (.scb-l-*) rather than a modifier on the
   Next ones, so the two sites tune independently and the tuner keeps them as
   separate entries.

   Starting points. Enzo tunes these in the lab and they get pasted back.
   ============================================================ */

/* --- Enzo's tuned values, 2026-08-18. Re-tune in cards.html, Save file. --- */

.scb-l-intake { --k: 1.1; }
.scb-l-intake .scb-body { top: calc(-16px * var(--k)); left: calc(-1px * var(--k)); }

/* he pulled the doc rows left and brought the form back in from 265, so the
   two planes overlap more than the Next card's do and the form shows more of
   its table */
.scb-l-assembly { --k: 0.99; }
.scb-l-assembly .scb-body { top: calc(-10px * var(--k)); left: calc(-52px * var(--k)); }
.scb-l-assembly .scb-paper { top: calc(58px * var(--k)); left: calc(164px * var(--k)); width: calc(430px * var(--k)); }

/* routing is the tightest scene in the kit: its module is the tallest, so at
   209px the plate shows the header and two markets whole with the third's top
   edge below. Past ~1.1 the zoom starts cutting into "Keystone Specialty"
   rather than revealing more. */
.scb-l-routing { --k: 1.08; }
.scb-l-routing .scb-body { top: calc(-18px * var(--k)); left: calc(3px * var(--k)); }

.scb-l-closing { --k: 1.16; }
/* -30 IS ENZO'S, RESTORED 2026-08-19 after he compared the site against the lab.
   It was held at -12 for two rounds on the "words cannot be cut" rule, because
   -30 puts "Nothing sends until you approve." 15.3 sim px into an 18px line.
   Measured again with him watching, that leaves ~1% of the line, which reads as
   HIDDEN rather than sliced, and it is the composition he wants: the foot sits
   18 sim px lower, which drops that line out of frame AND clears the 3.8px
   overlap the button row had with the certificate at -12. Both of his tells,
   one number. Do not "fix" this to -12 again. If the 1% sliver ever shows on a
   different display, go DOWN to -33 (fully out), never up. */
.scb-l-closing .scb-foot { bottom: calc(-30px * var(--k)); left: calc(-16px * var(--k)); }
.scb-l-closing .scb-paper { top: calc(-51px * var(--k)); left: calc(156px * var(--k)); width: calc(400px * var(--k)); }

/* renewals keeps its own plate on this site too, 352 x 238, close enough to
   the Next one that it was left where it sat */
.scb-l-renewals { --k: 0.8; }
.scb-l-renewals .scb-back { position: absolute; top: calc(0.8em); left: 0; width: 12em; }
.scb-l-renewals .scb-front { margin: 0 0 1.4em 5.5em; }
.scb-l-renewals .scb-list { top: calc(10px * var(--k)); bottom: calc(10px * var(--k)); left: calc(-35px * var(--k)); }
.scb-l-renewals .scb-read { top: calc(10px * var(--k)); bottom: calc(10px * var(--k)); left: calc(220px * var(--k)); right: calc(-250px * var(--k)); }


/* ==========================================================================
   OPENER · the mark draws itself in, then becomes the nav wordmark
   --------------------------------------------------------------------------
   Gated on ?intro=1 for now. Nothing here runs on a plain page load.

   TWO ACTS. The hold (2100ms): the three pieces of the mark draw in, the
   stick runs the nav's launch, the lockup scoots left and uncovers the word.
   The landing (1150ms): the white dissolves to paper, the nav items pop in,
   the lockup flies to the nav slot shrinking to the nav's own ratio and
   BECOMES the nav wordmark, and the hero assembles under it top to bottom.
   Then the warp.

   THE LIFT IS RETIRED, 2026-09-01. Until then the splash was a panel that
   slid up off the page on a curve fitted frame by frame to Website Intro.mov
   (cubic-bezier(0.8, 0, 0.22, 1), a strong ease-in-out, 1300ms), with the
   hero rising 96px under it. Enzo replaced it with the match cut above: the
   intro logo IS the nav logo, so the exit has a purpose (orientation) instead
   of being a curtain, and the apparent-motion problem between a centre mark
   dissolving and a nav mark appearing is gone by construction. The panel now
   only dissolves, so --ease-reference and --intro-rise went with it.

   THE ASSEMBLY IS THE SIM'S OWN RECIPE, borrowed verbatim from sim.html's
   island-in (0.94, over to 1.015 at 70%, settle at 1, 340ms on
   --ease-out-strong). Enzo pointed at the sim's opening as the reference. The
   1.5% is a keyframe overshoot on unclipped boxes, shipped in the sim since
   08-17; the h1 is scaled as a whole so its line clips scale with it and no
   letter tops get cut, which is what the 08-29 curve overshoot did.

   THE CONTOUR FIELD DOES NOT ANIMATE, measured as well as decided. Scaling
   it 1.12 to 1 under the landing dropped 14 frames on Enzo's laptop (477ms
   stalled inside the 1150ms, worst 117ms): 62 long SVG paths at full
   viewport re-rasterise every frame. With the field still, 1 frame. It sits
   under the white and the dissolve is its entrance.
   ========================================================================== */
:root {
  /* THE HOLD MUST OUTLAST THE WHOLE LOCKUP SEQUENCE, because the lift starts the
     moment the hold is up and does not wait for anything:
       scoot-at + max(scoot-dur, word cascade, word-track)  <=  intro-hold
     Currently 1280 + max(520, 5 letters at 70 plus 260 = 540, 620) = 1900 of a
     2100 hold, so the word settles and is held still for 200ms before the panel
     moves. The binding term is still --word-track, by 80ms now rather than 320.
     Shorten the hold below 1900 and the splash lifts mid-settle.
     Every timing that feeds this lives in the lockup block further down. */
  --intro-hold:  2100ms;  /* everything still, before the landing */
  --intro-lift:  1150ms;  /* the landing: dissolve, flight, assembly */
  --intro-ground: #FFFFFF;

  /* THE LANDING, Enzo's pick 2026-09-01 (zoom v1 in the A/B bar, judged on
     his machine against the lift). Every time is from the moment the hold
     ends, and every one is on the intro() knob in verso.js. */
  --zoom-dissolve:   400ms;  /* the white to paper */
  --zoom-nav-at:     100ms;  /* the nav's centre pill pops in, right side a step later */
  --zoom-flight-at:  150ms;  /* the lockup leaves for the nav slot... */
  --zoom-flight:     900ms;  /* ...and takes this long. Must end before the swap at --intro-lift. */
  --zoom-hero-at:    500ms;  /* the hero starts assembling: after the lockup has cleared its path */
  --zoom-step:        70ms;  /* the beat between one island and the next */
  --zoom-pop:        340ms;  /* one island: 0.94, over to 1.015, settle at 1 */

  /* --ease-air (cubic-bezier(0.34, 1.28, 0.64, 1)) is RETIRED, 2026-08-29: it
     overshot 2.6% and clipped the headline's tops through .ln's overflow. The
     assembly below overshoots 1.5% by KEYFRAME on unclipped boxes instead. */

  /* GENTLE. Nearly symmetric, no snap at either end: the curve for something
     that should appear without announcing itself. Used for the mark's fade-in,
     where an ease-out reads as an arrival and this reads as a materialisation. */
  --ease-gentle: cubic-bezier(0.33, 0, 0.3, 1);
}
:root[data-theme="dark"] { --intro-ground: #141619; }

/* THE HERO ASSEMBLES, it does not cascade. The shipped cold-open ladder (.co
   rise plus the h1 line lift) is switched off under .intro-run and replaced by
   one island per element, top to bottom, a --zoom-step apart. The ladder's own
   rules are untouched, so an ungated load is exactly as it was. The delays are
   from the moment .intro-lift lands, i.e. the end of the hold. */
:root.intro-run .hero-early, :root.intro-run .eyebrow, :root.intro-run .hero h1,
:root.intro-run .hero-sub, :root.intro-run .hero-actions { opacity: 0; animation: none; }
:root.intro-run .hero h1 .ln span { animation: none; transform: none; }
:root.intro-lift .hero-early   { animation: introIsland var(--zoom-pop) var(--ease-out-strong) var(--zoom-hero-at) both; }
:root.intro-lift .eyebrow      { animation: introIsland var(--zoom-pop) var(--ease-out-strong) calc(var(--zoom-hero-at) + var(--zoom-step)) both; }
:root.intro-lift .hero h1      { animation: introIsland var(--zoom-pop) var(--ease-out-strong) calc(var(--zoom-hero-at) + var(--zoom-step) * 2) both; }
:root.intro-lift .hero-sub     { animation: introIsland var(--zoom-pop) var(--ease-out-strong) calc(var(--zoom-hero-at) + var(--zoom-step) * 3) both; }
:root.intro-lift .hero-actions { animation: introIsland var(--zoom-pop) var(--ease-out-strong) calc(var(--zoom-hero-at) + var(--zoom-step) * 4) both; }
/* the scroll cue keeps its shipped fade, timed to land as the CTA does */
:root.intro-run .hero-cue { animation-delay: calc(var(--intro-hold) + var(--zoom-hero-at) + var(--zoom-step) * 7); }
@keyframes introIsland {
  0%   { opacity: 0; transform: scale(0.94); }
  70%  { opacity: 1; transform: scale(1.015); }
  100% { opacity: 1; transform: scale(1); } }

/* TWO LAYERS. #intro is the ground and only dissolves. #intro-lock carries the
   lockup and is what FLIES: verso.js measures the nav wordmark just before the
   hold ends and sets the transform inline, see flight() in the opener block
   there. It is a sibling of #intro rather than a child because a child could
   not outlive a dissolving parent. The contour field never moves at all: the
   page arrives THROUGH a field that was there the whole time. */
#intro, #intro-lock { display: none; }
:root.intro-run #intro {
  display: block; position: fixed; inset: 0; z-index: 900;
  background: var(--intro-ground);
  transition: opacity var(--zoom-dissolve) var(--ease-gentle); }
/* THE LOCKUP BECOMES THE NAV WORDMARK, Enzo's call 2026-09-01. It replaced the
   ride-the-panel exit picked earlier the same day, which had replaced a 700ms
   dissolve that ghosted over the headline. It is a FLIP in verso.js: the
   mark's centre is the transform origin, the nav mark's centre the target, the
   scale the ratio of the two mark heights (both 0.786 of their --wm, so 24/50),
   and the word is translated so the gap lands on the nav's 0.571 x --wm after
   scaling. Measured landing: 0.08px off the nav mark, same 20.3 x 18.9 box, so
   the swap to the real wordmark on landing is invisible.
   No transition here on purpose: verso.js sets it with the transform, off
   --zoom-flight and --zoom-flight-at, so one place owns the flight. */
:root.intro-run #intro-lock {
  display: flex; align-items: center; justify-content: center;
  position: fixed; inset: 0; z-index: 902; pointer-events: none;
  color: var(--ink); will-change: transform; }

/* THE WHITE DISSOLVES TO PAPER. #FFFFFF to #F5F6F5 is a 4% step, so this is
   barely visible and exists to avoid a hard cut, not to be seen. */
:root.intro-lift #intro { opacity: 0; }

/* THE SPLASH IS A PLAIN GROUND. Enzo's call 2026-08-29 (second pass): the
   contour field does NOT come up into the splash. #intro paints
   --intro-ground on its own, white in light and #141619 in dark, and the field
   stays where it always is, at z-index -1 under an opaque panel. Reverted from
   the first pass, which raised the real .topo to z-index 901 for the hold and
   dropped it back on landing. Two things went with it: the breathe animation
   that made the field move, and the outer <g> in verso.js that only existed to
   carry it. THE FIELD DOES NOT ANIMATE, on this page or any other. If a splash
   pattern comes back it is its own element, never the site's field. */

/* ══════════════ THE MARK DRAWS, THEN HANDS OFF TO THE WORD ══════════════
   Enzo's lab settings of 2026-08-29, emitted from _design-exploration/intro-lab.
   RETUNE IN THE LAB AND RE-EMIT. Every number below came off a slider, so hand
   editing here is how the lab and the page drift apart.

   THE SEQUENCE, each beat a separate answer to "why does this move":
     0 to 790      the three pieces of the mark DRAW themselves in
     820 to 1300   the middle stick runs the nav's launch gesture
     1280 to 1900  the lockup SCOOTS left and the word it uncovers arrives
     2100          the landing: the white dissolves, the lockup flies to the
                   nav slot and becomes the wordmark, the hero assembles
   3250ms to fully landed, --intro-hold plus --intro-lift.

   WHY THE MARK IS THREE ELEMENTS NOW. It was two CSS pseudos on .wordmark, and
   pseudos cannot be staggered independently because there are only ever two of
   them. The artwork actually holds three visual parts, so its alpha channel was
   flood filled and split into wing / stick / slash, each re-emitted onto the
   SAME 184x171 canvas, which is what keeps them in register when stacked.
   THE NAV WORDMARK IS UNTOUCHED and keeps its two-pseudo build. */

/* --p-* MUST BE REGISTERED or none of this animates. An unregistered custom
   property is an untyped token: CSS swaps it at the halfway point instead of
   interpolating, so the sweep would jump straight from hidden to shown with
   nothing in between. Registering it as <number> is what makes it an animation
   rather than a switch. */
@property --p-wing  { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --p-stick { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --p-slash { syntax: '<number>'; inherits: false; initial-value: 0; }

:root {
  /* TIGHTENED 2026-08-29 on Enzo's "plays for too long", 4420ms to 3200ms, a
     28% cut. Trimmed proportionally rather than by lopping one beat, so the
     shape he tuned in the lab survives and only the tempo changes. Two beats
     took more than their share and both had it to give: the 220ms of dead air
     between the draw finishing and the launch starting is gone, the launch now
     firing as the draw lands, and --word-track came off hardest because it was
     the BINDING term, 950ms of tracking still opening 320ms after every letter
     had already arrived. Nobody watches the end of a settle. */
  /* the draw */
  --wing-delay:    0ms;  --wing-dur:  700ms;
  --stick-delay: 350ms;  --stick-dur: 440ms;
  --slash-delay: 220ms;  --slash-dur: 470ms;
  /* the stick's launch, the nav's gesture */
  --launch-at:   820ms;  --launch-dur: 480ms;
  /* the scoot, and the word it spawns */
  --scoot-at:   1280ms;  --scoot-dur:  520ms;
  --word-stagger:  70ms; --word-dur:   260ms; --word-track: 620ms;

  /* SIZING IS ENZO'S LAB PAIR, and it is a KNOWING BREAK from the .wordmark
     component's locked ratio. The component ties mark to type at 0.786 : 1;
     this is 1.4 : 1, so the mark reads about twice as heavy against the type as
     it does in the nav. Asked and confirmed 2026-08-29: the intro lockup is its
     own thing and is not required to match the nav. That is also why none of
     this rides .wordmark, which would drag the component's own gap and
     font-size ratios in and then need overriding back out again. */
  --intro-wm:    50px;   /* the mark's box, artwork aspect 184:171 */
  --intro-type:  50px;   /* the word, raised from 30px to match the mark */
  --intro-gap:    7px;   /* between them. Enzo's lab pick 2026-08-30, down from
                            28.5. That was the nav component's 0.571 x --wm
                            ratio, which is tuned for a 24px mark beside 24px
                            type; at 50px on both it read as two objects rather
                            than one lockup. 7px is tight on purpose. */

  /* WEIGHT IS AT ITS CEILING AND THIS IS THE ONLY WAY PAST IT. Enzo asked for
     bolder letters 2026-08-29. font-weight cannot answer: CLAUDE.md locks the
     site to 400 and 500 only, and independently of that rule there IS no
     heavier Satoshi in the repo, just Regular and Medium, so 600 or 700 would
     get SYNTHETIC bold, the browser smearing the 500 glyphs sideways. That
     looks worse than 500, not bolder.

     -webkit-text-stroke thickens the real outline instead, so the letterforms
     stay Satoshi's rather than being faked. It strokes centred on the contour,
     so half the width goes inward and the counters tighten slightly. Keep it
     small: 0.4px on 50px type is about a third of a weight step and reads as
     weight. Past roughly 1px the bowls start filling in and it reads as a
     defect. Set to 0 to see the honest 500.
     The real fix, if this is not enough, is a Satoshi Bold woff2 in the folder
     plus an @font-face for it, and that is Enzo's call because it breaks the
     two-weights rule deliberately rather than by accident. */
  --intro-weight: 0.4px;
  --intro-word-shift: -0.14em; /* measured, cancels Satoshi's ascent bias */

  /* THE SCOOT'S CURVE. Emil's strong ease-in-out, and the decision framework
     picks in-out rather than out because the lockup is MOVING ON SCREEN rather
     than entering it: it is already visible and travelling to a new position,
     which wants acceleration at both ends. An ease-out would start at full
     speed and read as a shove. */
  --ease-move: cubic-bezier(0.77, 0, 0.175, 1);
}

/* ───────── the lockup ─────────
   A row of two, centred by #intro-lock's flex. The row is ALWAYS its full
   width, word included, because clip-path does not affect layout, and that is
   what makes the pure-CSS scoot distance below work at all. */
.intro-lockup { display: flex; align-items: center; gap: var(--intro-gap);
  transform-origin: center; }
/* ONE MASK LAYER PER ELEMENT, and that is the whole reason for the inner <b>.
   Each piece needs two cuts: its artwork, and the sweep that draws it in. Put
   both on ONE element as a two-layer mask and Chrome composites them through an
   offscreen buffer sized to that element, and wherever the element's box does
   not land on the device pixel grid that buffer's own edge antialiases into the
   frame and paints a faint grey RECTANGLE around the mark, in ink the artwork
   never had.

   Rounding the box to whole pixels only half fixed it: measured at one lockup
   width the rim fell from 166 stray pixels to 61, level with a single-layer
   mask, but raising the word to 50px moved the row's origin to a different
   fraction and it came straight back at 132. POSITION matters as much as size,
   and position here is flex centring plus an animated transform, so it cannot
   be snapped.

   So neither element carries two layers. The outer <i> masks with the SWEEP
   alone and owns --p and the launch; the inner <b> paints the ink and masks
   with the ARTWORK alone. Nesting composes them the same way intersect did,
   except a single-layer mask can only ever REMOVE alpha, so no stage can add
   coverage the artwork did not have. The rim cannot occur, at any size, at any
   sub-pixel offset.

   Also ruled out by measurement before landing here: mask-composite intersect
   vs add on the lower layer, pushing the gradient's hard stop outside the box
   with an overshoot, and oversizing the sweep layer to 140%. It was never the
   gradient. It was the buffer. */
.intro-mark { position: relative; flex: none;
  width: calc(var(--intro-wm) * 0.846); height: calc(var(--intro-wm) * 0.786); }
.intro-mark i { position: absolute; inset: 0; display: block; }
.intro-mark b { position: absolute; inset: 0; display: block;
  background: currentColor;
  -webkit-mask-image: var(--art); mask-image: var(--art);
  -webkit-mask-size: contain; mask-size: contain;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; }

/* THE SCOOT DISTANCE IS COMPUTED, NOT MEASURED, and that is the whole trick.
   translateX(50%) is half the ROW's own width, so "50% minus half the mark"
   lands the MARK on the centre while the row runs off to the right. Animating
   that to 0 centres the ROW instead, which is exactly "scoot left by half of
   gap plus word". Nothing measures the word, and if the word, the gap or the
   type size ever change the distance corrects itself. 0.423 is half of the
   0.846 the mark's own box is already built from. */
:root.intro-run .intro-lockup {
  animation: introScoot var(--scoot-dur) var(--ease-move) var(--scoot-at) both; }
@keyframes introScoot {
  from { transform: translateX(calc(50% - var(--intro-wm) * 0.423)); }
  to   { transform: translateX(0); } }

.intro-mark .pc-wing {
  --art: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAACrCAYAAADVcVbyAAACmElEQVR42u3bMU7rYBSE0WskVoQ7tpksCapkRSA5TZCgdYrgmXMkCkqS78377TjLtm3bdPmcmfehwlIY+J+/XwLZXsr//m1mLjLIXvCfN7r+tfAS5C74cv+5lq854UeUtTx0kYceUbzhjiuVF5lL6ZttycvuojQeWz7kkX9EaV83R5XC++BNa/4lkb4Fb1tzK178SWb7vXMKPqpfwyP3UX7xEaXlyOKY4mErIdDxNKHIiX9cVuTEPw++zMy3l5jkLzy8BrxGJ5m4izLBd1cctyy4SPCdzCNG7lNaR5To44r/eSx4bDTWW+C7nQ/w2qzycESZBx9ievvH6y1wgceex529HVFiQ3L2tuDRS269LTgI/Iirab0FLi4E7qEqBC4yGhf86h8WabcJ58m3DQVuwWODE7fAQeBHXFbrLXAQ+BEX1kNV4y7KBN9RcTyx4LErbr0tePSKW28LHst6W/DoFbfeFtx6Y8GPuODW24LH3k2x3hY8esWttwUHgT/T2XqTfETZe0wRuAX3ZWIEDgK33gh83M/GReYjF5rWG2dwBO7sjcBB4C5CcZH58EWm4wmxC269iV5w603sgltvohfcehOx4CfrTdvz4Nab2IetrDdRga+/or7ef4eoi8yZmYu4SQ4cPGyFwEHgIHAQOAgcBA4CR+AgcBA4CBwEDgIHgSNwEDgIHAQOAgeBg8AROAgcBA4CB4GDwEHgCBwEDgIHgYPAQeAgcAQOAgeBg8BB4CBwEDgCB4GDwEHgIHAQOAgcgYPAQeAgcBA4CBwEjsBB4CBwEDgIHAQOAkfgIHAQOAgcBA4CB4EjcBA4CBwEDgIHgYPAETgIHAQOAgeBg8BB4AgcBA4CB4GDwEHgIHAEDgIHgYPAQeCw1w0omojzt8T5twAAAABJRU5ErkJggg==");
  -webkit-mask-image: linear-gradient(134deg, #000 calc(var(--p-wing) * 100% - 0%), rgba(0,0,0,0) calc(var(--p-wing) * 100%)); mask-image: linear-gradient(134deg, #000 calc(var(--p-wing) * 100% - 0%), rgba(0,0,0,0) calc(var(--p-wing) * 100%));
  -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  animation: drawWing var(--wing-dur) cubic-bezier(0.33, 0, 0.3, 1) var(--wing-delay) both; }
@keyframes drawWing {
  from { --p-wing: 0; opacity: 0; transform: translate(43.2px, 41.7px); }
  to   { --p-wing: 1; opacity: 1; transform: none; } }
.intro-mark .pc-stick {
  --art: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAACrCAYAAADVcVbyAAADLUlEQVR42u3dQU4jMRRF0e8oSypE9gksCJBSMKg1mTkTkAKR/XzuAlp09eO0KSNovfeSUjt7BMM0ijQt6aGe7GqIYXefaAae1sfAY+oGrlvH/Tjwx9cMXKnjLkcUJY876ovM5jWhs623KFpl3EfaQye4gcfqTXDjjtab4AYerTfB625vTuhNcHrTm+Cz9UZvgtOb3gSnN70JTm96E5zeq+hNcHoXwZXYscpflOD0JrjoTXDRm+CiN8HpTW+Ci94Ep/fSehOc3gauH7tO8DFuq/7jOKLkH0+OlQdOcHoTXPQmOL3pTXB6ezVIcHobN8HpbeAEXxguj8DA/Q4bAxe9DZzeMnDR28DpLQMXvQ2c3jJwehu4qnaPwFW94wm9CU5vEZze9CY4vUVweovg9CY4velNcMnA6U1vA5cMnN4ycMnA6W3gkoHTm94GLhl4uZYvv37EVb3jiQhOb4J3etOb4PSmN8HpTW+CD9EzvQlOb3oTXPQmOL3pTXDRm+D0FsHprRUFpzfBRW8DL9fyf95migZ+Sxd6Gzi96W3g9Ka3gdOb3gZOb/MzcHrLwOmtWvAms7u11Mm46U1wA6c3wY1bBr78/5gegYHTWwZOb602cHqL4PQ28HItL+/BHU/oTXB6i+D0FsHprRUFp7diBae3ogWnt1z0yMDpTW8Dlwyc3vQ2cMnA6S2CS0kDp7cILs048J3eSr6qdzxRrOD0VrTg9Fas4LsfoqlkwemtWMHprWjB6S0XPfTWjILTWwSnt2YUnN4iOL01o+D0FsHprRkFp7cITm/NOPD3gZ/RZiaOKKnHk8PACV7B31Rl3AZ+cxdnb6UOnN6KHji9FTtweit64PRWJb8m7G4tlSp4p7eSBae3YgWnt6IFp7diBe8euXy7LL014cDpLYLTWzMO/NWjVvJbFG9OFCv41WNWsuD0Vqzg9Fa04PRWrOD0VrTg9JaLHmnGgdNbBJcMnN4abOBPHquSB/5Ab6W/JuwGLmdw45aBG7fGG3jzYyBEcL+ZQRMPvBm30gVvxq30I8q9Rv5i3Preue73NqN7W6LkLzJbVX3+w59p3Ir7AfhGrV/1Bc9kMteozNZbAAAAAElFTkSuQmCC");
  -webkit-mask-image: linear-gradient(221deg, #000 calc(var(--p-stick) * 126% - 26%), rgba(0,0,0,0) calc(var(--p-stick) * 126%)); mask-image: linear-gradient(221deg, #000 calc(var(--p-stick) * 126% - 26%), rgba(0,0,0,0) calc(var(--p-stick) * 126%));
  -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  animation: drawStick var(--stick-dur) cubic-bezier(0.33, 0, 0.3, 1) var(--stick-delay) both,
             introStick var(--launch-dur) both var(--launch-at); }
@keyframes drawStick {
  from { --p-stick: 0; opacity: 0; }
  to   { --p-stick: 1; opacity: 1; } }
.intro-mark .pc-slash {
  --art: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAACrCAYAAADVcVbyAAADaElEQVR42u3dQU4bQRiE0eqIG+EFx8wiF0JIJAvmQtk4q2SJEJgwf/X7ToBw6dHMjO11vV4jtXbnV6BP6CxqLgNX47D//TzfvCYqHXeSxMBVO24DV/W4DVzV4zZwVY87yTJwVWfgqtXbwEVwaareBi6CS1P1NnARXJqqt4GL4FKSXxP1TpLlHT1qPZ4QXG/paeq4Ca5qvQmu2rM3wVWvN8FVrTfBVa03wTW5FTd6VKx3DFytHW9m3hlcjWdvgqteb4KrWm+Cq1pvgqtab4KrWm8D198eB/yMl3eR74iiAceT470DJ7hq9Sa4qvUmuKr1Jriq9Sb43r20623ge3ffeN3bEUVTjicrngdX8dk7BBe9Ca5d9Sa4gVfrTXDjrtab4AZerTfBjbtab4IbeLXeBN+n5x31Jji9q/UmOL3rIzi9a/UmOL0JLnpP1Zvg9Ca46D1Vb4Irvspb9B6qt4GL4KL3VL0NXAQXvafqbeAiuOg9VW8DF8E1op/0dqve8WTDgROc3rXjJji96wdOcHrXjpvg9K4fOMHn9mTcBKf35gMnuGrHbeD0jjuZ0lC9DZzeBJdygg/RdBWF3o4nBNdOehOc3tV6E1zVeht4PFR1oy4Gro/0QG8Dpze9DZze++ht4PSu1tvA6V2tt4Gfu+/0jhs9cWPnq/Q2cNUOfMXThCrWO76ESvQmuOhNcAOnN8GNm94EN/Ct9Cb4ufpNb4LTm94Ej4eq6E1werfrTXB6V+tNcHpX601wetdHcHrX6k1wxYdvit4GLhm46G3gkoHTm94GLhk4vYv1NnARXNnhq7dX3KqX4wnBlXEPVa142Er0JrjoTXB6GzjB6W3cBKe3gRNcG4/bwPf+6m0DF70NXPQ2cNHbwOktAxe9DZzeMvCtOgxcKX5DwyU+m1Clx5Nj54ETPPUPVV3iDK4P9uDsbeD0preB05veBk5vehs4veUyYUovDS4vDcGbr3uL4PQmuOhNcAOnN8GNm94EN3B6E9y46U1weovgKXlDA70JTm+CKwMfqqI3welNcL3WD3oTnN70JrjoTXCC05vg9BbBWwWnN8HpLQOfJubhnfKOKM3HFEcTgteOy7gNvPb8a9wGnv/1SVGHcRu4kd/mr4Vx+yfzy3pJck9tgjdLvqh9/u78Cm6m7ZXY5+sPOwo84YA3LUwAAAAASUVORK5CYII=");
  -webkit-mask-image: linear-gradient(221deg, #000 calc(var(--p-slash) * 100% - 0%), rgba(0,0,0,0) calc(var(--p-slash) * 100%)); mask-image: linear-gradient(221deg, #000 calc(var(--p-slash) * 100% - 0%), rgba(0,0,0,0) calc(var(--p-slash) * 100%));
  -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  animation: drawSlash var(--slash-dur) cubic-bezier(0.33, 0, 0.3, 1) var(--slash-delay) both; }
@keyframes drawSlash {
  from { --p-slash: 0; opacity: 0; }
  to   { --p-slash: 1; opacity: 1; } }

/* THE STICK'S LAUNCH. Two animations on one element: its draw, then the nav's
   gesture. Out down the slant, an invisible reposition at zero opacity, back
   from above. The vectors are the artwork's own 20.38deg slant, x = 0.372y, the
   same pair the nav uses, so the two agree about the geometry.

   BOTH CURVES ARE LITERAL ON PURPOSE. animation-timing-function inside a
   @keyframes block does NOT accept var(): Chrome drops the declaration and
   falls back to `ease` with no warning and no error. wmBarLaunch in the nav has
   that bug right now, which is why its return leg runs on `ease` rather than
   the --ease-out-strong its source asks for. Do not tidy these into tokens. */
@keyframes introStick {
  0%     { transform: none; opacity: 1;
           animation-timing-function: cubic-bezier(0.5, 0, 1, 0.6); }
  40%    { transform: translate(-45%, 130%); opacity: 0;
           animation-timing-function: cubic-bezier(0, 0, 0.2, 1); }
  40.01% { transform: translate(45%, -130%); opacity: 0; }
  100%   { transform: none; opacity: 1; } }

/* ───────── the word the scoot uncovers ─────────
   Enzo's call 2026-08-29: the word should read as SPAWNED BY THE LOGO, the
   scoot itself being what brings it into existence.

   IT IS A CLIP, NOT AN OCCLUSION, and that is forced rather than chosen. The
   obvious build is to tuck the word under the mark and let the mark slide off
   it. That cannot work here: the mark is ink on transparent, a masked PNG with
   no opaque ground, so a word behind it would show THROUGH the gaps in the
   artwork instead of being hidden by it. Clipping the word to nothing and
   opening the clip left to right gets the same read honestly: the word does not
   exist yet to the left of the edge, so it materialises in the space the mark
   has just vacated.

   The clip and the scoot share a start, a duration and a curve, so the reveal
   edge tracks the mark exactly rather than racing or trailing it. */
/* ALIGNED ON INK, NOT ON BOXES. align-items: center on .intro-lockup lines up
   the two BOXES, and boxes are not what anyone looks at. Measured before the
   fix: mark ink centre 399.5, word ink centre 406.5, so the word sat 7px LOW
   while both boxes agreed on 400 exactly.

   THE CAUSE IS THE FONT, NOT THE LEADING, and the obvious suspect was wrong.
   line-height was 1.55, inherited from body copy, giving the word a 77.5px line
   box against the mark's 39px. Setting line-height: 1 shrank that box to 50px
   and moved the ink NOT AT ALL, still +7.0. Half-leading is distributed evenly
   above and below, so it never shifted the ink relative to the box centre in
   the first place. What is left is Satoshi's own ascent/descent asymmetry:
   "verso" is five x-height glyphs, no ascender, no descender, so its ink sits
   below the centre of any box you give it.

   line-height: 1 stays because 1.55 is a body-copy value and wrong for a lockup
   on its own merits, but --intro-word-shift is what actually does the work.
   0.14em, in em so it survives a change to --intro-type. RE-MEASURE IT if the
   typeface changes: screenshot the settled frame, compare the two ink centres,
   never the two boxes. */
.intro-word { font-family: var(--display); font-weight: 500;
  font-size: var(--intro-type); letter-spacing: -0.015em; white-space: pre;
  line-height: 1;
  -webkit-text-stroke: var(--intro-weight) currentColor;
  transform: translateY(var(--intro-word-shift));
  clip-path: inset(0 100% 0 0); }
:root.intro-run .intro-word {
  animation: introReveal var(--scoot-dur) var(--ease-move) var(--scoot-at) both,
             introTrack var(--word-track) var(--ease-out-strong) var(--scoot-at) both; }
@keyframes introReveal {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); } }

/* THE SETTLE, the half of the reference that makes it read as landing rather
   than appearing. Text animation.mov starts its line bunched and opens the
   tracking to final, so the units push each other apart as they arrive.
   -0.125em to the shipped -0.015em is Enzo's -0.11 of extra tightness. */
@keyframes introTrack {
  from { letter-spacing: -0.125em; }
  to   { letter-spacing: -0.015em; } }

/* PER LETTER, staggered left to right so each one lands as the clip clears it.
   85ms is a shade over Emil's 30-80ms band, kept because it is Enzo's tuned
   value and because five letters at 85ms is 340ms of cascade, still short.

   BLUR IS ISOTROPIC HERE AND DIRECTIONAL IN THE LAB, a deliberate downgrade.
   The reference smears horizontally and the lab reproduces that with an SVG
   feGaussianBlur at stdDeviation "6 0". CSS cannot INTERPOLATE between
   filter: url(#f) and filter: none, it swaps them, so shipping the SVG filter
   would resolve every letter's smear with a visible pop. A pop is a defect;
   isotropic versus directional at 6px on 30px type is a nuance. If the smear
   turns out to matter, the fix is two stacked copies per letter, one blurred
   and one sharp, crossfading opacity. Not an animated filter. */
.intro-word i { display: inline-block; font-style: normal;
  animation: introChar var(--word-dur) var(--ease-out-strong) both; }
.intro-word i:nth-child(1) { animation-delay: calc(var(--scoot-at) + var(--word-stagger) * 0); }
.intro-word i:nth-child(2) { animation-delay: calc(var(--scoot-at) + var(--word-stagger) * 1); }
.intro-word i:nth-child(3) { animation-delay: calc(var(--scoot-at) + var(--word-stagger) * 2); }
.intro-word i:nth-child(4) { animation-delay: calc(var(--scoot-at) + var(--word-stagger) * 3); }
.intro-word i:nth-child(5) { animation-delay: calc(var(--scoot-at) + var(--word-stagger) * 4); }
@keyframes introChar {
  from { opacity: 0; transform: translateX(29px); filter: blur(6px); }
  to   { opacity: 1; transform: none; filter: blur(0); } }

/* THE NAV IS ALREADY IN PLACE and its items pop in first, so the mark lands in
   a settled bar. The wordmark slot stays empty until the flying lockup has
   landed on it; .intro-done is what swaps them, and by then they are the same
   pixels. */
:root.intro-run .nav .wordmark { opacity: 0; }
:root.intro-done .nav .wordmark { opacity: 1; }
:root.intro-run .nav-mid, :root.intro-run .nav-right { opacity: 0; }
:root.intro-lift .nav-mid   { animation: introIsland var(--zoom-pop) var(--ease-out-strong) var(--zoom-nav-at) both; }
:root.intro-lift .nav-right { animation: introIsland var(--zoom-pop) var(--ease-out-strong) calc(var(--zoom-nav-at) + var(--zoom-step)) both; }

/* Belt to the head script's braces. Someone who asked for less motion should
   not get a three second gate in front of the page they came for. */
@media (prefers-reduced-motion: reduce) {
  :root.intro-run #intro, :root.intro-run #intro-lock { display: none; }
  :root.intro-run .nav .wordmark, :root.intro-run .nav-mid, :root.intro-run .nav-right,
  :root.intro-run .hero-early, :root.intro-run .eyebrow, :root.intro-run .hero h1,
  :root.intro-run .hero-sub, :root.intro-run .hero-actions { opacity: 1; animation: none; }
  :root.intro-run .hero-cue { animation-delay: 0s; }
}
