/* infiDive — shared design system (2026-07-24 redesign) */

:root {
  /* ======================================================================
     COLOUR — taken from the C-side product's own Figma file, not invented
     here. See docs/app-palette.md for the extraction and the evidence.

     The app is LIGHT-CHROME + DARK-CONTENT, not a single dark world: its
     bond row and chat surfaces are near-white, its episodes are full-bleed
     dark art. The site inherits that duality, because the site rebuilds the
     app's UI in HTML — if the two disagree on light vs dark, the rebuilt
     screens are simply lying about the product.

     Every value below is contrast-verified; the numbers are the measured
     WCAG ratios against that mode's own background.
     ====================================================================== */

  /* --- chrome: light ---------------------------------------------------- */
  --paper: #f8f8f8;          /* the app's page ground (its most common pixel) */
  --ink: #0a0a0c;            /* content ground, for episode surfaces */
  --surface: #ffffff;        /* cards and bubbles — white on grey, as the app does */
  --surface-2: #eceef2;      /* recessed */
  --muted: #d2d2d3;          /* the app's weak band */
  --border: rgba(16, 17, 20, 0.1);
  --border-strong: rgba(16, 17, 20, 0.2);
  /* One ramp, one hue direction. These three used to be #101114 (cool, B>G>R),
     #5a5560 and #6b6771 (both warm/violet, R>=G) — i.e. the page's near-black
     leaned cool while its two greys leaned purple, and the product-demo layer
     had a THIRD cast (#5b5d67, cool). Three hue families inside one viewport is
     what reads as "not quite precise" long before anyone can name why.
     Everything is now cool-neutral (B > G > R, same slope), and the contrast
     ratios are held at the values the old ramp was verified at. */
  --text-hi: #101114;        /* 17.78:1  AAA */
  --text-lo: #55575f;        /*  6.78:1  AA   (was #5a5560, 6.81 — parity) */
  --text-faint: #666871;     /*  5.23:1  AA   (was #6b6771, 5.20 — parity) */

  /* The accent is text-capable on light ONLY at this depth. #946efe, the
     gradient's own violet end, is 3.34:1 here — large text at best. That is
     why the product owner handed over #3E119D specifically. */
  --accent: #3e119d;         /* 11.16:1  AAA on --paper */
  --accent-soft: rgba(62, 17, 157, 0.1);
  --accent-ink: #ffffff;     /* 11.85:1 on the accent */

  /* The brand mark is a GRADIENT, not a solid: in the app it rings whichever
     character you are currently bonded with. Graphics only — the cyan end is
     1.19:1 on light and must never carry text. */
  --acc-cyan: #82f8ed;
  --acc-mid: #88d0f2;
  --acc-violet: #946efe;
  --acc-grad: linear-gradient(112deg, #82f8ed 0%, #88d0f2 46%, #946efe 100%);

  /* Character identity hues, the second layer of the app's colour system:
     inactive characters are ringed in their own colour, the active one in the
     brand gradient. Extend this list as the real cast is confirmed. */
  --id-elias: #fd4c59;
  --id-lead: #86fe4d;

  /* Retired. The old site was near-black + #ff5924 warm orange; the product
     has no orange anywhere. Kept only so any stylesheet still referencing
     --ai-glow resolves — violet is now the BRAND colour, so the old rule that
     quarantined violet to "AI is responding" no longer applies. */
  --ai-glow: rgba(148, 110, 254, 0.5);

  /* type */
  --font-display: "Fraunces", "Iowan Old Style", ui-serif, serif;
  --font-body: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", monospace;

  /* ---- SMALL-TEXT SCALE ------------------------------------------------
     Integers only, and only these. The page previously shipped 23 distinct
     pixel sizes of which SIX were fractional (11.5 / 12.5 / 13.5 / 14.5 /
     15.5 / 16.5). A half-pixel step cannot be read as hierarchy — it can only
     be read as nobody having decided — and at 15.5px it does not even survive
     rasterisation. Measured against Linear (10-15, all integers), Stripe and
     Vercel (11/12/14 only).

     The floor is 11px, not 10px. product-demo.css already states that rule for
     the phone mock ("8-10px type was the loudest tell that this is a web page
     drawing an app"); the site layer and the walkthrough were exempting
     themselves from it in nine places. Same rule now, everywhere.

     These lived in three separate blocks (.wt, .stage, and as literals in
     product-demo.css) which is why half the page could not reach them. */
  --t-11: 0.6875rem;
  --t-12: 0.75rem;
  --t-13: 0.8125rem;
  --t-14: 0.875rem;
  --t-15: 0.9375rem;
  --t-16: 1rem;
  --t-17: 1.0625rem;

  /* ---- LINE-HEIGHT LADDER ---------------------------------------------
     body sets 1.6, which is right for a 16-18px paragraph and wrong for
     everything below it: inherited unchanged it put 11px labels on a 17.6px
     line and 13px captions on 20.8px, so small type floated in a band nearly
     twice its own height. The references tighten as size drops — Linear 1.4 at
     12px / 1.5 at 13-14, Stripe 1.33-1.45 at 11-12, Vercel 1.33 at 12,
     Mobbin 1.43 at 14. Nothing in the sub-15px band may inherit 1.6.

     --lh-flush is for type that a box positions (pills, badges, buttons):
     there the padding sets the height and any line-height above 1 fights it. */
  --lh-11: 1.35;
  --lh-12: 1.4;
  --lh-13: 1.45;
  --lh-14: 1.45;
  --lh-15: 1.5;
  --lh-body: 1.6;
  --lh-flush: 1;

  /* ---- MICRO-LABEL ----------------------------------------------------
     The one recipe for every inert uppercase label on the page (.eyebrow,
     .pd-kicker, .wt-stamp, .plan .name, table headers, FAQ groups). Mono at
     600 with wide tracking, one step DARKER than body-secondary rather than
     lighter: a label earns its quietness from size and case, not from being
     faded until its stems break up. */
  --label-tracking: 0.13em;

  /* ---- DISPLAY STEPS --------------------------------------------------
     The small end of this file has had a scale since the token block
     above; the large end never did. Thirteen headlines each carried a
     hand-tuned font-size and then made three more decisions on their own,
     which is how the page ended up with twenty distinct letter-spacing
     values (six of them clustered between -0.005em and -0.02em, a spread
     no eye resolves) and a font-weight column that was flat at 600 from
     an 11px label to a 76px headline.

     A step here is FIVE values, not a size. Read across a row and the
     relationship is the point: as the size climbs the weight comes DOWN,
     the tracking goes further NEGATIVE, the leading TIGHTENS, and opsz
     follows the size the line will actually render at.

     That last one was the quiet defect. opsz is an optical-size axis
     measured in points: Fraunces at "opsz" 144 is drawn with hairlines
     meant for a 144pt line. Seven rules pinned 144 regardless of their
     own size, so .stage-exit was rendering 144pt hairlines at 36px and
     .stage-tail .wt-display at 56px — thin and underfed, which reads as
     cheap rather than as delicate. opsz now tracks the step.

     Sizes are deliberately NOT tokenised here. Each of the seven live
     headlines is clamped against its own container and the homepage
     cannot be verified by export (the WebGL stage times the renderer
     out), so moving sizes would be betting on something unverifiable.
     The four companion values are what were actually adrift.

     Fraunces is loaded as wght@400..700 (was 500..700 — nothing below
     500 was ever downloaded, which is also why .wt-lockclock's
     `font-weight: 300` has been silently clamped for as long as it has
     existed). 480 is reachable only because of that widened request; if
     the font URL is ever narrowed again, peak silently snaps to 500. */
  --fw-peak: 480;     --tr-peak: -0.045em;     --lh-peak: 0.78;     --opsz-peak: 144;
  --fw-display: 500;  --tr-display: -0.032em;  --lh-display: 0.98;  --opsz-display: 96;
  --fw-h1: 520;       --tr-h1: -0.026em;       --lh-h1: 1;          --opsz-h1: 56;
  --fw-h2: 550;       --tr-h2: -0.02em;        --lh-h2: 1.1;        --opsz-h2: 40;
  --fw-h3: 570;       --tr-h3: -0.015em;       --lh-h3: 1.15;       --opsz-h3: 32;

  /* radius — geometric progression, not round numbers */
  --radius-sm: 0.75rem;
  --radius-md: 1rem;
  --radius-lg: 1.333rem;
  --radius-xl: 1.777rem;

  /* motion */
  --ease-dive: cubic-bezier(0.22, 0.61, 0.36, 1);
  --duration-slow: 620ms;
  --duration-fast: 180ms;

  --max-width: 1120px;
}

/* ========================================================================
   DARK-CONTENT SCOPE.
   Any surface that is "the episode" rather than "the app around it" opts in
   with .on-ink, and the whole neutral + accent set inverts. Components never
   need to know which mode they are in — they read --text-hi and --accent and
   get the right one. This is the single mechanism that lets the site carry the
   app's light-chrome/dark-content duality without two copies of every rule.

   The accent swaps ends of the same gradient rather than changing hue: the
   deep violet reads on light, the bright violet reads on dark. Same brand,
   opposite ground.
   ======================================================================== */
.on-ink {
  --paper: #0a0a0c;
  --surface: #16161c;
  --surface-2: #1e1e26;
  --muted: #2a2a33;
  --border: rgba(246, 247, 248, 0.1);
  --border-strong: rgba(246, 247, 248, 0.2);
  --text-hi: #f6f7f8;        /* 18.44:1  AAA */
  --text-lo: #a6a8b2;        /*  8.35:1  AAA  (was #a8a6ae — warm cast) */
  --text-faint: #83858f;     /*  5.38:1  AA   (was #82808a — warm cast) */
  --accent: #946efe;         /*  5.58:1  AA  on --paper */
  --accent-soft: rgba(148, 110, 254, 0.16);
  --accent-ink: #0a0a0c;     /*  5.58:1 on the accent */
}

* { box-sizing: border-box; }

html {
  max-width: 100%;
  overflow-x: clip;
  scroll-behavior: smooth;
}

/* Reduced motion. NOTE: this deliberately does NOT use the common
   `animation-duration: 0.001ms !important` sledgehammer. That pattern resets an
   animation to frame 0, which would leave the walkthrough's breathing ring at its
   dimmest frame and its progress bars at zero width -- i.e. it would break the
   still image rather than freeze it. Instead each looping animation is switched
   off and explicitly HELD at a chosen frame, further down in the walkthrough
   block. See the `.wt` reduced-motion rules. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    transition-delay: 0s !important;
  }
}

/* Visually hidden, but present for assistive tech. The walkthrough's beat spine
   lives here: zero visible words, full screen-reader navigation. */
.vh {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.vh-focusable {
  position: absolute;
  left: 12px; top: 12px;
  z-index: 60;
  transform: translateY(-160%);
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 14px;
  font-weight: 600;
  padding: 11px 16px;
  border-radius: 999px;
  transition: transform 180ms var(--ease-dive);
}
.vh-focusable:focus { transform: translateY(0); }

body {
  margin: 0;
  max-width: 100%;
  overflow-x: clip;
  font-family: var(--font-body);
  background: var(--paper);
  color: var(--text-hi);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

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

.wrap {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 24px;
}

/* ---- Nav ---- */
.nav {
  /* stickiness lives on .nav-shell, which is the scroll-state query container */
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  background: rgba(248, 248, 248, 0.82);
  border-bottom: 1px solid var(--border);
}
.nav .wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 64px;
}
.nav-logo {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 19px;
  /* the value that was actually winning, from the duplicate further down */
  letter-spacing: -0.005em;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 0;   /* 30px tall -> 44 */
}
.nav-logo .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--acc-grad);
  box-shadow: 0 0 10px rgba(148, 110, 254, 0.45);
}
.nav-links { display: flex; gap: 28px; align-items: center; }
/* Hit areas, not just labels. These were 45x22 and 33x22 — a third of the 44px
   minimum the product layer already honours, and the site layer was quietly
   exempting itself from its own rule. Vertical padding only, so nothing moves. */
.nav-links a {
  color: var(--text-lo);
  font-size: var(--t-14);
  /* 1.45 not the inherited 1.6, and the padding goes 11 -> 12 to pay for it:
     tightening the line alone would have taken this hit area to 42.3px and
     quietly broken the 44px rule the comment above exists to enforce. */
  line-height: var(--lh-14);
  padding: 12px 0;
  /* 220ms on --ease-dive, matching the bar and the wordmark. This property
     serves two jobs — hover feedback and the light/dark crossfade over the
     stage — and it was running at 180ms/ease while the bar ran at
     220ms/--ease-dive, so the state change arrived in two instalments. */
  transition: color 220ms var(--ease-dive);
}
.nav-links a:hover { color: var(--text-hi); }
.nav-links .cta {
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
  padding: 12px 18px;   /* 38px tall -> 44 */
  border-radius: 999px;
}
.nav-links .cta:hover { color: var(--accent-ink); }

@media (max-width: 360px) {
  .nav-links > a:not(.cta) { display: none; }
}

/* ---- Hero: full-bleed portrait, no frame ---- */
.hero {
  display: grid;
  grid-template-columns: minmax(300px, 560px) 1fr;
  align-items: stretch;
  /* 60svh, not 90vh: the fold has to cut INTO beat 01 so ~40svh of live product
     UI is on screen at first paint. That is the scroll pull, and it is also the
     fastest product evidence the page can give. */
  min-height: 60svh;
  position: relative;
  overflow: hidden;
}
.hero-copy {
  padding: 104px 32px 40px clamp(24px, 4vw, 64px);
  display: flex;
  flex-direction: column;
  justify-content: center;
  position: relative;
  z-index: 2;
  text-align: left;
}
.hero-visual {
  position: relative;
  min-height: 460px;
}
.hero-visual img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
}
.hero-visual::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(100deg, var(--ink) 0%, rgba(10, 10, 13, 0) 38%);
}
.hero-visual::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(180deg, rgba(10, 10, 13, 0) 55%, var(--ink) 100%);
}
@media (max-width: 860px) {
  .hero { grid-template-columns: 1fr; min-height: auto; }
  .hero-copy { padding: 96px 24px 28px; }
  .hero-visual { height: 34svh; min-height: 240px; }
  .hero-visual::before { background: linear-gradient(180deg, var(--ink) 0%, rgba(10, 10, 13, 0) 30%); }
}
/* Section label. Deliberately NOT a capsule: in a real product a pill outline
   means "tappable", and every one of these labels is inert — which is exactly
   what made them read as template chrome. The label is now typography only, in
   the display face, and the section break it was faking is carried by a real
   rule (see .section-head). Accent purple is freed up for buttons and links. */
/* Mono, not the display serif. Fraunces is a high-contrast face: at 14px its
   hairlines are thinner than a device pixel, so setting it in caps, in a 5.2:1
   grey, at only 0.06em tracking produced the softest, least deliberate type on
   the page — the one place a screenshot at 100% looked blurred. Caps also need
   more tracking than 0.06em at any size.
   Mono is not a new idea here: .wt-stamp, .plan .name, .compare-table th and
   .faq-group > h3 are ALL already mono uppercase labels. The serif eyebrow was
   the outlier, so this is the site agreeing with itself. */
.eyebrow {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--t-11);
  line-height: var(--lh-11);
  font-weight: 600;
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--text-lo);
  margin-bottom: 14px;
}
.hero h1 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(40px, 6vw, 72px);
  margin: 0 0 22px;
  letter-spacing: -0.03em;
  line-height: 1.02;
  text-wrap: balance;
  color: var(--text-hi);
}
.hero h1 em {
  font-style: italic;
  color: var(--accent);
}
.hero p.lead {
  font-size: 18px;
  color: var(--text-lo);
  max-width: 480px;
  margin: 0 0 34px;
}
.porthole {
  position: relative;
  width: min(100%, 420px);
  aspect-ratio: 3 / 4;
  border-radius: 42% 58% 63% 37% / 41% 44% 56% 59%;
  overflow: hidden;
  background: linear-gradient(160deg, var(--surface-2), var(--ink) 70%);
  box-shadow: 0 0 0 1px var(--border), 0 40px 90px -30px rgba(0, 0, 0, 0.6), 0 0 120px -40px var(--accent-soft);
}
.porthole img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
}
.porthole::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(120% 90% at 50% 110%, rgba(10, 10, 13, 0.55), transparent 60%);
  pointer-events: none;
}

/* ---- Simple page header (Pricing/Docs — not the full-bleed homepage hero) ---- */
.page-header {
  padding: 96px 0 24px;
  text-align: center;
}
/* step display. CONSOLIDATED: this rule was declared twice — here at
   30-46px/1.1/-0.02em and again further down at 40-76px/0.98/-0.03em, with
   the second one winning every property it named. Only the later block's
   values were ever on screen; the ones above were dead weight that read like
   the live spec. One declaration now, carrying the larger (real) size.

   `pretty`, not `balance`: both subpage headlines set their own break with a
   <br>, so there is nothing to balance across — balance would only redistribute
   the wrap inside a sentence the author already ended. pretty guards the last
   line against an orphan and leaves the authored break alone. Same reasoning
   as #cast .section-head h2 above. */
.page-header h1 {
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  font-variation-settings: "opsz" var(--opsz-display), "wght" var(--fw-display);
  font-size: clamp(40px, 6.2vw, 76px);
  letter-spacing: var(--tr-display);
  line-height: var(--lh-display);
  margin: 0 0 16px;
  text-wrap: pretty;
}
.page-header .lead {
  font-size: 18px;
  color: var(--text-lo);
  max-width: 560px;
  margin: 0 auto;
}

/* ---- Buttons ---- */
.btn-row { display: flex; gap: 16px; flex-wrap: wrap; }
.hero .btn-row { justify-content: flex-start; }

.appstore-badge {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  background: var(--accent);
  border: 1px solid var(--accent);
  color: var(--accent-ink);
  padding: 12px 22px;
  border-radius: var(--radius-sm);
  font-size: 15px;
  transition: transform var(--duration-fast) ease, opacity var(--duration-fast) ease;
}
.appstore-badge:hover { transform: translateY(-2px); opacity: 0.92; }
.appstore-badge .glyph { font-size: 22px; line-height: var(--lh-flush); }
/* Two stacked lines inside a fixed-height button: the box sets the rhythm, so
   both run flush and the 4px gap is stated once rather than arriving as two
   different inherited leadings. opacity 0.75 -> a real weight step, because
   fading white type on a saturated accent is how the small line lost its edge. */
.appstore-badge .label small {
  display: block;
  font-size: var(--t-11);
  line-height: var(--lh-flush);
  font-weight: 500;
  opacity: 0.82;
  margin-bottom: 4px;
}
.appstore-badge .label strong {
  display: block;
  font-size: var(--t-16);
  line-height: var(--lh-flush);
  font-weight: 600;
}

.btn-secondary {
  display: inline-flex;
  align-items: center;
  padding: 12px 22px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  color: var(--text-hi);
  font-size: 15px;
  transition: border-color var(--duration-fast) ease, opacity var(--duration-fast) ease;
}
.btn-secondary:hover { border-color: var(--accent); opacity: 0.92; }

/* ---- Sections ---- */
section { padding: 64px 0; scroll-margin-top: 88px; }
/* The rule spans the section's content column, not the 640px text measure, so
   it reads as structure rather than as an underline on the label. */
.section-head {
  margin: 0 0 44px;
  border-top: 1px solid var(--border);
  padding-top: 20px;
}
.section-head h2,
.section-head p { max-width: 640px; }
.section-head.centered { text-align: center; }
.section-head.centered h2,
.section-head.centered p { margin-left: auto; margin-right: auto; }
/* step h2. font-weight and the axis carry the SAME number on purpose: a
   pinned "wght" wins over font-weight, but an <h2> whose font-weight is left
   at the UA's `bold` = 700 while the axis says 550 declares two weights for
   one word, and any renderer that misses the axis synthesises the heavier
   one. Both stated, everywhere in this scale. */
.section-head h2 {
  font-family: var(--font-display);
  font-weight: var(--fw-h2);
  font-variation-settings: "opsz" var(--opsz-h2), "wght" var(--fw-h2);
  font-size: clamp(26px, 3.6vw, 36px);
  margin: 0 0 14px;
  letter-spacing: var(--tr-h2);
  line-height: var(--lh-h2);
  text-wrap: balance;
}
.section-head p {
  color: var(--text-lo);
  margin: 0;
  font-size: var(--t-16);
  line-height: var(--lh-body);   /* 16px IS the size 1.6 was chosen for */
  text-wrap: pretty;
}

.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
@media (max-width: 860px) { .grid-3 { grid-template-columns: 1fr; } }

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 28px;
  transition: border-color var(--duration-fast) ease;
}
.card:hover { border-color: var(--border-strong); }
.card .icon {
  width: 44px; height: 44px;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--accent);
  display: flex; align-items: center; justify-content: center;
  font-size: 19px;
  margin-bottom: 18px;
}
.card h3 { font-size: var(--t-17); line-height: var(--lh-15); margin: 0 0 8px; font-weight: 600; }
.card p {
  color: var(--text-lo);
  font-size: var(--t-14);        /* was 14.5 */
  line-height: var(--lh-14);
  margin: 0;
  text-wrap: pretty;
}

/* ---- Vio companion section ---- */
.companion {
  display: grid;
  grid-template-columns: 0.82fr 1.18fr;
  gap: 20px;
  align-items: start;
}
@media (max-width: 860px) { .companion { grid-template-columns: 1fr; gap: 40px; } }
.companion-visual { margin-top: 52px; }
@media (max-width: 860px) { .companion-visual { margin-top: 0; } }
.companion-visual .porthole {
  width: 100%;
  max-width: 260px;
  margin: 0 0 0 auto;
  transform: rotate(3deg);
  position: relative;
  z-index: 2;
}
@media (max-width: 860px) { .companion-visual .porthole { margin: 0 auto; transform: none; } }
.companion-copy { position: relative; z-index: 1; }
.companion-copy h2 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(26px, 3.6vw, 38px);
  margin: 0 0 18px;
  letter-spacing: -0.015em;
  max-width: 12ch;
}
.companion-copy .bio {
  color: var(--text-lo);
  font-size: var(--t-16);        /* was 16.5 */
  line-height: var(--lh-body);   /* was 1.7 — one leading for all 16px prose */
  margin: 0 0 30px;
  max-width: 56ch;
  text-wrap: pretty;
}
.skill-row {
  display: flex;
  flex-direction: column;
  margin-top: 8px;
  max-width: 420px;
}
.skill-pill {
  padding: 16px 0;
}
.skill-pill:first-child { padding-top: 0; }
.skill-pill:not(:first-child) {
  border-top: 1px solid var(--border);
}
.skill-pill .kind {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--t-11);
  line-height: var(--lh-11);
  font-weight: 600;
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 8px;
}
.skill-pill .title {
  display: block;
  font-weight: 600;
  font-size: var(--t-15);
  line-height: var(--lh-15);
  margin-bottom: 6px;
  font-family: var(--font-display);
}
.skill-pill .line {
  display: block;
  color: var(--text-lo);
  font-size: var(--t-13);        /* was 13.5 */
  line-height: var(--lh-13);
  text-wrap: pretty;
}

/* ---- Memory mechanic feature (full-bleed, asymmetric) ---- */
.memory-feature {
  background: var(--surface);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  padding: 100px 0;
}
@media (max-width: 600px) { .memory-feature { padding: 72px 0; } }
.memory-feature .inner {
  position: relative;
  max-width: 620px;
  padding-left: 64px;
}
@media (max-width: 600px) { .memory-feature .inner { padding-left: 8px; } }
.memory-feature .quote-mark {
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(110px, 16vw, 200px);
  line-height: 1;
  color: var(--accent);
  opacity: 0.16;
  position: absolute;
  top: -64px;
  left: -24px;
  pointer-events: none;
  user-select: none;
}
@media (max-width: 600px) { .memory-feature .quote-mark { display: none; } }
.memory-feature h2 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(28px, 3.8vw, 42px);
  line-height: 1.12;
  max-width: 13ch;
  margin: 0 0 20px;
  letter-spacing: -0.015em;
}
.memory-feature p {
  color: var(--text-lo);
  font-size: 17px;
  line-height: 1.65;
  max-width: 46ch;
  margin: 0;
}

/* ---- Social proof band ---- */
.proof-band {
  text-align: center;
  max-width: 640px;
  margin: 0 auto;
  padding: 8px 0;
}
.proof-stars { color: var(--accent); font-size: 18px; letter-spacing: 6px; margin-bottom: 18px; }
.proof-band blockquote {
  margin: 0 0 14px;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(19px, 2.4vw, 24px);
  line-height: 1.45;
  color: var(--text-hi);
}
.proof-band cite {
  color: var(--text-faint);
  font-style: normal;
  font-size: var(--t-14);
  line-height: var(--lh-14);
}
.proof-pending {
  color: var(--text-faint);
  font-size: var(--t-14);
  line-height: var(--lh-14);
  font-style: italic;
}

/* ---- Cast / More Worlds gallery ---- */
#cast {
  max-width: 1440px;
  overflow: clip;
  --cast-card-width: clamp(224px, 22vw, 320px);
  --cast-x-outer: clamp(188px, 24vw, 350px);
  --cast-x-inner: clamp(128px, 13vw, 190px);
  --cast-y-outer: clamp(58px, 6vw, 90px);
  --cast-y-inner: clamp(24px, 2.6vw, 40px);
  --cast-caption-muted: rgba(255, 255, 255, 0.72);
  view-timeline-name: --cast-fan-timeline;
  view-timeline-axis: block;
}
#cast .section-head {
  /* 1080, not 920: "More than one relationship." sets to 941px at the 72px
     display size, so 920 clipped it by 21px and forced a second line. */
  max-width: min(1080px, 100%);
  margin: 0 auto;
  text-align: center;
}
#cast .section-head .eyebrow {
  margin-bottom: 0;
}
.cast-icon-reel {
  position: relative;
  width: 112px;
  height: 58px;
  margin: 8px auto 12px;
  transform: translateY(20%);
  overflow: hidden;
}
.cast-icon-reel img {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 112px;
  height: 112px;
  max-width: none;
  opacity: 0;
  object-fit: contain;
  transform: translate(-50%, calc(-50% + 8px)) rotate(-4deg) scale(0.94);
  animation: cast-icon-cycle 12.5s ease-in-out infinite both;
  animation-delay: calc(var(--icon-index) * 2.5s);
}
@keyframes cast-icon-cycle {
  0% {
    opacity: 0;
    transform: translate(-50%, calc(-50% + 8px)) rotate(-4deg) scale(0.94);
  }
  3%, 16% {
    opacity: 1;
    transform: translate(-50%, -50%) rotate(0) scale(1);
  }
  20%, 100% {
    opacity: 0;
    transform: translate(-50%, calc(-50% - 8px)) rotate(3deg) scale(0.98);
  }
}
/* .section-head caps h2 and p at a 640px reading measure, which is right for
   body copy and far too narrow for a 72px display line — it was the real reason
   this headline broke, not the container above. Display sizes take the
   container's width. */
#cast .section-head h2 {
  max-width: none;
}
/* step display. This headline is twice the size of the .section-head h2 it
   inherits from, so it takes the display row, not the h2 row — inheriting
   h2's -0.02em left a 72px line tracked for a 36px one. */
#cast .section-head h2 {
  margin-bottom: 0;
  font-size: clamp(40px, 5.5vw, 72px);
  font-weight: var(--fw-display);
  font-variation-settings: "opsz" var(--opsz-display), "wght" var(--fw-display);
  letter-spacing: var(--tr-display);
  line-height: var(--lh-display);
  /* pretty, not balance: balance evens out line lengths across the whole block
     and will happily break mid-sentence to do it. pretty only guards the last
     line against an orphan, which is all this needs once the sentences are
     separated below. */
  text-wrap: pretty;
}
#cast .section-head h2 span {
  display: block;
}
.cast-fan {
  position: relative;
  height: clamp(490px, 43vw, 610px);
  margin-top: 15.6px;
  isolation: isolate;
}
.cast-card {
  --fan-x: 0px;
  --fan-y: 0px;
  --fan-rotate: 0deg;
  --fan-z: 5;
  margin: 0;
  position: absolute;
  z-index: var(--fan-z);
  top: 0;
  left: 50%;
  width: var(--cast-card-width);
  transform:
    translateX(calc(-50% + var(--fan-x)))
    translateY(var(--fan-y))
    rotate(var(--fan-rotate))
    scale(var(--fan-scale, 1));
  /* The reference fan rotates every card around its own centre. The old 130%
     origin pulled the centre card upward and dropped the outer cards into an
     exaggerated U, so the layout looked broken even after the animation ended. */
  transform-origin: 50% 50%;
  transition: transform 1.35s cubic-bezier(.16, 1, .3, 1);
}
.cast-card--far-left {
  --fan-x: calc(-1 * var(--cast-x-outer));
  --fan-y: var(--cast-y-outer);
  --fan-rotate: -14deg;
  --fan-scale: 0.82;
  --fan-z: 1;
  --fan-delay: 140ms;
}
.cast-card--left {
  --fan-x: calc(-1 * var(--cast-x-inner));
  --fan-y: var(--cast-y-inner);
  --fan-rotate: -6deg;
  --fan-scale: 0.92;
  --fan-z: 2;
  --fan-delay: 60ms;
}
.cast-card--right {
  --fan-x: var(--cast-x-inner);
  --fan-y: var(--cast-y-inner);
  --fan-rotate: 6deg;
  --fan-scale: 0.92;
  --fan-z: 2;
  --fan-delay: 60ms;
}
.cast-card--far-right {
  --fan-x: var(--cast-x-outer);
  --fan-y: var(--cast-y-outer);
  --fan-rotate: 14deg;
  --fan-scale: 0.82;
  --fan-z: 1;
  --fan-delay: 140ms;
}
/* Solid, and its own edge. Modelled on the Lando Norris socials fan the owner
   supplied: every card there is an edge-to-edge photograph, so the card reads as
   one object and needs no border and no shadow to say where it stops.
   That exact move is unavailable here — the character webp files are alpha
   cutouts (43-71% opaque, figure on transparency), so there is no photograph to
   bleed. A deep fill is the equivalent: the card becomes a solid panel with the
   figure standing on it instead of a near-white void with a small figure in the
   middle, and the fill states the boundary the hairline used to state.
   Taller too — 5/8 rather than 3/4 — because the art is 0.46-0.71 aspect and a
   squat card left it swimming. */
.cast-card .frame {
  position: relative;
  border-radius: var(--radius-lg);
  overflow: hidden;
  aspect-ratio: 5 / 8;
  border: 0;
  background:
    radial-gradient(120% 80% at 50% 8%, #2b2438 0%, transparent 62%),
    linear-gradient(180deg, #1a1620 0%, #0c0a11 100%);
  transition:
    transform var(--duration-fast) var(--ease-dive),
    opacity var(--duration-fast) ease,
    border-color var(--duration-fast) ease;
}
/* The lead stays full-size while the surrounding cards step down. Its warmer
   ground and top stacking order provide emphasis without pushing it into the
   headline above. */
.cast-card--lead .frame {
  background:
    radial-gradient(120% 80% at 50% 6%, #4a2f7d 0%, transparent 64%),
    linear-gradient(180deg, #241a38 0%, #0e0a16 100%);
}
.cast-card--lead {
  --fan-scale: 1;
}
.cast-card .frame::after {
  content: "";
  position: absolute;
  z-index: 1;
  inset: 0;
  background: linear-gradient(180deg, transparent 62%, #07060a 100%);
  opacity: 0.9;
  pointer-events: none;
}
.cast-card .frame img {
  width: 100%;
  height: 100%;
  /* No padding, bottom-anchored: the subject fills the frame the way it does in
     the reference rather than floating inside a margin. */
  padding: 0;
  object-fit: contain;
  object-position: center bottom;
  display: block;
}
/* Every card is named. These were opacity 0 with the name revealed on hover,
   which meant that on any touch screen four of the five people in "more than
   one relationship" were permanently anonymous — the section argues for a cast
   and then credits one of them. The .frame::after gradient already darkens the
   bottom 42% to --ink, so white type there is legible on all five.
   Hover still does something: the card lifts and the others dim. */
.cast-card figcaption {
  position: absolute;
  z-index: 2;
  left: 24px;
  right: 24px;
  bottom: 24px;
  margin: 0;
  text-align: left;
  font-size: var(--t-16);
  line-height: var(--lh-flush);
  color: var(--accent-ink);
  font-weight: 600;
  opacity: 1;
  transform: none;
  transition:
    opacity var(--duration-fast) ease,
    transform var(--duration-fast) var(--ease-dive);
}
.cast-card figcaption small {
  display: block;
  color: var(--cast-caption-muted);
  font-family: var(--font-mono);
  font-size: var(--t-11);
  line-height: var(--lh-11);
  font-weight: 600;
  margin-top: 6px;
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
}
/* The lead's opacity/transform override is gone with the base now visible; it
   is distinguished by its accent border and tinted fill instead. */

/* The fan overlaps left-to-right with the lead on top, so each card's LEFT edge
   is the one its neighbour covers on the right half of the fan. Left-aligned
   captions there were being eaten — "Guitarist" rendered as "rist" and the
   bassist's showed a single letter. The two right-hand cards caption from their
   own right edge, which is the edge that stays exposed. */
.cast-card--right figcaption,
.cast-card--far-right figcaption {
  text-align: right;
}
@media (hover: hover) and (pointer: fine) {
  .cast-card:hover {
    z-index: 10;
  }
  .cast-card:hover .frame {
    border-color: var(--accent);
    transform: translateY(-24px) scale(1.025);
  }
  .cast-card:hover figcaption {
    opacity: 1;
    transform: none;
  }
  .cast-fan:has(.cast-card:hover) .cast-card:not(:hover) .frame {
    opacity: 0.64;
  }
  .cast-fan:has(.cast-card:hover) .cast-card--lead:not(:hover) figcaption {
    opacity: 0;
    transform: translateY(8px);
  }
}

.cast-fan.is-staged .cast-card {
  transform:
    translateX(-50%)
    translateY(96px)
    rotate(0deg)
    scale(1);
  transition-duration: 480ms;
  transition-timing-function: cubic-bezier(.22, 1, .36, 1);
  transition-delay: 0ms;
}
.cast-fan.is-staged.is-arrived .cast-card {
  transform:
    translateX(-50%)
    translateY(0)
    rotate(0deg)
    scale(1);
}
.cast-fan.is-staged.is-open .cast-card {
  transform:
    translateX(calc(-50% + var(--fan-x)))
    translateY(var(--fan-y))
    rotate(var(--fan-rotate))
    scale(var(--fan-scale, 1));
  transition-duration: 1.35s;
  transition-timing-function: cubic-bezier(.16, 1, .3, 1);
  transition-delay: var(--fan-delay, 0ms);
}
@media (max-width: 900px) {
  #cast {
    /* This band had the widest cards AND the full +/-16deg rotation, with the
       rotation only easing off below 600 — the two end cards swung 114px past
       the section edge at 900. Narrower cards, a tighter radius, and the
       rotation stepped down here rather than in one jump at 600. */
    /* The floors have to sit below what 601px produces, or they stop the fan
       shrinking exactly where it needs to most: a 180px floor on the radius
       meant 25vw was overridden all the way up to 720px wide. */
    --cast-card-width: clamp(150px, 27vw, 250px);
    --cast-x-outer: clamp(108px, 22vw, 230px);
    --cast-x-inner: clamp(58px, 12vw, 120px);
    --cast-y-outer: clamp(44px, 6vw, 68px);
    --cast-y-inner: clamp(18px, 2.8vw, 30px);
  }
  .cast-card--far-left { --fan-rotate: -11deg; }
  .cast-card--far-right { --fan-rotate: 11deg; }
  .cast-card--left { --fan-rotate: -5.5deg; }
  .cast-card--right { --fan-rotate: 5.5deg; }
  .cast-fan {
    height: clamp(460px, 68vw, 580px);
  }
}
@media (max-width: 600px) {
  #cast {
    /* Tighter spread and a narrower card below 600px. With the taller 5/8 card
       and the lead scaled up, the previous spread swung the outer two cards past
       the viewport on a 390px screen and #cast's overflow:clip cut them through
       the middle of a figure — a crop that reads as broken rather than as bleed. */
    --cast-card-width: clamp(112px, 33vw, 138px);
    --cast-x-outer: clamp(62px, 19vw, 78px);
    --cast-x-inner: clamp(31px, 9.5vw, 39px);
    --cast-y-outer: 32px;
    --cast-y-inner: 12px;
  }
  #cast .section-head h2 {
    font-size: clamp(36px, 11vw, 48px);
  }
  .cast-icon-reel {
    width: 88px;
    height: 52px;
    margin: 6px auto 9px;
  }
  .cast-icon-reel img {
    width: 92px;
    height: 92px;
  }
  .cast-fan {
    height: 290px;
    margin-top: 11.7px;
  }
  .cast-card--lead {
    --fan-y: -8px;
  }
  .cast-card--far-left {
    --fan-rotate: -8deg;
  }
  .cast-card--left {
    --fan-rotate: -4.5deg;
  }
  .cast-card--right {
    --fan-rotate: 4.5deg;
  }
  .cast-card--far-right {
    --fan-rotate: 8deg;
  }
  /* 10px inset, not 16. The card is 138px wide here and the lead is scaled 1.1,
     so its LAYOUT width stays 138 while it draws at 152 — "STRONGEST BOND" needs
     107px and a 16px inset left only 104.5 of layout measure, wrapping it by
     three pixels. 10px gives 118 and keeps the label on one line without going
     under 11px type. */
  .cast-card figcaption {
    left: 10px;
    right: 10px;
    bottom: 14px;
  }
  /* The ONE place on the site that deliberately sits below --label-tracking,
     and the arithmetic is why: 0.13em pushes "STRONGEST BOND" past the 118px
     of measure the inset above buys back. 0.03em fit, but 11px uppercase at
     0.03em is under the floor where caps stay legible — the counters close up
     and the word turns into a grey bar. 0.08em is the lowest tracking that
     still reads as tracked: ~115px set against 118px available. It fits by
     three pixels, same as the inset above it. If the label ever grows a
     character, shorten the label — do not take the tracking back out. */
  .cast-card figcaption small {
    letter-spacing: 0.08em;
  }
  /* Lead only, below 600px. The fan compresses to ~150px cards with heavy
     overlap here, so five captions land on top of each other — "Drumm…",
     "Bandm…", "…rist" and three stacked CHOOSE LIFEs. Naming everyone is right
     on desktop and actively worse here.
     This is a patch over a layout problem, not a fix for it: at this width the
     fan itself only really shows three of the five people. The names want to
     leave the cards entirely on mobile — a row under the fan — which is a
     markup change, not a caption tweak. */
  .cast-card:not(.cast-card--lead) figcaption {
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .cast-icon-reel img {
    display: none;
    animation: none;
  }
  .cast-icon-reel img:first-child {
    display: block;
    opacity: 1;
    transform: translate(-50%, -50%);
  }
  .cast-card {
    animation: none;
    transition: none;
  }
  .cast-card .frame,
  .cast-card figcaption {
    transition: none;
  }
}

/* ---- Steps ---- */
.steps {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 20px;
  counter-reset: step;
}
@media (max-width: 860px) { .steps { grid-template-columns: 1fr 1fr; } }
.step { position: relative; padding-top: 12px; }
.step::before {
  counter-increment: step;
  content: counter(step);
  font-family: var(--font-mono);
  display: inline-flex;
  align-items: center; justify-content: center;
  width: 32px; height: 32px;
  border-radius: 50%;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--accent);
  font-weight: 600;
  margin-bottom: 14px;
}
/* font-weight stated, not inherited: an <h4> takes the UA's `bold` = 700, and
   Public Sans is loaded at 400/500/600 only — so every one of these headings
   was being SYNTHESISED bold by the browser (a smeared copy of the 600, offset
   and re-drawn). Same fix on .faq-item h4. */
.step h4 {
  margin: 0 0 6px;
  font-size: var(--t-15);        /* was 15.5 */
  line-height: var(--lh-15);
  font-weight: 600;
}
.step p {
  color: var(--text-faint);
  font-size: var(--t-13);        /* was 13.5 */
  line-height: var(--lh-13);
  margin: 0;
  text-wrap: pretty;
}

/* ---- Download CTA band ---- */
.cta-band {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  padding: 56px 40px;
  text-align: center;
}
/* step h2. This was the barest headline on the site: 34px of display serif
   with no tracking and no leading declared anywhere, falling through to the
   UA's `normal` — and it is the closing headline on all three pages. */
.cta-band h2 {
  font-family: var(--font-display);
  font-weight: var(--fw-h2);
  font-variation-settings: "opsz" var(--opsz-h2), "wght" var(--fw-h2);
  font-size: clamp(24px, 3.5vw, 34px);
  margin: 0 0 12px;
  letter-spacing: var(--tr-h2);
  line-height: var(--lh-h2);
  text-wrap: balance;
}
.cta-band p { color: var(--text-lo); margin: 0 0 28px; }

/* index's pre-launch variant: a dated season promise instead of a download.
   Slightly tighter than the shared band — with no button to anchor it, the
   full 56px padding read as an empty card. */
.cta-band--teaser { padding: 44px 32px 46px; }
.cta-band--teaser .cta-coming {
  margin: 0 0 14px;
  color: var(--text-hi);
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
}

/* ---- Footer ---- */
footer {
  border-top: 1px solid var(--border);
  padding: 32px 0;
  color: var(--text-faint);
  font-size: var(--t-13);        /* was 13.5 */
  line-height: var(--lh-13);
}
footer .wrap {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
footer .foot-links {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 16px;
}
footer a:hover { color: var(--text-hi); }
.disclaimer {
  color: var(--text-faint);
  font-size: var(--t-12);        /* was 12.5 */
  line-height: var(--lh-12);     /* was 1.6 — 20px of leading on 12.5px type */
  max-width: 68ch;               /* was 720px; a measure belongs in characters */
  text-wrap: pretty;
}

/* ---- Pricing ---- */
.pricing-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
  align-items: stretch;
}
@media (max-width: 860px) { .pricing-grid { grid-template-columns: 1fr; } }
.plan {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 32px 28px;
  display: flex;
  flex-direction: column;
}
.plan.featured {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 32%, transparent),
              0 20px 60px color-mix(in srgb, var(--accent) 10%, transparent);
  position: relative;
}
/* A badge is type inside a box: line-height flush, padding does the height.
   700 -> 600 because 700 does not exist in the loaded Public Sans and was being
   synthesised — on 11.5px caps that smear is most of what the badge looked like. */
.plan.featured .badge {
  position: absolute; top: -13px; left: 28px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--t-12);        /* was 11.5 */
  line-height: var(--lh-flush);
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 6px 12px; border-radius: 999px;
}
.plan .name {
  font-family: var(--font-mono);
  font-size: var(--t-13);        /* was 14 — it is a label, not a heading */
  line-height: var(--lh-13);
  font-weight: 600;
  color: var(--text-lo);
  text-transform: uppercase; letter-spacing: var(--label-tracking);
}
/* Mono, so the Fraunces display steps do not apply — a price is a number.
   -0.02em was declared in a second .plan .price block far below (with the note
   about "$9.99" setting as "$9 . 99"); folded in here so the price has one
   declaration. Mono fits loose by design and a 34px setting has to take some of
   that back.

   tabular-nums REMOVED as dead weight, NOT as a fix: IBM Plex Mono is
   monospaced, so every glyph — figures, the period, the dollar sign — is
   already on one advance and the property has nothing left to do. Verified by
   render: pricing.html before and after the removal hashes identically, not one
   pixel. (It stays on .compare-table td and the three product-demo counters,
   where the face is mono but the figures really do form a vertical column.)

   Which also means the airy "$9 . 99" is not a bug to close with tracking — it
   is what a monospaced face does to a period, and -0.02em cannot reach it. The
   only ways to close it are to set the price in Fraunces or Public Sans, or to
   accept it as the mono look. Left as mono, deliberately: the price sits in the
   same register as .plan .name above it. */
.plan .price {
  font-family: var(--font-mono);
  font-size: 34px;
  font-weight: 600;
  letter-spacing: -0.02em;
  margin: 10px 0 4px;
}
/* the /month suffix returns to normal tracking — it is words, not figures */
.plan .price span {
  font-size: var(--t-14);
  color: var(--text-faint);
  font-weight: 400;
  letter-spacing: 0;
}
.plan .tagline {
  color: var(--text-lo);
  font-size: var(--t-14);
  line-height: var(--lh-14);
  margin-bottom: 20px;
  text-wrap: pretty;
}
.plan ul { list-style: none; padding: 0; margin: 0 0 28px; flex: 1; }
.plan li {
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
  font-size: var(--t-14);
  color: var(--text-lo);
  display: flex;
  gap: 10px;
  line-height: var(--lh-14);
}
/* The mark is defined once, in the polish pass at the foot of this file — a
   masked check rather than U+25C6 BLACK DIAMOND. Leaving the old glyph here to
   be silently overridden 2000 lines later is how a stylesheet stops being
   readable, so it goes. */
.plan .btn-secondary, .plan .appstore-badge { justify-content: center; }

.compare-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 56px;
  font-size: var(--t-14);
  line-height: var(--lh-14);
}
.compare-table-wrap { overflow-x: auto; }
.compare-table th, .compare-table td {
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
  text-align: left;
}
.compare-table th {
  color: var(--text-lo); font-weight: 600;
  font-size: var(--t-12);        /* was 12.5 */
  line-height: var(--lh-12);
  text-transform: uppercase; letter-spacing: var(--label-tracking);
  font-family: var(--font-mono);
}
.compare-table td:not(:first-child), .compare-table th:not(:first-child) {
  text-align: center; color: var(--text-lo); font-variant-numeric: tabular-nums;
}

/* ---- Docs ---- */
.docs-layout {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: 48px;
}
@media (max-width: 860px) { .docs-layout { grid-template-columns: 1fr; } }
.docs-nav { position: sticky; top: 88px; align-self: start; }
.docs-nav a {
  display: block;
  padding: 8px 0;
  color: var(--text-faint);
  font-size: var(--t-14);
  line-height: var(--lh-14);
  border-left: 2px solid transparent;
  padding-left: 12px;
  transition: color var(--duration-fast) ease, border-color var(--duration-fast) ease;
}
.docs-nav a:hover, .docs-nav a.active { color: var(--text-hi); border-color: var(--accent); }

/* step h3 — 22px is the bottom of that step's range. Tracking was declared in a
   second .docs-content h2 block further down (-0.01em); it lives here now with
   the rest of the step. */
.docs-content h2 {
  font-family: var(--font-display);
  font-weight: var(--fw-h3);
  font-variation-settings: "opsz" var(--opsz-h3), "wght" var(--fw-h3);
  font-size: 22px;
  letter-spacing: var(--tr-h3);
  line-height: var(--lh-h3);
  margin: 48px 0 16px;
}
.docs-content h2:first-child { margin-top: 0; }
.docs-content p { color: var(--text-lo); }
.docs-content ol, .docs-content ul { color: var(--text-lo); padding-left: 20px; }
.docs-content li { margin-bottom: 8px; }
.docs-content code {
  font-family: var(--font-mono);
  background: var(--surface-2);
  padding: 2px 7px;
  border-radius: 6px;
  font-size: var(--t-13);
}
.faq-group { margin-bottom: 40px; }
/* 16px was a label set at body size, which is why it competed with the questions
   under it. Same micro-label recipe as every other mono cap on the site. */
.faq-group > h3 {
  font-size: var(--t-12);
  line-height: var(--lh-12);
  color: var(--text-lo);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  margin: 0 0 12px;
  font-family: var(--font-mono);
  font-weight: 600;
}
.faq-item {
  border-bottom: 1px solid var(--border);
  padding: 18px 0;
}
.faq-item h4 {
  margin: 0 0 8px;
  font-size: var(--t-15);        /* was 15.5 */
  line-height: var(--lh-15);
  font-weight: 600;              /* see .step h4 — UA bold was being synthesised */
}
.faq-item p {
  margin: 0;
  font-size: var(--t-14);
  line-height: var(--lh-14);
  text-wrap: pretty;
}
.safety-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--accent);
  font-size: var(--t-14);        /* was 14.5 */
  line-height: var(--lh-14);
  font-weight: 500;
  margin-top: 8px;
}

/* ==========================================================================
   THE WALKTHROUGH  (.wt)
   Spec: docs/superpowers/specs/2026-07-25-scroll-walkthrough-design.md
   A six-screen scroll narrative in which the visitor makes one real choice and
   is quoted back to themselves four screens later. Sticky panels + CSS only for
   everything load-bearing; the JS file adds entrance cascades and nothing else.
   ========================================================================== */

.wt {
  /* ---- type: two bands with a hard dead zone between 17px and 40px --------
     Band A never grows (0vw coefficient). Band B grows violently. The empty
     middle IS the scale contrast; it is doing the work that deleted copy used
     to do. Ratio runs 4.3:1 on a phone to 12.8:1 on a desktop by itself.
     Band A's steps now come from :root — this block redefined the same five
     tokens the site layer could not see, and its --t-10 was below the 11px
     floor. Only the two display sizes are local, because only they are. */
  --t-display: clamp(2.5rem, 1.15rem + 6.4vw, 6.5rem);   /*  40 -> 104px */
  --t-peak: clamp(3rem, 1.30rem + 8.0vw, 8.5rem);        /*  48 -> 136px */

  /* ---- the app's own font. Resolves to real SF Pro on iPhone/Mac, which is
     what makes rebuilt iOS UI stop looking like web cards. Deliberately a
     different family from the editorial layer -- same reasoning as the app
     radii below not reusing --radius-*. */
  --font-ios: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui,
              "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

  /* ---- app radii: their own scale. Must NOT reuse --radius-*; the site's
     1.333 geometric web series is a large part of why rebuilt app UI reads as
     a web card. The asymmetric single-corner tightening is the iMessage tell. */
  --ios-bubble: 18px;
  --ios-tail: 5px;
  --ios-card: 16px;
  --ios-choice: 13px;
  --ios-col-x: 30px;
  --ios-col-y: 38px;

  /* ---- motion: three easings for the whole page, plus linear for the two
     diegetic playback fills. Duration ladder has a dead zone too: nothing
     between 700ms and 1400ms, so responses are either crisp or cinematic. */
  --ease-press: cubic-bezier(0.4, 0, 1, 1);
  --ease-surface: cubic-bezier(0.16, 1, 0.30, 1);

  --shadow-col: 0 0 0 1px rgba(245, 245, 247, 0.055),
                0 2px 14px -4px rgba(0, 0, 0, 0.7),
                0 24px 70px -18px rgba(0, 0, 0, 0.85),
                inset 0 1px 0 rgba(245, 245, 247, 0.06);
  /* Six layers, each offset LEFTWARD so one light source is implied across the
     whole page. The final inset hairline catching light on the top edge is what
     makes a flat div read as a physical card. */
  --shadow-ios: 0 0 0 1px rgba(245, 245, 247, 0.05),
                -1px 1px 2px rgba(0, 0, 0, 0.5),
                -3px 3px 6px rgba(0, 0, 0, 0.45),
                -6px 6px 14px -3px rgba(0, 0, 0, 0.5),
                -12px 12px 28px -6px rgba(0, 0, 0, 0.55),
                -24px 24px 56px -12px rgba(0, 0, 0, 0.6),
                inset 0 1px 0 rgba(245, 245, 247, 0.06);

  --gate: 40svh;
  position: relative;
  overflow: visible;   /* no ancestor may clip, or sticky dies silently */
  padding: 0;          /* the global `section{padding:64px 0}` would otherwise
                          add 128px of dead space inside the walkthrough */
}

/* One implied light source, upper left. Never centred: a centred radial
   darkening with a soft glow behind a column IS the banned generic-dark-AI look. */
.wt::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 1;
  background: radial-gradient(120% 90% at 32% 22%,
              transparent 0, transparent 44%, rgba(10, 10, 13, 0.5) 100%);
}

/* ---- descent arc -------------------------------------------------------
   Stepped, not swept: a continuous gradient across 13 viewports of a 5% luma
   range bands on 8-bit panels. Each beat is flat; each seam bridges. 04 breaks
   upward into warm paper -- being remembered is the surfacing moment, and the
   dive metaphor is what earns it. 05 is the floor (1:12am). The close returns
   to the hero's exact value so the loop closes in colour as well as in words. */
.wt-beat { position: relative; padding: 0; scroll-margin-top: 0; }
/* All sections are the app's own light ground now. Darkness is a property of the
   depicted SCREEN, not of the page, so it lives on .wt-col via .on-ink. The old
   13-viewport "descent arc" is gone with it: value is diegetic here, not
   decorative, which is a stronger reason for it than the arc ever had. */
.wt-01 { height: 190svh; background: var(--paper); }
.wt-02 { height: 200svh; background: var(--paper); }
.wt-03 { height: 180svh; background: var(--paper); }
.wt-04 { height: 240svh; background: var(--paper); }
.wt-05 { height: 180svh; background: var(--paper); }

.wt-seam { position: relative; }
.wt-seam--a { height: 16svh; background: var(--paper); }
.wt-seam--b { height: 24svh; background: var(--paper); }
/* seam C is the longest emptiness on the page and it carries the slow surfacing
   gradient. 80svh of rising light. The stamp is pinned to the top third, where
   the ground is still dark -- centred, it would land mid-gradient with
   unpredictable contrast. */
.wt-seam--c {
  height: 80svh;
  background: var(--paper);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 9svh;
}
/* seam D is half seam C's height: slow surface, fast plunge. */
.wt-seam--d {
  height: 40svh;
  background: var(--paper);
  display: grid;
  place-items: center;
}
.wt-gate {
  /* LOAD-BEARING GEOMETRY -- DO NOT EDIT WITHOUT READING THIS.
     .wt-gate must be the IMMEDIATE next sibling of #wt-02, #wt-02 must be
     exactly 200svh tall, its .wt-panel exactly 100svh, and there must be NO
     margin between them. Then the gate's top edge sits at sectionTop+200svh
     while the pin releases at sectionTop+100svh -- i.e. the gate is at least one
     full viewport BELOW the fold for the entire pin, so collapsing it changes
     only off-screen document. Break any of the three and Safari (which has zero
     scroll-anchoring support, on the platform that matters most) will visibly
     jerk on iPhone. */
  height: var(--gate);
  background: #0a090c;
}
@media (prefers-reduced-motion: no-preference) {
  .wt-gate { transition: height 620ms var(--ease-dive); }
}

.wt-stamp {
  font-family: var(--font-mono);
  font-size: var(--t-11);
  line-height: var(--lh-11);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;     /* the widest tracking on the page */
  color: var(--text-faint);
}

/* ---- panel + desktop frame --------------------------------------------- */
.wt-panel {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;   /* svh, never dvh: dvh resizes as the iOS toolbar collapses
                       and would fight the pin. Never -webkit-fill-available
                       either: buggy inside flex/grid on iOS. */
  overflow: clip;
  background: transparent;   /* the section behind carries the colour; this also
                                eliminates all overdraw */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 2;
  --col: min(400px, calc(78svh * 0.4613));
}
.wt-01 .wt-panel, .wt-05 .wt-panel { --col: min(560px, calc(84svh * 0.4613)); }

.wt-flank { display: none; }

@media (min-width: 1100px) {
  .wt-panel {
    display: grid;
    grid-template-columns: 1fr var(--col) 1fr;
    align-items: center;
    column-gap: clamp(24px, 4vw, 72px);
  }
  /* Flanks are never symmetrical and never both empty -- two matched empty
     flanks around a centred column is exactly what reads as a broken
     responsive layout. Beats 01 and 05 have no display line and no crop; they
     solve it by going wide instead (560px vs 400px). */
  .wt-flank { display: flex; align-items: center; }
  .wt-flank--l { justify-content: flex-end; }
  .wt-flank--r { justify-content: flex-start; }
}

/* ---- the app column ---------------------------------------------------- */
.wt-col {
  position: relative;
  width: var(--col);
  max-width: 100%;
  aspect-ratio: 393 / 852;    /* iPhone logical points */
  border-radius: var(--ios-col-x) / var(--ios-col-y);
  background: var(--surface);
  overflow: hidden;
  box-shadow: var(--shadow-col);
  font-family: var(--font-ios);
  display: flex;
  flex-direction: column;
  justify-self: center;
}

/* ---- the story frame (beats 01, 02) ----------------------------------- */
.wt-stage { position: relative; flex: 1; min-height: 0; overflow: hidden; }
.wt-shot {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  filter: brightness(var(--fig-lum, 0.68)) contrast(var(--fig-con, 1.08));
}
.wt-shot--a { object-position: 50% 22%; }
.wt-shot--b { object-position: 54% 34%; opacity: 0; }
/* Two crops of one static render, cross-dissolving. This is the answer to "it
   must read as video with no licensable footage" -- the two rejected clips were
   YSL runway footage and a third-party-watermarked reel. Honest caveat: a slow
   dissolve between two crops reads as a cinematic CUT, not as motion within a
   shot. Combined with a burned-in subtitle and a filling bar it should land;
   this is the first thing to eyeball on a real build. */
@keyframes wt-shot { 0%, 44% { opacity: 0; } 56%, 100% { opacity: 1; } }
.wt-01 .wt-shot--b { animation: wt-shot 9s var(--ease-dive) infinite alternate; }
.wt-stage::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(10, 10, 13, 0.55) 0%,
              transparent 26%, transparent 52%, rgba(10, 10, 13, 0.92) 100%);
}

/* bonds row -- home-feed furniture. Moved here from beat 05: at beat 01 it says
   "a roster exists" in two words. Never enters, never hover-lifts. */
.wt-bonds {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 3;
  padding: 14px 16px 0;
}
.wt-bonds-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
}
.wt-bonds .lbl {
  font-family: var(--font-body);
  font-size: var(--t-11);
  line-height: var(--lh-11);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  color: var(--text-faint);
}
.wt-bonds .hint {
  font-family: var(--font-body);
  font-size: var(--t-11);        /* was --t-10, below the 11px floor */
  line-height: var(--lh-11);
  color: var(--text-faint);
  opacity: 0.72;
}
.wt-bonds ul { display: flex; gap: 8px; list-style: none; margin: 8px 0 0; padding: 0; }
.wt-bonds li {
  width: 44px; height: 44px;
}
.wt-bonds button {
  appearance: none;
  width: 44px; height: 44px;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: 50%;
  overflow: hidden;
  background: var(--surface-2);
  opacity: 0.34;
  cursor: pointer;
  transition:
    opacity 180ms ease,
    background 180ms ease,
    scale 100ms ease-out;
}
.wt-bonds button[aria-pressed="true"] {
  border-color: transparent;
  background: var(--acc-grad);
  opacity: 1;
  box-shadow: 0 0 18px rgba(148, 110, 254, 0.28);
}
.wt-bonds button:active { scale: 0.97; }
.wt-bonds button:focus-visible {
  outline: 2px solid var(--acc-violet);
  outline-offset: 3px;
}
@media (hover: hover) and (pointer: fine) {
  .wt-bonds button:hover { opacity: 0.72; }
  .wt-bonds button[aria-pressed="true"]:hover { opacity: 1; }
}
.wt-bonds img {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
  object-position: 50% 12%;
}

.wt-slug {
  position: absolute;
  left: 16px; bottom: 96px;
  z-index: 3;
}
.wt-slug .world {
  font-family: var(--font-display);
  font-size: var(--t-17);
  font-variation-settings: "opsz" 24, "wght" 500;   /* the opposite end of the
     optical axis from the display lines. Fraunces appears inside the app column
     in exactly this one role and nowhere else. */
  color: var(--text-hi);
  display: block;
}
.wt-slug .series {
  display: block;
  margin-bottom: 4px;
  font-family: var(--font-mono);
  font-size: var(--t-11);
  line-height: var(--lh-11);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
}
.wt-slug .ep {
  font-family: var(--font-mono);
  font-size: var(--t-11);
  line-height: var(--lh-11);
  letter-spacing: 0.1em;
  color: var(--text-faint);
}
.wt-slug .cast-credit {
  display: block;
  margin-top: 4px;
  font-family: var(--font-body);
  font-size: var(--t-11);
  line-height: var(--lh-11);
  color: var(--text-lo);
}

/* burned-in subtitle: a centred caption inside a vertical frame is the single
   most universally-read "this is video" signal that exists, and it costs one
   line of dialogue we already own. */
.wt-sub {
  position: absolute;
  left: 22px; right: 22px; bottom: 62px;
  z-index: 3;
  text-align: center;
  font-size: var(--t-15);
  line-height: var(--lh-15);
  color: #f5f5f7;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}

/* progress bar: ACHROMATIC IN EVERY STATE. The accent means "your agency";
   spending that meaning on system chrome to buy a 300ms flourish would forfeit
   the beat-04 payoff. */
.wt-bar {
  position: absolute;
  left: 16px; right: 16px; bottom: 44px;
  z-index: 3;
  height: 2px;
  background: rgba(245, 245, 247, 0.1);
  border-radius: 2px;
}
.wt-bar i { display: block; height: 100%; background: var(--text-lo); border-radius: 2px; }
@keyframes wt-play { from { width: 0; } to { width: 62%; } }
.wt-01 .wt-bar i { animation: wt-play 9s linear infinite; }

.wt-dive {
  position: absolute;
  left: 16px; right: 16px; bottom: 14px;
  z-index: 3;
  display: flex; align-items: center; justify-content: center;
  min-height: 44px;
  max-width: 300px;
  margin: 0 auto;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
  font-size: var(--t-15);
  line-height: var(--lh-flush);
}

/* the next World, deliberately cropped so its title is cut off. Three signals in
   one element: vertical feed / more than one World / more than one kind of
   character. */
.wt-next {
  height: 52px;
  flex: none;
  position: relative;
  overflow: hidden;
  border-top: 1px solid var(--border);
}
.wt-next img {
  width: 100%;
  object-fit: cover;
  object-position: 50% 8%;
  filter: brightness(0.5);
}

/* ---- beat 02: the halt ------------------------------------------------- */
.wt-02 .wt-col { justify-content: space-between; }
.wt-02 .wt-stage { flex: 1; }
.wt-dim {
  position: absolute;
  inset: 0;
  z-index: 4;
  background: radial-gradient(120% 80% at 50% 42%,
              rgba(10, 9, 12, 0.62) 0%, rgba(10, 9, 12, 0.9) 100%);
  backdrop-filter: blur(2px) saturate(1.4);
  -webkit-backdrop-filter: blur(2px) saturate(1.4);
  transition: opacity 620ms var(--ease-dive);
}
/* Apple's materials boost saturation behind blur; omitting saturate() is why web
   recreations look like grey frosted glass. This is the ONLY backdrop-filter on
   the entire page. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .wt-dim { background: rgba(10, 9, 12, 0.92); }
}
.wt-02 .wt-bar { bottom: auto; top: 12px; opacity: 0.5; }
.wt-02 .wt-bar i { width: 32%; }
.wt-clock {
  position: absolute;
  top: 22px; right: 16px;
  z-index: 5;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-11);
  line-height: var(--lh-11);
  letter-spacing: 0.08em;
  color: var(--text-faint);
}
.wt-ask {
  position: relative;
  z-index: 5;
  margin: auto 22px 0;
  font-size: var(--t-15);
  line-height: 1.4;
  color: var(--text-hi);
  text-align: center;
}
.wt-choices {
  position: relative;
  z-index: 5;
  border: 0;
  margin: 18px 0 0;
  padding: 0 16px max(24px, env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: 12px;
}
/* sr-only, NOT display:none -- display:none removes the input from the a11y tree
   and kills the label association. No <form> element either: that would make
   Enter submit, and radios group document-wide by name anyway. */
.wt-radio { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.wt-choice {
  display: flex;
  align-items: center;
  min-height: 56px;
  padding: 15px 18px;
  border-radius: var(--ios-choice);
  font: 500 var(--t-15)/1.35 var(--font-ios);
  /* Tinted outline, never a solid fill. Two solid orange buttons would be the
     largest chromatic area on the page and would inflate the accent past the
     point where beat 04 can land. */
  background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  color: var(--accent);
  cursor: pointer;
  transition: background-color 120ms linear, border-color 120ms linear,
              opacity 180ms linear;
}
@media (hover: hover) {
  .wt-choice:hover {
    background: color-mix(in srgb, var(--accent) 18%, transparent);
    border-color: color-mix(in srgb, var(--accent) 52%, transparent);
  }
}
/* Press accelerates INTO the surface and does not come back past 1. A spring on
   a decision the visitor just made would turn it into a game button. */
.wt-choice:active { transform: scale(0.985); transition: transform 90ms var(--ease-press); }
.wt-radio:focus-visible + .wt-choice { outline: 2px solid var(--accent); outline-offset: 3px; }

.wt-hint {
  position: relative;
  z-index: 5;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--t-11);
  line-height: var(--lh-11);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 10px 0 0;
  opacity: 0;
  transition: opacity 420ms var(--ease-dive);
}
.wt-02.is-live .wt-hint { opacity: 1; transition-delay: 4200ms; }

/* answered: the unchosen chip fades but STAYS IN THE DOM. The road not taken
   remaining faintly visible is what makes the beat-04 fork echo legible. */
html:has(#wt-c-a:checked) #wt-l-b,
html:has(#wt-c-b:checked) #wt-l-a,
html[data-choice="a"] #wt-l-b,
html[data-choice="b"] #wt-l-a {
  opacity: 0.2;
  border-color: transparent;
  background: transparent;
}
html:has(.wt-radio:checked) .wt-hint,
html[data-answered] .wt-hint { opacity: 0 !important; }
html:has(.wt-radio:checked) .wt-02 .wt-dim,
html[data-answered] .wt-02 .wt-dim { opacity: 0.42; }
@keyframes wt-resume { from { width: 32%; } to { width: 100%; } }
html:has(.wt-radio:checked) .wt-02 .wt-bar i,
html[data-answered] .wt-02 .wt-bar i {
  animation: wt-resume 1400ms linear 520ms forwards;
}
/* THE SOFT BLOCK. The resistance the visitor actually feels is the bar refusing
   to move -- beat 01 taught, wordlessly, that scrolling plays the story. This
   gate is only the secondary layer, and it is desktop / fine-pointer /
   motion-tolerant only. 40svh is under NN/g's "roughly half a screen" ceiling. */
html:has(.wt-radio:checked), html[data-answered] { --gate: 0svh; }
@media (pointer: coarse), (max-width: 700px), (prefers-reduced-motion: reduce) {
  .wt { --gate: 0svh; }
}

/* ---- variants: three branches, display-switched ------------------------
   The default is --none, NEVER variant A. A visitor who never clicked must not
   be shown a quote-back of a line she never chose: on the page whose entire
   claim is "it remembers exactly what you said", the default render would
   fabricate a memory. display:none (not opacity, not visibility, not inert --
   which is Chromium-only) removes hidden branches from tab order AND the a11y
   tree. */
.wt-var { display: none; }
.wt-var--none { display: block; }
html:has(#wt-c-a:checked) .wt-var--none, html[data-choice="a"] .wt-var--none,
html:has(#wt-c-b:checked) .wt-var--none, html[data-choice="b"] .wt-var--none { display: none; }
html:has(#wt-c-a:checked) .wt-var--a, html[data-choice="a"] .wt-var--a,
html:has(#wt-c-b:checked) .wt-var--b, html[data-choice="b"] .wt-var--b { display: block; }

/* ---- beat 03: chat ---------------------------------------------------- */
.wt-03 .wt-col { justify-content: flex-end; }
.wt-chathead {
  flex: none;
  position: relative;
  height: 40%;
  overflow: hidden;
  border-bottom: 1px solid var(--border);
}
.wt-chathead img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 56% 20%;
  filter: brightness(0.62) contrast(1.1);
}
.wt-chathead::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(180deg, transparent 40%, var(--surface) 100%);
}
.wt-who {
  position: absolute;
  left: 16px; bottom: 10px;
  font-size: var(--t-13);
  line-height: var(--lh-13);
  font-weight: 600;
  color: var(--text-hi);
}
.wt-thread {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 9px;
  padding: 14px 14px 0;
}
.wt-time {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-11);
  line-height: var(--lh-11);
  letter-spacing: 0.08em;
  color: var(--text-faint);
  text-align: center;
  padding-bottom: 2px;
}
.wt-msg {
  max-width: 82%;
  padding: 9px 13px;
  font-size: var(--t-15);
  line-height: 1.38;
}
/* The asymmetric single-corner tightening is the actual iMessage signature. */
.wt-msg--me {
  align-self: flex-end;
  /* NO ORANGE. The visitor's line must GO AWAY for beat 04 to be recall rather
     than repetition. */
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--ios-bubble) var(--ios-bubble) var(--ios-tail) var(--ios-bubble);
  color: var(--text-hi);
}
.wt-msg--him {
  align-self: flex-start;
  background: var(--surface-2);
  border-radius: var(--ios-bubble) var(--ios-bubble) var(--ios-bubble) var(--ios-tail);
  color: var(--text-hi);
}
/* Violet appears in exactly one place on the whole page, as a 1px outline with a
   ~6% gradient fill -- a tinted surface that never becomes a purple block. It
   carries no words: no "thinking...", no "typing...". That vocabulary is the
   generic-AI register the brand is avoiding. */
.wt-composing {
  align-self: flex-start;
  display: inline-flex;
  gap: 5px;
  padding: 8px 12px;
  border-radius: 999px;
  border: 1px solid rgba(139, 124, 255, 0.42);
  background: linear-gradient(rgba(139, 124, 255, 0.07), rgba(139, 124, 255, 0.02));
}
.wt-skel { display: flex; flex-direction: column; gap: 5px; width: 128px; }
.wt-skel span {
  height: 9px;
  border-radius: 4px;
  background: var(--surface-2);
  background-image: linear-gradient(90deg, transparent, rgba(139, 124, 255, 0.14) 45%, transparent 90%);
  background-size: 220% 100%;
  animation: wt-shimmer 1450ms linear infinite;
}
/* Uneven widths read as real text arriving. Replaces the three-dot ellipsis,
   which is a stock chat cliche, and is more literally accurate about what the
   machine is doing. */
.wt-skel span:nth-child(1) { width: 82%; }
.wt-skel span:nth-child(2) { width: 96%; animation-delay: 110ms; }
.wt-skel span:nth-child(3) { width: 54%; animation-delay: 220ms; }
@keyframes wt-shimmer { from { background-position: 120% 0; } to { background-position: -120% 0; } }

.wt-skills {
  flex: none;
  display: flex;
  gap: 7px;
  padding: 12px 14px max(14px, env(safe-area-inset-bottom));
  overflow: hidden;
}
.wt-skills span {
  flex: 1;
  min-width: 0;
  text-align: center;
  font-size: var(--t-11);
  line-height: var(--lh-flush);
  font-weight: 500;
  color: var(--text-lo);
  padding: 8px 6px;
  border: 1px solid var(--border);
  border-radius: 999px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- beat 04: the peak, on warm paper --------------------------------- */
.wt-04 {
  /* Scoped only to this beat; these never enter :root. The page inverts, the app
     column does NOT -- every other beat shows the app in dark mode, and a
     light-mode app here would be the same product with two faces. It also means
     no light-mode app UI has to be built at all. */
  /* These aliases used to define a one-off warm-paper island back when the rest
     of the site was near-black. The page is light everywhere now, so they simply
     point at the global tokens and the beat stops being a special case. */
  --paper-ink: var(--text-hi);
  --paper-lo: var(--text-lo);
  --paper-line: var(--border);
  --shadow-paper: 0 1px 2px rgba(20, 17, 13, 0.1),
                  -2px 4px 12px -3px rgba(20, 17, 13, 0.16),
                  -8px 14px 34px -10px rgba(20, 17, 13, 0.2);
}
.wt-04 .wt-panel {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(18px, 3vh, 34px);
  padding: 72px 22px max(24px, env(safe-area-inset-bottom));
}
@media (min-width: 1100px) {
  .wt-04 .wt-panel {
    display: grid;
    grid-template-columns: 1.25fr 0.75fr;
    align-items: center;
    column-gap: clamp(32px, 5vw, 88px);
    padding: 0 clamp(32px, 6vw, 96px);
  }
}
.wt-peak-wrap { min-width: 0; }
.wt-peak {
  font-family: var(--font-display);
  font-size: var(--t-peak);
  font-style: italic;
  /* Italic Fraunces appears exactly ONCE on the entire page. This is it. */
  font-variation-settings: "opsz" 144, "wght" 500, "SOFT" 0, "WONK" 0;
  letter-spacing: -0.014em;
  line-height: 0.98;
  /* Back to the brand colour, which the old palette made impossible: #ff5924 on
     warm paper was 2.14:1 and failed even the 3:1 large-text floor, so the peak
     had to be set in plain ink. #3e119d on #f8f8f8 is 11.16:1 — normal-text
     grade — so the visitor's own sentence can now be the brand's own violet at
     136px. That restores the semantic the whole page is built on: the accent
     marks YOU, and here it is, in his mouth. */
  color: var(--accent);
  margin: 0;
  text-wrap: balance;
  min-height: 2.1em;   /* reserved so the reveal never reflows */
}
.wt-peak q { quotes: none; }
.wt-peak .qm { color: var(--paper-lo); }

/* the fork echo: the beat-02 chips reprinted small, chosen at full opacity,
   unchosen at 0.2. Zero words, and it makes the 02 -> 04 connection unmissable
   without a caption. The cheapest and most effective thing on the page. */
.wt-fork { display: flex; flex-direction: column; gap: 7px; margin: 22px 0 0; }
.wt-fork li {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 9px;
  font-family: var(--font-ios);
  font-size: var(--t-13);
  line-height: var(--lh-13);
  color: var(--paper-ink);
  opacity: 0.2;
}
.wt-fork li::before {
  content: "";
  width: 9px; height: 9px;
  flex: none;
  border-radius: 2px;
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--paper-lo);
}
html:has(#wt-c-a:checked) .wt-fork li:first-child, html[data-choice="a"] .wt-fork li:first-child,
html:has(#wt-c-b:checked) .wt-fork li:last-child, html[data-choice="b"] .wt-fork li:last-child {
  opacity: 1;
}
/* The accent survives at beat 04 as a SOLID MARK, not as type -- non-text has no
   contrast requirement, and a small orange square on warm paper is gorgeous. */
html:has(#wt-c-a:checked) .wt-fork li:first-child::before, html[data-choice="a"] .wt-fork li:first-child::before,
html:has(#wt-c-b:checked) .wt-fork li:last-child::before, html[data-choice="b"] .wt-fork li:last-child::before {
  background: var(--accent);
  box-shadow: none;
}

.wt-evidence {
  font-family: var(--font-ios);
  background: var(--surface);
  border-radius: var(--ios-col-x) / var(--ios-col-y);
  box-shadow: var(--shadow-paper);
  padding: 18px 16px;
  display: flex;
  flex-direction: column;
  gap: 9px;
  max-width: 340px;
  justify-self: center;
}
.wt-lock {
  display: flex;
  align-items: center;
  gap: 7px;
  font-family: var(--font-ios);
  font-size: var(--t-13);
  line-height: var(--lh-13);
  color: var(--paper-lo);
  margin: 0;
}
.wt-lock svg { flex: none; }

/* ---- beat 05: the lock screen ---------------------------------------- */
.wt-05 .wt-col {
  background: #030305;
  justify-content: flex-start;
  align-items: center;
  padding: 12svh 16px 0;
}
.wt-wall {
  position: absolute;
  inset: -8%;
  z-index: 0;
  width: 116%; height: 116%;
  object-fit: cover;
  object-position: 50% 16%;
  /* blur solves the upscale problem and the genre problem in one property */
  filter: brightness(0.3) blur(40px);
}
.wt-lockclock {
  position: relative;
  z-index: 2;
  font-family: var(--font-ios);
  font-variant-numeric: tabular-nums;
  font-size: clamp(3.5rem, 10vw, 7rem);
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--text-hi);
  opacity: 0.92;
  display: flex;
  align-items: baseline;
  gap: 8px;
}
.wt-lockclock small { font-size: var(--t-17); font-weight: 500; letter-spacing: 0; }
.wt-note {
  position: relative;
  z-index: 2;
  margin-top: 5svh;
  width: 100%;
  max-width: 340px;
  background: var(--surface);
  border-radius: var(--ios-card);
  box-shadow: var(--shadow-ios);
  padding: 12px 14px;
  display: flex;
  gap: 11px;
  align-items: flex-start;
}
.wt-note .icon {
  position: relative;
  flex: none;
  width: 36px; height: 36px;
  border-radius: 9px;
  overflow: hidden;
  background: var(--surface-2);
}
.wt-note .icon img { width: 100%; height: 100%; object-fit: cover; object-position: 52% 10%; }
/* The one chromatic event on this screen, and the only thing alive on it. Peak
   at 44%, not 50% -- a breath inhales slightly faster than it exhales, and a
   symmetric sine reads as a notification pulse rather than as breathing. */
.wt-ring::after {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: 50%;
  box-shadow: 0 0 0 1px var(--accent), 0 0 18px -2px var(--accent);
  animation: wt-breathe 5200ms cubic-bezier(0.37, 0, 0.63, 1) infinite;
}
@keyframes wt-breathe {
  0% { transform: scale(1); opacity: 0.34; }
  44% { transform: scale(1.055); opacity: 0.62; }
  100% { transform: scale(1); opacity: 0.34; }
}
.wt-note .body { min-width: 0; }
.wt-note .from {
  font-size: var(--t-11);
  line-height: var(--lh-11);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-faint);
}
.wt-note .ttl { font-size: var(--t-13); line-height: var(--lh-13); font-weight: 600; color: var(--text-hi); margin: 2px 0 1px; }
.wt-note .txt { font-size: var(--t-12); line-height: var(--lh-12); color: var(--text-lo); margin: 0; }

/* ---- display lines in the flanks ------------------------------------- */
.wt-display {
  font-family: var(--font-display);
  font-size: var(--t-display);
  font-variation-settings: "opsz" 144, "wght" 500, "SOFT" 0, "WONK" 0;
  letter-spacing: -0.02em;
  line-height: 0.94;
  color: var(--text-hi);
  max-width: 15ch;
  margin: 0;
}
.wt-crop {
  position: relative;
  width: min(240px, 100%);
  overflow: hidden;
  border-radius: var(--radius-sm);
  /* top/bottom dissolve so a figure never ends on a rectangle */
  mask-image: linear-gradient(to bottom, transparent 0, #000 9%, #000 88%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 9%, #000 88%, transparent 100%);
}
.wt-crop img {
  width: 320px;
  max-width: none;
  filter: brightness(var(--fig-lum)) contrast(var(--fig-con));
}
/* Rendered crop boxes are capped at 240px. The source render is only 421x760 --
   there is no higher-resolution original -- so the tight crops would sit at
   2.4-3.1x upscale at 300px and visibly soften. Scale disparity between beats is
   therefore delivered by crop tightness and by column-width variation, which is
   a weaker version of the effect than the reference sites achieve. Restoring the
   full ladder needs a ~3100px re-export. */
.wt-crop--hand { --fig-lum: 0.58; --fig-con: 1.12; aspect-ratio: 125 / 110; }
.wt-crop--hand img { margin: -105px 0 0 -60px; }
.wt-crop--choker { --fig-lum: 0.62; --fig-con: 1.1; aspect-ratio: 195 / 105; }
.wt-crop--choker img { margin: -160px 0 0 -140px; }

/* ---- close: the loop, and the highest-intent screen on the page ------- */
.wt-close {
  height: 110svh;
  display: grid;
  place-items: center;
  text-align: center;
  padding: 0 24px;
  background: var(--paper);
  position: relative;
  z-index: 2;
}
.wt-close .inner { display: flex; flex-direction: column; align-items: center; gap: 26px; }
.wt-close h2 {
  font-family: var(--font-display);
  font-size: var(--t-display);
  font-variation-settings: "opsz" 144, "wght" 500, "SOFT" 0, "WONK" 0;
  font-style: normal;   /* italic is spent at beat 04 */
  letter-spacing: -0.02em;
  line-height: 0.94;
  color: var(--text-lo);
  text-wrap: balance;
  max-width: 15ch;
  margin: 0;
}
.wt-close h2 em { font-style: normal; color: var(--accent); }
.wt-restart {
  font-family: var(--font-mono);
  font-size: var(--t-11);
  line-height: var(--lh-flush);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.wt-restart:hover { color: var(--text-lo); }

/* ---- nav over paper --------------------------------------------------- */
/* One declaration, all four properties. Two separate `transition` declarations
   on .nav would not merge — the later would replace the earlier outright and the
   light/dark colour crossfade would silently stop working. */
.nav {
  transition: background-color 220ms var(--ease-dive),
              border-color 220ms var(--ease-dive),
              color 220ms var(--ease-dive),
              translate 320ms var(--ease-dive);
}
/* Inverted from the previous build: the nav is light by default now, so the
   class marks the DARK-content stretches instead. Same observer, opposite
   meaning.

   OVER INK THE BAR IS A SCRIM, NOT A PLATE — and it no longer asks
   backdrop-filter to do anything, because over the stage it cannot.

   Measured, not assumed: the bar rendered a flat #4c4c4d across its whole
   width, top of page, over near-black artwork. rgba(10,10,12,0.72) composited
   over the LIGHT page ground #f8f8f8 computes to (77,77,78); over the stage's
   own #0d0d11 it would compute to (10,10,12). The screenshot measures
   (76,76,77) — so the blur was sampling the page background and never saw the
   artwork at all. The stage is GPU-promoted (a filter on .stage-back, a
   scroll-driven timeline on the frame), and backdrop-filter does not sample
   across that.

   Three things were wrong with the result, not one:
     - It was a mid-grey band. The whole site is light shell over dark content
       and the posture rule says do not average the two into a middle grey.
       This averaged them, in the chrome, on the first screen.
     - The links (#a8a6ae) scored 3.57:1 on it. They were picked against a
       near-black bar, where they score 8.22:1. AA needs 4.5:1 at 14px.
     - blur(14px) over a flat fill is a no-op that still costs a composited
       layer for the length of the hero.

   So: no fill, no blur, no border over ink — a top-down gradient instead. It
   darkens where the type is and falls to nothing at the bottom edge, so the
   artwork runs up to the top of the window with no seam across it. Same
   reading as the app's own treatment: light falling on a stage, not a panel
   laid over one.

   The scrim is a pseudo-element cross-faded by opacity rather than a
   background-image on .nav itself. background-image is not in the transition
   list above and gradients do not interpolate to a flat colour anyway, so
   painting it directly would have snapped the light/dark change that the
   single-declaration note above exists to protect. */
/* position: relative for this lives in the single .nav position declaration
   further down — the one that hands stickiness to .nav-shell. Declaring it
   here too would be a second declaration of the same property, and the later
   one wins: the scrim would resolve against .nav-shell instead and stop
   travelling with the bar when it retracts. */
.nav::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(
    to bottom,
    rgba(10, 10, 12, 0.78),
    rgba(10, 10, 12, 0.46) 62%,
    rgba(10, 10, 12, 0)
  );
  transition: opacity 220ms var(--ease-dive);
}
/* .wrap is static, so it would paint under the positioned scrim. Positioned
   and later in the DOM, it paints over it — no z-index needed. */
.nav .wrap { position: relative; }

html.nav-on-ink .nav {
  background: transparent;
  border-bottom-color: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
html.nav-on-ink .nav::before { opacity: 1; }
/* The wordmark had no transition of its own: the bar cross-faded over 220ms,
   the links over --duration-fast (180ms), and the wordmark snapped. Three
   speeds for one change of state. All 220ms now, on the same curve. */
html.nav-on-ink .nav-logo { color: #f6f7f8; }
html.nav-on-ink .nav-links a { color: #a8a6ae; }
html.nav-on-ink .nav-links a:hover { color: #f6f7f8; }
.nav-logo { transition: color 220ms var(--ease-dive); }

/* ---- entrance cascades ------------------------------------------------
   Every base state is authored under html.js, so with JS disabled everything
   renders FINISHED rather than blank. Classes are added and never removed:
   replay-on-scroll-back is the definitive tell of a stock scroll library. */
html.js .wt-ask,
html.js .wt-choices .wt-choice,
html.js .wt-peak,
html.js .wt-evidence,
html.js .wt-lock,
html.js .wt-note,
html.js .wt-lockclock { opacity: 0; }

.wt-beat.is-live .wt-ask {
  animation: wt-rise 520ms var(--ease-dive) 140ms both;
}
.wt-beat.is-live #wt-l-a { animation: wt-rise 480ms var(--ease-dive) 420ms both; }
.wt-beat.is-live #wt-l-b { animation: wt-rise 480ms var(--ease-dive) 500ms both; }
@keyframes wt-rise {
  from { opacity: 0; transform: translateY(8px); filter: blur(5px); }
  to { opacity: 1; transform: none; filter: blur(0); }
}

/* beat 04 -- the page's only slow moment. The peak is bought with 600ms of
   silence, not with motion. Expo.OUT, not inOut: a surfacing should decelerate,
   and expo.out front-loads ~76% of the perceptual change into the first 350ms,
   so a fast scroller who outruns the 140svh pin still sees essentially the whole
   reveal. font-size is never animated (reflow); letter-spacing tightens as it
   arrives, which is what a typographer does by hand. */
.wt-04.is-live .wt-evidence { animation: wt-rise 420ms var(--ease-dive) 600ms both; }
.wt-04.is-live .wt-peak { animation: wt-surface 1400ms var(--ease-surface) 1300ms both; }
.wt-04.is-live .wt-lock { animation: wt-rise 500ms var(--ease-dive) 2900ms both; }
@keyframes wt-surface {
  from { opacity: 0; transform: translateY(18px); filter: blur(9px); letter-spacing: -0.008em; }
  to { opacity: 1; transform: none; filter: blur(0); letter-spacing: -0.014em; }
}

/* beat 05 -- iOS banners do not bounce. No scale, no spring. */
.wt-05.is-live .wt-lockclock { animation: wt-fade 420ms var(--ease-dive) 200ms both; }
.wt-05.is-live .wt-note { animation: wt-drop 700ms var(--ease-dive) 1400ms both; }
@keyframes wt-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes wt-drop {
  from { opacity: 0; transform: translateY(-118%); }
  40% { opacity: 1; }
  to { opacity: 1; transform: none; }
}

/* ---- mobile: the primary case ---------------------------------------- */
@media (max-width: 1099px) {
  .wt-seam .wt-display {
    display: block;
    font-size: clamp(1.75rem, 0.9rem + 4.6vw, 3rem);
    max-width: 18ch;
    margin: 0 22px;
    color: var(--text-hi);
  }
  .wt-seam--a, .wt-seam--b { display: flex; align-items: center; }
  .wt-panel { --col: 100vw; padding: 0; }
  .wt-col {
    width: 100vw;
    max-width: 100vw;
    aspect-ratio: auto;
    height: 100svh;
    border-radius: 0;
    box-shadow: none;
  }
  .wt-04 .wt-panel { padding: 84px 22px max(24px, env(safe-area-inset-bottom)); }
  .wt-evidence { max-width: 100%; border-radius: var(--ios-card); }
  /* Display lines move OUT of the panel into the seams on mobile, so the only
     text inside a panel is the diegetic app UI. */
  .wt-flank { display: none !important; }
}
@media (max-width: 700px) {
  /* Track budget: 1016svh vs 1384 on desktop (73%). Mobile visitors spend about
     half as long as desktop, so the phone has no budget for extra length and
     none at all for friction. */
  .wt-01 { height: 150svh; }
  .wt-02 { height: 160svh; }
  .wt-03 { height: 130svh; }
  .wt-04 { height: 170svh; }
  .wt-05 { height: 130svh; }
  .wt-seam--a { height: 10svh; }
  .wt-seam--b { height: 16svh; }
  .wt-seam--c { height: 44svh; padding-top: 5svh; }
  .wt-seam--d { height: 22svh; }
  .wt-close { height: 96svh; }
  /* Beat 02 on mobile is optimised for TAP RATE and nothing else is allowed in
     frame. The hint drops to 1800ms because a 70svh pin at flick speed will
     never reach 4200ms. */
  .wt-02.is-live .wt-hint { transition-delay: 1800ms; }
  .wt-02 .wt-ask { margin-bottom: 4svh; }
  .wt-peak { min-height: 3.2em; }
}

/* ---- reduced motion: HOLD frames, never reset them ------------------- */
@media (prefers-reduced-motion: reduce) {
  html.js .wt-ask, html.js .wt-choices .wt-choice, html.js .wt-peak,
  html.js .wt-evidence, html.js .wt-lock, html.js .wt-note,
  html.js .wt-lockclock { opacity: 1; }
  .wt-ring::after { animation: none; transform: scale(1.055); opacity: 0.62; }
  .wt-01 .wt-shot--b { animation: none; opacity: 0; }
  .wt-01 .wt-bar i { animation: none; width: 62%; }
  html:has(.wt-radio:checked) .wt-02 .wt-bar i,
  html[data-answered] .wt-02 .wt-bar i { animation: none; width: 100%; }
  .wt-skel { display: none; }   /* three static grey bars read as a load failure */
  .wt-peak { filter: none; letter-spacing: -0.014em; }  /* the voice change is STATIC */
  .wt-dim { backdrop-filter: none; -webkit-backdrop-filter: none; }
}

/* ---- variant plumbing -------------------------------------------------
   .wt-var--a{display:block} would break these two, which need their own
   display. Specificity: html:has(#id) contributes 1,1,1 so the two-class
   selector below (1,3,1) beats the generic branch rule (1,2,1). */
html:has(#wt-c-a:checked) .wt-evidence.wt-var--a, html[data-choice="a"] .wt-evidence.wt-var--a,
html:has(#wt-c-b:checked) .wt-evidence.wt-var--b, html[data-choice="b"] .wt-evidence.wt-var--b { display: flex; }
html:has(#wt-c-a:checked) .wt-reask, html:has(#wt-c-b:checked) .wt-reask,
html[data-choice="a"] .wt-reask, html[data-choice="b"] .wt-reask { display: none; }

/* The fork echo needs something to echo. Hidden until the visitor has answered;
   two greyed rows next to the re-ask would just repeat it. */
.wt-fork { display: none; padding: 0; }
html:has(.wt-radio:checked) .wt-fork, html[data-answered] .wt-fork { display: flex; }

.wt-evidence-slot { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
@media (min-width: 1100px) { .wt-evidence-slot { justify-self: center; } }
.wt-evidence .wt-msg { max-width: 100%; align-self: stretch; }
.wt-evidence .wt-time { text-align: left; }

/* the no-answer branch, on paper. The chip's dark-beat colours would fail
   contrast here, so it gets its own pair: #7a2a0e on the tinted paper is 6.6:1. */
.wt-reask { display: flex; flex-direction: column; gap: 10px; max-width: 32ch; }
.wt-reask-q {
  font-family: var(--font-display);
  font-size: var(--t-17);
  font-variation-settings: "opsz" 48, "wght" 500;
  color: var(--paper-ink);
  margin: 0 0 4px;
}
.wt-04 .wt-choice {
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 50%, transparent);
  color: var(--accent);
  min-height: 48px;
}
@media (hover: hover) {
  .wt-04 .wt-choice:hover {
    background: color-mix(in srgb, var(--accent) 24%, transparent);
    border-color: color-mix(in srgb, var(--accent) 72%, transparent);
  }
}

/* ---- beat 03: the composing chip is a moment, not a steady state -------
   It sits absolutely at the thread's foot, over the space the reply already
   reserves, and cross-fades with it: chip in at 300ms, out at 1240ms, reply in
   at 1300ms. Absolute so the thread's height never changes (no reflow, no jank).
   Hidden entirely without JS, because then the reply is already on screen -- and
   hidden until answered, because the machine is only composing since you spoke. */
.wt-thread { position: relative; }
.wt-composing { display: none; position: absolute; left: 14px; bottom: 6px; }
html.js:has(.wt-radio:checked) .wt-composing,
html.js[data-answered] .wt-composing { display: inline-flex; }
.wt-03.is-live .wt-composing { animation: wt-chip 1440ms linear 300ms both; }
@keyframes wt-chip { 0% { opacity: 0; } 18% { opacity: 1; } 65% { opacity: 1; } 100% { opacity: 0; } }
html.js .wt-thread .wt-msg { opacity: 0; }
.wt-03.is-live .wt-msg--me { animation: wt-rise 380ms var(--ease-dive) 200ms both; }
.wt-03.is-live .wt-msg--him { animation: wt-rise 460ms var(--ease-dive) 1300ms both; }
/* the unanswered branch has no cascade to wait for */
.wt-03.is-live .wt-var--none.wt-msg { animation: wt-rise 460ms var(--ease-dive) 200ms both; }

@media (prefers-reduced-motion: reduce) {
  html.js .wt-thread .wt-msg { opacity: 1; }
  .wt-composing { display: none !important; }
}

/* ---- fixes from the first real render ---------------------------------
   1. beat 02's ask + choices were laid out as if the stage were a flex column;
      it wasn't, so `margin:auto` did nothing and they sat at the top of the
      frame instead of in the thumb zone. The stage now IS a flex column whose
      only in-flow children are those two, aligned to the end. Everything else
      in the stage (shot, dim, bar, clock) is absolute and unaffected. */
.wt-02 .wt-stage {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding-bottom: 8px;
}
.wt-02 .wt-ask { margin: 0 22px; }

/* 2. The nav is 64px, sticky, and in flow, so it sat on top of every pinned
      panel and swallowed beat 01's bonds row. Keeping the column full-bleed is
      the whole point on mobile -- a translucent blurred bar over app content
      reads as a status bar and reinforces it -- so the content clears the nav
      rather than the column shrinking away from it. */
.wt-bonds { padding-top: 78px; }
@media (min-width: 1100px) { .wt-bonds { padding-top: 14px; } }
.wt-05 .wt-col { padding-top: 16svh; }

/* ==========================================================================
   THE STAGE — hero and beat 01 merged; the figure steps out of the episode.
   Replaces the old split-column hero entirely (see index.html for why).
   ========================================================================== */

.stage {
  position: relative;
  height: 200svh;
  background: var(--paper);
  padding: 0;
}
.stage-pin {
  position: sticky;
  top: 0;
  /* Above .stage-tail (3). `overflow: visible` alone was not enough to let him
     leave: a sticky element is ALWAYS its own stacking context, so the figure's
     z-index 6 could never compete with a sibling of the pin — the tail's opaque
     paper simply painted over the whole pin and sliced him at its top edge. */
  z-index: 4;
  height: 100svh;
  overflow: visible;   /* he has to be able to leave the frame */
  display: grid;
  place-items: center;
}
.stage-trip { position: absolute; top: 62svh; left: 0; width: 1px; height: 1px; }

/* the frame: the "episode". Its own clip is what recedes. */
.stage-frame {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  clip-path: inset(0 0 0 0 round 0);
  background: #08070a;
}
/* Shallow-depth stage light, not a second copy of him. Scaled up past the
   frame so the blur never reveals a soft edge, and offset so the region behind
   his head is the darkest part of the plate. */
.stage-back {
  position: absolute;
  inset: -12%;
  width: 124%;
  height: 124%;
  object-fit: cover;
  object-position: 46% 30%;
  filter: blur(26px) brightness(0.34) saturate(0.9);
}
.stage-frame::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(8, 7, 10, 0.72) 0%, transparent 22%,
              transparent 46%, rgba(8, 7, 10, 0.86) 100%);
}

.stage-ui { position: absolute; inset: 0; z-index: 3; }
.stage-ui .wt-bonds { padding-top: 78px; }
.stage-ui .wt-slug { left: 22px; bottom: 96px; }
.stage-ui .wt-sub { left: 24px; right: 24px; bottom: 62px; }
.stage-ui .wt-bar { left: 22px; right: 22px; bottom: 44px; }
.stage-ui .wt-bar i { animation: wt-play 9s linear infinite; }
@media (min-width: 1100px) {
  .stage-ui .wt-bonds { padding-top: 92px; padding-left: clamp(24px, 4vw, 64px); }
  .stage-ui .wt-slug { left: clamp(24px, 4vw, 64px); }
  .stage-ui .wt-sub { left: 0; right: 0; }
  .stage-ui .wt-bar { left: clamp(24px, 4vw, 64px); right: clamp(24px, 4vw, 64px); }
}

/* the figure: sharp, alpha, ABOVE the frame so it can overhang once the frame
   pulls in. Bottom-aligned; at 78svh tall on a phone this is a downscale of the
   760px source, so it stays reasonably crisp. */
.stage-figure {
  position: absolute;
  z-index: 4;
  bottom: 0;
  left: 50%;
  height: 78svh;
  width: auto;
  max-width: none;
  transform-origin: 50% 100%;
  translate: -50% 0;
  filter: brightness(0.6) contrast(1.12) saturate(0.95);
  pointer-events: none;
}
@media (min-width: 1100px) {
  .stage-figure { height: 88svh; left: 62%; }
}

/* the title card. z below the figure on purpose. */
.stage-title {
  position: absolute;
  z-index: 2;
  left: 24px;
  right: 24px;
  top: 22svh;
}
@media (min-width: 1100px) {
  .stage-title { left: clamp(24px, 5vw, 88px); right: auto; top: auto; bottom: 20svh; max-width: 15ch; }
}
.stage-title .eyebrow { margin-bottom: 18px; }
/* step peak-caps — CONSOLIDATED from two declarations.
   This rule was declared here (2.5-6.5rem / 0.94 / -0.02em) and again ~700
   lines below (3.5-4.5rem / 0.78 / -0.045em / uppercase). The lower one won
   every property it named but did NOT restate font-variation-settings, so the
   live headline was silently drawing its size and tracking from one block and
   its axis from another. Nobody could read the effective spec off either.
   Merged here, carrying the lower block's values because those were the ones
   on screen.

   --lh-peak is 0.78 and that is only legal because this step is uppercase:
   caps have no descenders, so leading that would collide on a lowercase line
   of the same size is correct here. It is the reason peak-caps is its own step
   rather than a size of `display`.

   font-weight and the axis carry the same number — see the note at
   .section-head h2.

   "SOFT" 0, "WONK" 0 dropped: the Fraunces request in all three <head>s asks
   for ital,opsz,wght only, so those two axes were never downloaded and the
   declaration has always been a no-op. Six more copies of it survive in the
   dead .wt-* walkthrough block. */
.stage-title h1 {
  font-family: var(--font-display);
  font-size: clamp(3.5rem, 16vw, 4.5rem);
  font-weight: var(--fw-peak);
  font-variation-settings: "opsz" var(--opsz-peak), "wght" var(--fw-peak);
  letter-spacing: var(--tr-peak);
  line-height: var(--lh-peak);
  text-transform: uppercase;
  margin: 0;
  color: var(--text-hi);
  text-shadow: 0 2px 24px rgba(8, 7, 10, 0.55);
}
.stage-title h1 em { font-style: italic; color: var(--accent); }

/* ---- the emergence -----------------------------------------------------
   Scrubbed to the document's first viewport of scrolling. The stage sits at
   the very top of the page, so scroll(root) offsets are absolute and need no
   named timeline: 0 -> 100svh IS the detachment. No JS, no scroll listener. */
@keyframes st-frame {
  0%   { clip-path: inset(0 0 0 0 round 0); opacity: 1; }
  64%  { clip-path: inset(9svh 13vw 17svh 13vw round 30px); opacity: 1; }
  100% { clip-path: inset(9svh 13vw 17svh 13vw round 30px); opacity: 0; }
}
@keyframes st-figure {
  0%   { scale: 1; translate: -50% 0; filter: brightness(0.6) contrast(1.12) saturate(0.95); }
  100% { scale: 1.16; translate: -50% -3svh; filter: brightness(0.96) contrast(1) saturate(1); }
}
@keyframes st-fade { 0%, 26% { opacity: 1; } 68% { opacity: 0; } 100% { opacity: 0; } }

@supports (animation-timeline: scroll()) {
  .stage-frame, .stage-figure, .stage-ui, .stage-title {
    animation-timeline: scroll(root block);
    animation-range: 0 100svh;
    animation-timing-function: linear;
    animation-fill-mode: both;
  }
  .stage-frame { animation-name: st-frame; }
  .stage-figure { animation-name: st-figure; }
  .stage-ui, .stage-title { animation-name: st-fade; }
}

/* Fallback: one 900ms transition when the trip point crosses, instead of a
   scrubbed emergence. JS only adds a class -- it still never touches scroll. */
@supports not (animation-timeline: scroll()) {
  .stage-frame, .stage-figure, .stage-ui, .stage-title {
    transition: clip-path 900ms var(--ease-dive), opacity 900ms var(--ease-dive),
                scale 900ms var(--ease-dive), translate 900ms var(--ease-dive),
                filter 900ms var(--ease-dive);
  }
  .stage.is-out .stage-frame { clip-path: inset(9svh 13vw 17svh 13vw round 30px); opacity: 0; }
  .stage.is-out .stage-figure { scale: 1.16; translate: -50% -3svh; filter: brightness(0.96) contrast(1) saturate(1); }
  .stage.is-out .stage-ui, .stage.is-out .stage-title { opacity: 0; }
}

/* Reduced motion: no emergence at all. The stage holds its FIRST frame -- an
   episode paused, with the title card over it -- which is a complete, legible
   still on its own. Holding the detached end state instead would show a figure
   floating with no context and no explanation. */
@media (prefers-reduced-motion: reduce) {
  .stage { height: 100svh; }
  .stage-frame, .stage-figure, .stage-ui, .stage-title { animation: none !important; transition: none !important; }
  .stage.is-out .stage-frame { clip-path: inset(0 0 0 0 round 0); opacity: 1; }
  .stage.is-out .stage-figure { scale: 1; translate: -50% 0; }
  .stage.is-out .stage-ui, .stage.is-out .stage-title { opacity: 1; }
}

/* ---- stage composition fixes from the first render --------------------
   Two real faults, both about the overlap that is the whole point:

   (a) MOBILE: the title sat at 22svh and the 78svh bottom-aligned figure put his
       head at exactly 22svh, so his hair covered the middle of the headline —
       unreadable, not artful. The title moves above his head; his hair now
       reaches up into the last line, which is the overlap we wanted.

   (b) DESKTOP: there was NO overlap at all. Title left, figure right, nothing
       touching — i.e. the same left-copy/right-image split the whole redesign
       exists to escape. The figure moves inboard so the mic stand and his
       forearm cross the headline's right edge. A mic stand is thin, so it reads
       as depth rather than as an obstruction. */
/* (c) MOBILE, third fault, found by driving a real 390x844 viewport: moving the
   title up to 7svh to clear his hair (fault a) drove it into the bonds row
   instead — measured 342x94px of headline sitting directly on top of "YOUR
   BONDS" and the avatars. Two layers of legible type is never the overlap we
   wanted; that only ever meant type crossing the FIGURE.
   The clearance is a fixed px value, not svh, because the thing being cleared
   is fixed px too: 78px of nav offset + a 17px label + 9px + a 40px avatar =
   ~153px of rail regardless of viewport height. An svh value re-collides on
   every short phone.
   Accepted cost: at 390px there is no longer room for his hair to reach into
   the last line. The mic stand and forearm still cross the composition lower
   down, so the shot keeps its depth; the headline just stops fighting the UI. */
.stage-title { top: 168px; }
.stage-figure { height: 72svh; }
@media (min-width: 1100px) {
  .stage-title {
    top: auto;
    bottom: 14svh;
    max-width: 13ch;
    /* capped below the shared --t-display ceiling: at 104px this ran to six
       lines and climbed into the bonds row */
    font-size: clamp(2.5rem, 1.4rem + 4.4vw, 5rem);
  }
  /* DEAD as written: a later unconditional .stage-title h1 already set its own
     font-size, and at this same breakpoint an even later rule sets 5.5-8.5rem.
     Left in place — clearing dead CSS is a separate pass — but it decides
     nothing, and .stage-title's font-size above is inherited by nothing. */
  .stage-title h1 { font-size: inherit; }
  .stage-figure { height: 92svh; left: 54%; }
}

/* Desktop stage, second pass. The overlap now exists but landed badly: the mic
   stand ran through the middle of "They", and the title's last line sat on top
   of the episode slug. Raising the title clears the slug, and nudging the figure
   outboard moves the stand into the gap after "They" instead of through it.
   The overlap should read as depth, never as an occluded letter. */
@media (min-width: 1100px) {
  .stage-title { bottom: 26svh; }
  .stage-figure { left: 57%; }
}

/* The burned-in subtitle was centred on desktop, which put it behind the figure
   and truncated it to '"So s'. A caption reading as cut off is worse than no
   caption. It moves into the clear left column, above the episode slug, where it
   still reads as a subtitle because it sits inside the frame with the slug. */
@media (min-width: 1100px) {
  .stage-ui .wt-sub {
    left: clamp(24px, 4vw, 64px);
    right: auto;
    bottom: 132px;
    max-width: 34ch;
    text-align: left;
  }
}

/* ==========================================================================
   STAGE, THIRD PASS — after actually driving the page with a real scroll.
   Two failures that static endpoint screenshots could never have shown:

   (1) THE EMERGENCE WAS INVISIBLE. The frame's clip-path was receding exactly as
       specified, but the frame's contents are near-black on a near-black page, so
       there was no visible boundary for him to cross. A frame you cannot see
       cannot be stepped out of. The plate is now measurably lighter than the page
       and gains a hairline + shadow as it pulls in, so an actual rectangle
       exists, and his shoulders visibly overhang it.

   (2) THE PIN RELEASED HALFWAY THROUGH. .stage was 200svh with a 100svh pin, so
       sticky held only the first 100svh -- and animation-range ended at exactly
       that point. The second 100svh was dead: a figure scaled to 705px of an
       844px viewport sliding upward, filling the screen with a torso. The stage
       is shorter now, the emergence finishes before the pin releases, and he ends
       smaller so the exit reads as a person leaving rather than a chest close-up.
   ========================================================================== */

.stage { height: 170svh; }

/* the plate has to be visible to be left */
.stage-frame { background: #16141b; }
.stage-back { filter: blur(26px) brightness(0.52) saturate(0.92); }
.stage-frame::after {
  background: linear-gradient(180deg, rgba(12, 11, 16, 0.66) 0%, transparent 20%,
              transparent 44%, rgba(12, 11, 16, 0.8) 100%);
}
/* the edge itself: a hairline and a cast shadow that only exist once the frame
   has pulled in, so the boundary appears exactly when it starts to matter */
.stage-edge {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  border-radius: 30px;
  box-shadow: inset 0 0 0 1px rgba(245, 245, 247, 0.14),
              0 40px 90px -30px rgba(0, 0, 0, 0.9);
  opacity: 0;
}

/* he ends smaller than he started growing toward: 1.16x of 72svh filled the
   whole viewport and left no headroom */
.stage-figure { height: 66svh; }
@media (min-width: 1100px) { .stage-figure { height: 84svh; } }

@keyframes st-frame {
  0%   { clip-path: inset(0 0 0 0 round 0); opacity: 1; }
  70%  { clip-path: inset(7svh 9vw 13svh 9vw round 30px); opacity: 1; }
  100% { clip-path: inset(7svh 9vw 13svh 9vw round 30px); opacity: 0; }
}
@keyframes st-edge {
  0%, 12% { opacity: 0; clip-path: inset(0 0 0 0 round 0); }
  70%     { opacity: 1; clip-path: inset(7svh 9vw 13svh 9vw round 30px); }
  100%    { opacity: 0; clip-path: inset(7svh 9vw 13svh 9vw round 30px); }
}
@keyframes st-figure {
  0%   { scale: 1; translate: -50% 0; filter: brightness(0.62) contrast(1.12) saturate(0.95); }
  70%  { scale: 1.08; translate: -50% -2svh; filter: brightness(0.95) contrast(1.02) saturate(1); }
  100% { scale: 1.08; translate: -50% -2svh; filter: brightness(0.95) contrast(1.02) saturate(1); }
}

@supports (animation-timeline: scroll()) {
  /* 62svh, not 100: the pin releases at 70svh (170 - 100), so the emergence has
     to be complete before then or it gets cut off mid-move. */
  .stage-frame, .stage-figure, .stage-ui, .stage-title, .stage-edge {
    animation-range: 0 62svh;
  }
  .stage-edge { animation-name: st-edge; }
}
@supports not (animation-timeline: scroll()) {
  .stage-edge { transition: opacity 900ms var(--ease-dive), clip-path 900ms var(--ease-dive); }
  .stage.is-out .stage-edge { opacity: 0; clip-path: inset(7svh 9vw 13svh 9vw round 30px); }
}
@media (prefers-reduced-motion: reduce) {
  .stage { height: 100svh; }
  .stage-edge { animation: none !important; transition: none !important; opacity: 0; }
}

/* ==========================================================================
   STAGE, FOURTH PASS — the emergence still did not read.
   Driving it revealed the frame was receding only 9vw (about 35px on a 390px
   phone), which is not enough inset to register as a frame at all: it looked
   like a slow zoom plus a brightness lift, not like leaving a shot. The frame
   now collapses all the way down to a small playing card in the upper third, so
   he plainly towers over it and overlaps it top and bottom. That is
   unmistakable, and it is also truer to the product: the episode keeps playing,
   small, while he is here.

   Second: the pin releases 100svh before the section ends, and during that exit
   a 600px figure sliding up filled the screen with a torso. He now fades over
   the exit range, using a second animation on the same timeline with its own
   animation-range, so there is nothing left to crop.
   ========================================================================== */

.stage { height: 145svh; }

@keyframes st-frame {
  0%   { clip-path: inset(0 0 0 0 round 0); opacity: 1; }
  76%  { clip-path: inset(9svh 24vw 47svh 24vw round 22px); opacity: 1; }
  100% { clip-path: inset(9svh 24vw 47svh 24vw round 22px); opacity: 0.9; }
}
@keyframes st-edge {
  0%, 10% { opacity: 0; clip-path: inset(0 0 0 0 round 0); }
  40%     { opacity: 1; }
  76%     { opacity: 1; clip-path: inset(9svh 24vw 47svh 24vw round 22px); }
  100%    { opacity: 0.85; clip-path: inset(9svh 24vw 47svh 24vw round 22px); }
}
/* he holds his ground while the shot shrinks behind him — the scale barely
   changes, because the read comes from the frame collapsing, not from zoom */
@keyframes st-figure {
  0%   { scale: 1; translate: -50% 0; filter: brightness(0.62) contrast(1.12) saturate(0.95); }
  76%  { scale: 1.04; translate: -50% 0; filter: brightness(0.97) contrast(1.02) saturate(1); }
  100% { scale: 1.04; translate: -50% 0; filter: brightness(0.97) contrast(1.02) saturate(1); }
}
@keyframes st-out { from { opacity: 1; } to { opacity: 0; } }
.stage-edge { border-radius: 22px; }

@supports (animation-timeline: scroll()) {
  .stage-frame, .stage-ui, .stage-title, .stage-edge { animation-range: 0 46svh; }
  /* two animations, two ranges, one timeline: emerge, then fade out over the
     exit so the receding figure never becomes a chest close-up */
  .stage-figure {
    /* No fade on exit any more. The old st-out fade existed because a 600px
       figure sliding up a NEAR-BLACK screen cropped to a chest close-up with
       nothing around it. Against near-white you can see it is a person walking
       out of frame, so letting him go with the section is better than dissolving
       him into an empty screen. */
    animation-name: st-figure;
    animation-range: 0 46svh;
  }
}
@supports not (animation-timeline: scroll()) {
  .stage.is-out .stage-frame { clip-path: inset(9svh 24vw 47svh 24vw round 22px); opacity: 0.9; }
  .stage.is-out .stage-edge { opacity: 1; clip-path: inset(9svh 24vw 47svh 24vw round 22px); }
  .stage.is-out .stage-figure { scale: 1.04; translate: -50% 0; filter: brightness(0.97) contrast(1.02) saturate(1); }
}
@media (prefers-reduced-motion: reduce) { .stage { height: 100svh; } }

/* ==========================================================================
   STAGE, FIFTH PASS — the mechanic itself was wrong.
   The figure sat ON TOP of the frame from frame zero, so he was never inside it;
   he was always in front. Nothing can detach from a boundary it starts outside
   of, which is why four passes of tuning the frame's recede never produced the
   effect: all it ever showed was a rectangle shrinking behind a static figure.

   Correct mechanic, two copies of the same art:
     .stage-inner   — INSIDE the frame's clip. This is him in the shot. As the
                      frame collapses he is cut away in here.
     .stage-figure  — OUTSIDE the clip. Fades up as the frame collapses.
   Plus genuine relative motion: the shot recedes into a small card in the upper
   right while he advances, growing and settling lower. The shot keeps playing,
   small, behind him — which is also true to the product.
   ========================================================================== */

.stage-inner {
  position: absolute;
  z-index: 2;
  bottom: 0;
  left: 50%;
  height: 66svh;
  width: auto;
  max-width: none;
  translate: -50% 0;
  transform-origin: 50% 100%;
  filter: brightness(0.62) contrast(1.12) saturate(0.95);
}
@media (min-width: 1100px) { .stage-inner { height: 84svh; left: 57%; } }

.stage-edge { z-index: 3; }
.stage-ui { z-index: 4; }
.stage-title { z-index: 5; }
.stage-figure { z-index: 6; opacity: 0; }
html:not(.js) .stage-figure { opacity: 1; }

/* the shot retreats to a small card, upper right, and keeps playing */
@keyframes st-frame {
  0%   { clip-path: inset(0 0 0 0 round 0); opacity: 1; }
  78%  { clip-path: inset(9svh 6vw 58svh 44vw round 18px); opacity: 1; }
  100% { clip-path: inset(9svh 6vw 58svh 44vw round 18px); opacity: 1; }
}
@keyframes st-edge {
  0%, 8% { opacity: 0; clip-path: inset(0 0 0 0 round 0); }
  34%    { opacity: 1; }
  78%    { opacity: 1; clip-path: inset(9svh 6vw 58svh 44vw round 18px); }
  100%   { opacity: 1; clip-path: inset(9svh 6vw 58svh 44vw round 18px); }
}
/* he advances: grows and settles lower, so there is real relative motion
   against the retreating shot rather than a static figure and a shrinking box */
@keyframes st-figure-in {
  0%, 22% { opacity: 0; }
  62%     { opacity: 1; }
  100%    { opacity: 1; }
}
@keyframes st-advance {
  0%   { scale: 1; translate: -50% 0; filter: brightness(0.62) contrast(1.12) saturate(0.95); }
  78%  { scale: 1.2; translate: -50% 4svh; filter: brightness(1) contrast(1.02) saturate(1); }
  100% { scale: 1.2; translate: -50% 4svh; filter: brightness(1) contrast(1.02) saturate(1); }
}

@supports (animation-timeline: scroll()) {
  .stage-inner {
    animation-name: st-advance;
    animation-timeline: scroll(root block);
    animation-range: 0 52svh;
    animation-timing-function: linear;
    animation-fill-mode: both;
  }
  .stage-figure {
    animation-name: st-advance, st-figure-in, st-out;
    animation-range: 0 52svh, 0 52svh, 58svh 88svh;
  }
}
@supports not (animation-timeline: scroll()) {
  .stage-inner { transition: scale 900ms var(--ease-dive), translate 900ms var(--ease-dive), filter 900ms var(--ease-dive); }
  .stage.is-out .stage-frame { clip-path: inset(9svh 6vw 58svh 44vw round 18px); opacity: 1; }
  .stage.is-out .stage-edge { opacity: 1; clip-path: inset(9svh 6vw 58svh 44vw round 18px); }
  .stage.is-out .stage-inner,
  .stage.is-out .stage-figure { scale: 1.2; translate: -50% 4svh; filter: brightness(1) contrast(1.02) saturate(1); }
  .stage.is-out .stage-figure { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .stage-inner { animation: none !important; transition: none !important; }
  .stage-figure { animation: none !important; opacity: 0 !important; }
}

/* On phones the st-out fade reads as a decode failure, not an exit: mid-scroll
   the figure sits at ~30% opacity, bleached over the paper, under the tail
   text. Keep him solid and let the tail band — opaque paper, raised above his
   z-index 6 — occlude him from below as it rises. He leaves with the section
   instead of dissolving on it. Desktop keeps the fade: there the figure and
   the tail line deliberately share the frame. */
@media (max-width: 760px) {
  @supports (animation-timeline: scroll()) {
    /* 44svh, not 52: the 144svh mobile section releases its pin at 44svh of
       scroll, and the emergence should be complete by then. */
    .stage-figure {
      animation-name: st-advance, st-figure-in;
      animation-range: 0 44svh, 0 44svh;
    }
  }
  /* Above the figure's z-index 6. The extra selector weight is required: the
     base `.stage-tail { z-index: 3 }` rule sits LATER in this file and would
     otherwise re-flatten this override. */
  header.stage .stage-tail { z-index: 7; }
  /* The solid figure is 88svh of pinned art: at the section's end his legs
     paint past its bottom edge, under the tail band. Clip the section's own
     paint so he leaves with it instead of trailing into the next section. */
  header.stage { contain: paint; }
}

/* the episode keeps playing in the card he left. Positioned in the card's final
   rect (matching st-frame's end inset) and faded up as the card forms. Without
   this the card is an empty rectangle and he reads as walking past a shape
   rather than out of a screen. */
.stage-mini {
  position: absolute;
  z-index: 7;
  top: 9svh;
  left: 44vw;
  right: 6vw;
  height: 33svh;
  border-radius: 18px;
  pointer-events: none;
  opacity: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 8px;
  padding: 12px;
}
.stage-mini .ep {
  font-family: var(--font-mono);
  font-size: var(--t-11);
  line-height: var(--lh-11);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--text-lo);
}
.stage-mini .wt-bar { position: static; left: auto; right: auto; bottom: auto; }
.stage-mini .wt-bar i { animation: wt-play 9s linear infinite; }
@media (min-width: 1100px) {
  .stage-mini { left: auto; right: clamp(24px, 5vw, 88px); width: min(260px, 22vw); height: 30svh; top: 14svh; }
}
@keyframes st-mini { 0%, 52% { opacity: 0; } 82% { opacity: 1; } 100% { opacity: 1; } }
@supports (animation-timeline: scroll()) {
  .stage-mini {
    animation-name: st-mini;
    animation-timeline: scroll(root block);
    animation-range: 0 46svh;
    animation-timing-function: linear;
    animation-fill-mode: both;
  }
}
@supports not (animation-timeline: scroll()) {
  .stage-mini { transition: opacity 900ms var(--ease-dive); }
  .stage.is-out .stage-mini { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) { .stage-mini { animation: none !important; opacity: 0; } }

/* The frame's backdrop is now a composited stage plate rather than a blurred copy
   of the lead. It already carries its own haze, key light and depth, so it needs
   only a light depth-of-field blur instead of the 26px that was previously doing
   the work of hiding that there was no scene at all. */
.stage-back {
  inset: -6%;
  width: 112%;
  height: 112%;
  object-position: 50% 42%;
  filter: blur(6px) brightness(1.02) saturate(1);
}
.stage-frame { background: #0d0b11; }  /* the episode's own ground */

/* the card's own content. The full-bleed plate, cropped by the frame at this
   screen position, shows only the upper glow — no band, so the card read as an
   empty rectangle. This is an explicit crop of the plate's lower half where the
   figures actually are, so the card unambiguously contains a scene. */
.stage-mini { overflow: hidden; background: #0d0b11; }
.stage-mini-shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 56% 78%;   /* the band, not the sky-glow */
  filter: brightness(1.15) saturate(1.05);
}
.stage-mini .ep, .stage-mini .wt-bar { position: relative; z-index: 2; }
.stage-mini .ep { text-shadow: 0 1px 3px rgba(0,0,0,.8); }

/* Keep the original full-screen stage composition and change only its colour
   temperature so it belongs to the later cool grey, violet, and cyan palette. */
.stage-frame { background: #0d0b13; }
.stage-back { filter: blur(5px) brightness(1.03) saturate(0.52) hue-rotate(240deg); }
.stage-frame::after {
  background:
    radial-gradient(circle at 78% 18%, rgba(148, 110, 254, 0.2), transparent 34%),
    radial-gradient(circle at 18% 76%, rgba(130, 248, 237, 0.05), transparent 24%),
    linear-gradient(180deg, rgba(9, 9, 13, 0.62) 0%, transparent 24%,
      transparent 46%, rgba(9, 9, 13, 0.84) 100%);
}
.stage-mini { background: #0d0b13; }
.stage-mini-shot { filter: brightness(0.96) saturate(0.54) hue-rotate(240deg); }
.stage-mini::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(135deg, rgba(62, 17, 157, 0.2), rgba(9, 9, 13, 0.12));
  pointer-events: none;
}
.stage-ui .wt-slug { bottom: 72px; }
.stage-ui .wt-sub {
  bottom: 168px;
  text-align: left;
}
.stage-inner,
.stage-figure,
.stage-ui .wt-sub,
.stage-ui .cast-credit {
  transition: opacity 180ms ease;
}
.stage.is-cast-switching .stage-inner,
.stage.is-cast-switching .stage-figure,
.stage.is-cast-switching .stage-ui .wt-sub,
.stage.is-cast-switching .stage-ui .cast-credit {
  opacity: 0 !important;
}
.stage.is-webgl-switching .stage-inner,
.stage.is-webgl-switching .stage-figure,
.stage.is-webgl-switching .stage-ui .wt-sub,
.stage.is-webgl-switching .stage-ui .cast-credit {
  opacity: 0 !important;
}
@media (max-width: 1099px) {
  /* The cast can overlap the copy, but the episode identity must stay readable. */
  .stage-ui { z-index: 7; }
  .stage-ui .wt-sub,
  .stage-ui .wt-slug {
    text-shadow: 0 1px 12px rgba(9, 9, 13, 0.92);
  }
}
@media (prefers-reduced-motion: reduce) {
  .stage-inner,
  .stage-figure,
  .stage-ui .wt-sub,
  .stage-ui .cast-credit,
  .wt-bonds button {
    transition: none;
  }
}

/* ==========================================================================
   THREE COMPOSITION FIXES, from driving the page at a real 390px viewport with
   settle times long enough for the reveals to finish. (An earlier pass "found"
   several empty screens that were only my capture outrunning a 1300ms delay —
   the reveals were fine. These three are the real ones.)
   ========================================================================== */

/* (1) beat 04, unanswered. This is the branch a visitor who scrolled past the
   question actually lands on, and it was a caption floating on a full screen of
   paper: the page's peak reading as unfinished. It is now composed as its own
   screen — the page asks again, at display scale, and the two choices are real
   targets rather than footnotes. Deliberately NOT explained: no "click one and
   we'll remember", which would narrate the mechanic instead of running it. */
.wt-reask { gap: 0; max-width: 22ch; }
.wt-reask-eyebrow {
  font-family: var(--font-mono);
  font-size: var(--t-11);
  line-height: var(--lh-11);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--paper-lo);
  margin: 0 0 14px;
}
.wt-reask-q {
  font-family: var(--font-display);
  font-size: clamp(2rem, 0.9rem + 5.2vw, 4.5rem);
  font-variation-settings: "opsz" 144, "wght" 500, "SOFT" 0, "WONK" 0;
  letter-spacing: -0.018em;
  line-height: 0.98;
  color: var(--paper-ink);
  margin: 0 0 30px;
}
.wt-reask-q em { font-style: italic; }
.wt-reask .wt-choice { min-height: 54px; margin-bottom: 10px; font-size: var(--t-15); line-height: var(--lh-flush); }
.wt-reask .wt-choice:last-child { margin-bottom: 0; }
/* the unanswered branch has no evidence card to show, so the slot must not
   reserve room for one */
html:not([data-answered]):not(:has(.wt-radio:checked)) .wt-evidence-slot { display: none; }

/* (2) beat 03. The chathead was 40% and the thread bottom-aligned, leaving a
   dead band across the middle of the screen. Messages hugging the bottom is
   correct for a chat, so the portrait grows into the space instead. */
.wt-chathead { height: 52%; }
@media (min-width: 1100px) { .wt-chathead { height: 46%; } }
.wt-thread { padding-top: 10px; }

/* (3) beat 05. A real lock screen's lower half is empty because the home
   indicator terminates it; without that terminator the same emptiness reads as
   unfinished layout. Also tightened the gap so the notification sits under the
   clock rather than drifting from it. */
.wt-05 .wt-col { padding-top: 19svh; }
.wt-note { margin-top: 3.4svh; }
.wt-home {
  position: absolute;
  left: 50%;
  bottom: 9px;
  translate: -50% 0;
  width: 134px;
  height: 5px;
  border-radius: 3px;
  background: rgba(245, 245, 247, 0.32);
  z-index: 2;
}

/* Desktop already carries these lines in the flanks; the seam copies would be a
   duplicate reading for screen readers and a second, weaker set on screen. */
@media (min-width: 1100px) { .wt-seam .wt-display { display: none; } }

/* The stage's tail. A sticky child 100svh tall releases a full viewport before
   its section ends, and on a near-white page that exit read as an unfinished
   screen rather than as breathing room (on the old near-black page the same gap
   read as atmosphere — the value flip changes what emptiness means). Putting the
   editorial line here means the figure rises out of frame as the sentence rises
   into it, which is a transition instead of a hole. */
.stage-tail {
  position: relative;
  z-index: 3;
  height: 45svh;
  display: flex;
  align-items: center;
  padding: 0 22px;
  background: var(--paper);
}
@media (min-width: 1100px) {
  .stage-tail { padding: 0 clamp(24px, 5vw, 88px); height: 40svh; }
}
/* step h1. Self-contained on purpose: this used to take its size here and then
   inherit tracking, leading and axis from the bare .wt-display rule ~950 lines
   up, which belongs to the retired walkthrough. That rule pinned "opsz" 144 —
   144pt hairlines rendering at 56px, the second-worst optical mismatch on the
   page. The step now states all five values where the live selector is. */
.stage-tail .wt-display {
  display: block;
  font-size: clamp(1.75rem, 0.9rem + 4.6vw, 3.5rem);
  font-weight: var(--fw-h1);
  font-variation-settings: "opsz" var(--opsz-h1), "wght" var(--fw-h1);
  letter-spacing: var(--tr-h1);
  line-height: var(--lh-h1);
  max-width: 20ch;
  color: var(--text-hi);
  margin: 0;
}

/* ==========================================================================
   THE NAV RETRACTS ON SCROLL DOWN.
   Direction-aware, and still zero JS: a scroll-state query container reports
   the scrollport's current direction, so CSS alone can do what this pattern
   normally needs a scroll listener for. That matters here because wt.js is
   explicitly forbidden from touching scroll — see its header.

   Two conditions, not one. `scroll-direction: down` alone would also retract the
   bar during the very first pixels of scrolling from the top; `scrollable: top`
   is false until there is something above you, so the nav stays put until the
   page has actually moved.
   ========================================================================== */
.nav-shell {
  position: sticky;
  top: 0;
  z-index: 20;
}
.nav {
  /* relative, not static: the shell still owns the stickiness (it is the
     sticky box), but .nav has to be the containing block for its own ::before
     scrim — otherwise the scrim resolves against .nav-shell and stays put
     while the retract below translates the bar out from under it. */
  position: relative;
}

/* Driven by a class from wt.js, not by a container query: scroll-direction is
   parsed but unimplemented in Chrome 150 (measured), so there is no CSS-only way
   to know which way the visitor is going. See the carve-out note in wt.js. */
html.nav-away .nav { translate: 0 -100%; }

/* Never trap keyboard focus behind a retracted bar. !important because the state
   class and this rule have the same specificity and tabbing must always win. */
.nav:focus-within { translate: 0 0 !important; }

/* With JS off the class is never added and the bar just stays — the previous
   behaviour, so there is nothing to regress. */

@media (prefers-reduced-motion: reduce) {
  .nav { transition-duration: 1ms; }
}

/* ==========================================================================
   FIRST FRAME — EPISODE TITLE CARD
   The scene now introduces one episode before it introduces the product. The
   existing cast art remains the production constraint; hierarchy and point of
   view do the narrative work until a purpose-built ensemble key visual exists.
   ========================================================================== */
/* The stage inherits the :root scale; it used to redeclare five of the same
   tokens (including a sub-floor --t-10) for no reason other than that the
   walkthrough block had. */
.stage-frame {
  background: var(--ink);
}
.stage-back {
  object-position: 50% 78%;
  filter: blur(2px) brightness(0.86) contrast(1.08) saturate(0.5) hue-rotate(240deg);
}
.stage-frame::after {
  background:
    radial-gradient(circle at 74% 24%, color-mix(in srgb, var(--accent) 20%, transparent), transparent 34%),
    linear-gradient(90deg, rgba(9, 9, 13, 0.28) 0%, rgba(9, 9, 13, 0.08) 48%, rgba(9, 9, 13, 0.38) 100%),
    linear-gradient(180deg, rgba(9, 9, 13, 0.56) 0%, transparent 30%, rgba(9, 9, 13, 0.84) 100%);
}

.stage-ui { z-index: 8; }
.stage-ui .wt-bonds {
  padding-top: 80px;
  padding-left: 24px;
}
.stage-ui .wt-bonds .cast-credit {
  display: block;
  margin-top: 8px;
  font-family: var(--font-body);
  font-size: var(--t-11);
  line-height: var(--lh-11);
  color: var(--text-lo);
}
.stage-ui .wt-sub {
  left: 24px;
  right: 24px;
  bottom: 32px;
  max-width: 34ch;
  font-size: var(--t-13);
  line-height: var(--lh-13);
  text-align: left;
}

.stage-title {
  z-index: 7;
  top: 232px;
  bottom: auto;
  left: 24px;
  right: 24px;
  max-width: 560px;
}
.stage-original {
  display: block;
  margin-bottom: 16px;
  font-family: var(--font-mono);
  font-size: var(--t-11);
  line-height: var(--lh-11);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--text-lo);
}
/* The second .stage-title h1 declaration used to sit here. Its values are now
   the single declaration further up (step peak-caps); nothing is lost. The
   ≥1100px size override further down still applies — it is later than both. */
.stage-title h1 em {
  display: block;
  margin-left: 0.55em;
}
.stage-episode {
  display: flex;
  align-items: baseline;
  gap: 16px;
  margin-top: 24px;
  font-family: var(--font-mono);
  text-transform: uppercase;
}
.stage-episode span {
  font-size: var(--t-11);
  line-height: var(--lh-11);
  letter-spacing: var(--label-tracking);
  color: var(--accent);
}
.stage-episode strong {
  font-size: var(--t-13);
  line-height: var(--lh-13);
  /* 600, not the UA's inherited `bold` = 700: IBM Plex Mono is loaded at 500/600
     only, so "SOUNDCHECK" — five words into the page, over the hero art — was
     being drawn as a synthesised bold. Mono is the worst face to fake a weight
     on, because every stem is the same width and the smear has nowhere to hide. */
  font-weight: 600;
  letter-spacing: var(--label-tracking);
  color: var(--text-hi);
}
.stage-logline {
  max-width: 32ch;
  margin: 16px 0 0;
  font-family: var(--font-body);
  font-size: var(--t-13);
  line-height: var(--lh-13);
  color: var(--text-lo);
  text-wrap: pretty;
}
.stage-enter {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  gap: 8px;
  margin-top: 16px;
  padding: 0 16px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: color-mix(in srgb, var(--surface) 76%, transparent);
  color: var(--text-hi);
  font-size: var(--t-12);
  line-height: var(--lh-flush);
  font-weight: 600;
  text-decoration: none;
  backdrop-filter: blur(16px) saturate(150%);
  -webkit-backdrop-filter: blur(16px) saturate(150%);
  transition: opacity 100ms ease-out, scale 100ms ease-out, border-color 180ms ease;
}
.stage-enter:active {
  opacity: 0.85;
  scale: 0.97;
}
.stage-enter:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.stage-tail .wt-display {
  max-width: 15ch;
}
.stage-tail .wt-display em {
  font-style: italic;
  color: var(--accent);
}

@media (min-width: 1100px) {
  .stage-ui .wt-bonds {
    padding-top: 88px;
    padding-left: clamp(24px, 5vw, 88px);
  }
  .stage-ui .wt-sub {
    left: clamp(24px, 5vw, 88px);
    bottom: 40px;
  }
  .stage-title {
    top: 25svh;
    bottom: auto;
    left: clamp(24px, 5vw, 88px);
    right: auto;
    max-width: clamp(480px, 45vw, 680px);
  }
  .stage-title h1 {
    font-size: clamp(5.5rem, 8.6vw, 8.5rem);
  }
  .stage-logline {
    font-size: var(--t-15);
    line-height: var(--lh-15);   /* the step up in size takes the step up in leading */
  }
}

@media (hover: hover) and (pointer: fine) {
  .stage-enter:hover {
    border-color: var(--accent);
  }
}

@media (max-width: 430px) {
  .stage-inner,
  .stage-figure {
    left: 68%;
  }
  .stage-ui .wt-sub {
    bottom: 72px;
  }
  .stage-ui .wt-bonds .hint {
    font-size: 0;
  }
  .stage-ui .wt-bonds .hint::after {
    content: "Choose a POV";
    font-size: var(--t-11);
    line-height: var(--lh-11);
  }
}

/* Phone composition: the receding episode is scenery, not a modal. On narrow
   screens its old 50vw x 33svh crop sat above the emerged figure and covered
   Vio's face. Keep the same "walks out of the episode" relationship, but make
   the card genuinely secondary: smaller and one layer behind the figure.
   These values mirror the mobile frame's final clip below, so the playing card
   does not jump when its live overlay fades in. The desktop choreography starts
   at 1100px and is intentionally untouched. */
@media (max-width: 700px) {
  .stage-mini {
    z-index: 5;
    top: 10svh;
    left: auto;
    right: 5vw;
    width: 42vw;
    height: 25svh;
    border-radius: 16px;
    padding: 10px;
  }

  @keyframes st-frame-phone {
    0%   { clip-path: inset(0 0 0 0 round 0); opacity: 1; }
    78%  { clip-path: inset(10svh 5vw 65svh 53vw round 16px); opacity: 1; }
    100% { clip-path: inset(10svh 5vw 65svh 53vw round 16px); opacity: 1; }
  }

  @keyframes st-edge-phone {
    0%, 8% { opacity: 0; clip-path: inset(0 0 0 0 round 0); }
    34%    { opacity: 1; }
    78%    { opacity: 1; clip-path: inset(10svh 5vw 65svh 53vw round 16px); }
    100%   { opacity: 1; clip-path: inset(10svh 5vw 65svh 53vw round 16px); }
  }

  @supports (animation-timeline: scroll()) {
    .stage-frame { animation-name: st-frame-phone; }
    .stage-edge { animation-name: st-edge-phone; }
  }

  @supports not (animation-timeline: scroll()) {
    .stage.is-out .stage-frame {
      clip-path: inset(10svh 5vw 65svh 53vw round 16px);
    }
    .stage.is-out .stage-edge {
      clip-path: inset(10svh 5vw 65svh 53vw round 16px);
    }
  }
}

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

/* The resting subject is always the original sharp image. WebGL is visible only
   for the brief single-subject exit/entrance between cast members. */
.stage-webgl {
  position: absolute;
  inset: 0;
  z-index: 6;
  width: 100%;
  height: 100%;
  opacity: 0;
  pointer-events: none;
}
.stage.is-webgl-switching .stage-webgl {
  opacity: 1;
}
.stage.has-stage-webgl .stage-back {
  transform:
    translate3d(
      calc(var(--stage-depth-x, 0px) * -0.45),
      calc(var(--stage-depth-y, 0px) * -0.45),
      0
    )
    scale(1.015);
}
.stage.has-stage-webgl .stage-inner,
.stage.has-stage-webgl .stage-figure {
  transform:
    translate3d(
      calc(var(--stage-depth-x, 0px) * 0.34),
      calc(var(--stage-depth-y, 0px) * 0.34),
      0
    );
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .stage-webgl { display: none; }
  .stage.has-stage-webgl .stage-back,
  .stage.has-stage-webgl .stage-inner,
  .stage.has-stage-webgl .stage-figure { transform: none; }
}


/* ---- Hit areas on the site layer -------------------------------------------
   Footer links measured 43x16 / 32x16 / 118x16 and the pricing link 185x23,
   all well under 44px. Padding raises the target; the matching negative margin
   keeps the visual rhythm of the line they sit in unchanged, so this buys the
   touch area without moving a single pixel of type. */
.foot-links a,
.safety-link {
  display: inline-block;
  padding: 14px 6px;
  margin: -14px 0;
}


/* ==========================================================================
   THE EXIT LINE — copy for the moment he is out of the shot.

   The fault it fixes: .stage-ui and .stage-title are both animated to opacity 0
   by 46svh, which is exactly when the emergence completes. From there to the
   end of the section the only words on the page were "Tell them once. / They
   never forget." — the product claim, with nothing naming the thing the visitor
   just watched happen. The one move the stage makes had no caption.

   WHY IT IS IN THE TAIL AND NOT IN THE PIN. Measured, not assumed: .stage-tail
   is z-index 3 over a z-index-auto sticky pin and paints an opaque --paper
   background, so it is already climbing over the bottom of the pinned frame
   from the first pixels of scroll — its top edge sits at 900 - scrollY. A
   caption placed low in the pin is sliced by that edge before the emergence
   even finishes. The pin area that survives to 46svh is the column left of the
   collapsed card (its left edge is 24vw), about 270px wide at 1440 once the
   page gutter is taken off, which is not a column display type fits in. The
   overlap is deliberate — the tail line rising as the figure rises out IS the
   transition — so the caption joins it rather than fighting it.

   Two registers, one block. The caption is 40% the size of the claim, set in
   --text-lo, and carries the only italic and the only accent in the pair. The
   claim stays the largest, darkest thing on the screen. Read in order it is
   what happened, then what it means.
   ========================================================================== */

.stage-tail {
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: clamp(30px, 4.6vh, 58px);
  /* MUST equal `.stage`'s height minus the 100svh pin (see the .stage height
     rule) — shorter leaves a hole below the band with the figure showing
     through. 44svh pairs with the 144svh mobile section. */
  height: 44svh;
}
@media (min-width: 1100px) { .stage-tail { height: 52svh; } }

.stage-exit {
  margin: 0;
  max-width: 22ch;
  font-family: var(--font-display);
  /* step h3. Was "opsz" 144 on a 22-36px line — the widest optical mismatch on
     the site: hairlines drawn for a 144pt headline, rendered at a third of it,
     which is why this lead-in read thin and washed rather than quiet. It is
     quiet because it is small and --text-lo, not because its stems break up. */
  font-weight: var(--fw-h3);
  font-variation-settings: "opsz" var(--opsz-h3), "wght" var(--fw-h3);
  font-size: clamp(1.375rem, 0.75rem + 2.1vw, 2.25rem);
  line-height: var(--lh-h3);
  letter-spacing: var(--tr-h3);
  color: var(--text-lo);
}
/* Per-line, not `.stage-exit > span`: that selector outspecifies the kicker's
   own class and would hold it at display:block, which silently kills the
   leader rule below (a width on an inline ::before does nothing). */
.stage-exit-l1,
.stage-exit-l2 { display: block; }

/* The site's label register, not a third one: same mono 11/600 at
   --label-tracking as .eyebrow. Three sizes carry the three levels here — 11px
   label, ~36px caption, 56px claim — and the scale's dead zone between 17 and
   40px is crossed twice, so nothing is asked to hold a level by weight alone. */
.stage-exit-kicker {
  display: flex;
  align-items: center;
  gap: 12px;
  font-family: var(--font-mono);
  font-size: var(--t-11);
  line-height: var(--lh-11);
  font-weight: 600;
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 16px;
}
/* A leader rule, not an ornament: it puts an edge one step outside the text
   block, so the caption's indented second line has something to be measured
   against instead of reading as ragged setting. */
.stage-exit-kicker::before {
  content: "";
  flex: none;
  width: 24px;
  height: 1px;
  background: currentColor;
}

.stage-exit-l1 b { font-weight: inherit; }
/* the step right: the block leans the way he is walking */
.stage-exit-l2 { padding-left: 1.6ch; }
@media (min-width: 1100px) { .stage-exit-l2 { padding-left: 2.4ch; } }
/* the payoff word — the only italic and the only accent above the claim */
.stage-exit-l2 em {
  font-style: italic;
  color: var(--accent);
}

/* The claim keeps its own size; it must stay the biggest thing here or the two
   registers collapse into one. That size is declared once, with the rest of its
   step, at the .stage-tail .wt-display rule above — this line restated the
   identical clamp() a third time and is removed. The requirement it documents
   still holds: .stage-exit (step h3, 22-36px) must stay below the claim (step
   h1, 28-56px), which the scale now enforces by construction. */

/* ---- the reveal --------------------------------------------------------
   One line per beat, so the sentence writes itself as he leaves rather than
   arriving finished. A view() timeline, not the root scroll(): unlike the
   pinned layers above, this block is in normal flow, so its own entry into the
   viewport is the correct clock and no svh offsets have to be hand-kept in
   sync with the emergence. */
@keyframes st-exit-rise {
  from { opacity: 0; translate: 0 0.5em; }
  to   { opacity: 1; translate: 0 0; }
}
@supports (animation-timeline: view()) {
  .stage-exit > span {
    animation-name: st-exit-rise;
    animation-timeline: view(block);
    animation-fill-mode: both;
  }
  .stage-exit-kicker { animation-range: entry 4% entry 42%; }
  .stage-exit-l1     { animation-range: entry 12% entry 56%; }
  .stage-exit-l2     { animation-range: entry 22% entry 72%; }
}

/* Reduced motion holds the stage on its FIRST frame — the episode still playing
   full-bleed, nobody out of anything — so the caption would be describing a
   step that, in this mode, never happens. The claim below it still stands on
   its own, which is why only the caption goes. */
@media (prefers-reduced-motion: reduce) {
  .stage-exit { display: none; }
  .stage-tail { height: 45svh; }
}

/* The section box has to grow with the tail or the tail overflows it and the
   sticky pin releases early: .stage is a hard height, and its content is
   exactly pin (100svh) + tail. */
/* Section height minus the 100svh pin must EQUAL .stage-tail's height — the
   tail is what fills the flow after the pin, and any shortfall is a hole with
   the pinned figure showing through it. Mobile trims both by 14svh to close
   the near-empty stretch after the hero; desktop keeps the original pairing
   because its scrub ranges (0–52svh) are tuned to the 52svh release point. */
.stage { height: 144svh; }
@media (min-width: 1100px) { .stage { height: 152svh; } }
@media (prefers-reduced-motion: reduce) { .stage { height: 100svh; } }


/* ==========================================================================
   POLISH PASS — the icon layer, accent discipline, and card elevation.

   Three faults, all of them the same fault wearing different clothes: a
   decision that was made once and then not carried through the rest of the
   site.

   (1) GLYPHS PRESSED INTO SERVICE AS ICONS. product-demo.css already states
       the rule — "Real icons, not characters... 20px box, 1.6 stroke, round
       caps, currentColor" — and applies it to exactly one component, the
       skillbar. Everywhere else the page still draws its chrome with
       typography: U+2606 as a save button, U+2302/U+25C7/U+25CE as a tab bar,
       U+2039 as a back chevron, U+2041 as a Memory mark, U+25CF x3 as cellular
       bars, and U+F8FF — an Apple PRIVATE USE codepoint — as the App Store
       mark, which is a tofu box on every non-Apple device that visits.
       An icon set is told apart by its OUTLINE at 20px. Glyphs from six
       different Unicode blocks have six different optical weights, six
       different vertical centres, and no shared stroke; that mismatch is most
       of what "not quite premium" is made of on this page.

   (2) ACCENT SPENT FIVE TIMES ON THE FIRST SCREEN. The brand's own rule is at
       most two per viewport. The first viewport was carrying: the nav's
       gradient dot, the nav CTA fill, "S1 · E03", the italic LIFE, and the
       active bond ring. The dot goes first — the brand mark is the Fraunces
       wordmark, and a glowing gradient dot beside it is a second, invented
       logo. The episode number demotes to secondary text. What is left is the
       pair the page actually wants: the title's payoff word, and the one
       primary action.

   (3) CARDS RINGED RATHER THAN LIFTED. .card, .plan and .cta-band each drew a
       1px enclosing border on a surface only 3% lighter than its ground, so
       nothing sat above anything — the outline was doing the work elevation
       should do. White on grey plus a directional shadow states the same
       boundary and states a Z position with it. Hairlines stay, but only
       between things, never around them.
   ========================================================================== */

/* ---- the icon grid ------------------------------------------------------
   One box, one stroke, currentColor. Every icon on the site inherits its
   colour from the type it sits beside, which is the whole reason a monoline
   set holds together where a pile of glyphs cannot. */
.ico {
  width: 20px;
  height: 20px;
  flex: none;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.ico--sm { width: 16px; height: 16px; stroke-width: 1.7; }
.ico--fill { fill: currentColor; stroke: none; }

/* The one check mark, as a mask so it takes a background colour rather than a
   stroke colour — pricing needs it inside a ::before, where no element exists
   to hang an SVG on. */
:root {
  --ico-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.2 10.7l3.9 3.7L15.8 5.9'/%3E%3C/svg%3E");
}

/* ---- (1a) the App Store mark -------------------------------------------
   NOT an Apple logo, deliberately. The old U+F8FF is Apple's private-use
   codepoint: it draws the mark on Apple hardware and a tofu box everywhere
   else, so a third of the visitors to a page whose one conversion is "get the
   app" were seeing an empty rectangle on the button. Redrawing the mark
   ourselves is not the fix either — Apple ships official "Download on the App
   Store" badge artwork and requires it be used unmodified.
   TODO(blocking, with the App Store id): replace this whole badge with Apple's
   supplied SVG. Until then it is a plain download mark, which is honest about
   what it is and legible on every platform. */
.appstore-badge .glyph {
  width: 21px;
  height: 21px;
  flex: none;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ---- (2) accent discipline on the first screen ------------------------- */
/* The wordmark stands alone. No dot, no glow, no second mark. */
.nav-logo .dot { display: none; }
/* Tracking consolidated into the single .nav-logo declaration near the top of
   this file. It was set twice — 0.01em there and -0.005em here — and only this
   one was ever on screen. The wordmark is the one Fraunces on the page that is
   not on a display step: it is a mark, set once at one size, and it keeps its
   own tracking. */
.nav-logo {
  gap: 0;
}
/* An episode number is metadata, not a call to action. It reads as a label
   from its case, its tracking and its face — it never needed the accent, and
   spending the accent here is what pushed the first screen to five. */
.stage-episode span { color: var(--text-lo); }

/* ---- (2b) the hero's only action --------------------------------------- */
/* 12px was the smallest type in the entire title card and it was on the one
   thing the visitor is meant to press. 13px with the label tracking the rest
   of the site's UI text uses, and a hover that moves — a button that does not
   acknowledge the pointer reads as an image of a button. */
.stage-enter {
  padding: 0 20px;
  font-size: var(--t-13);
  letter-spacing: 0.02em;
  background: color-mix(in srgb, var(--surface) 14%, transparent);
  border-color: rgba(246, 247, 248, 0.28);
  color: #f6f7f8;
}
.stage-enter span {
  transition: translate 180ms var(--ease-dive);
}
@media (hover: hover) and (pointer: fine) {
  .stage-enter:hover {
    background: color-mix(in srgb, var(--surface) 22%, transparent);
    border-color: rgba(246, 247, 248, 0.5);
  }
  .stage-enter:hover span { translate: 0 2px; }
}
@media (prefers-reduced-motion: reduce) {
  .stage-enter span { transition: none; }
}

/* ---- (3) elevation instead of enclosure -------------------------------- */
/* One shadow recipe for the whole site layer, offset downward from the same
   implied light source the app column already uses. Two layers: a tight one
   that reads as contact, a wide one that reads as height. */
:root {
  --lift-1: 0 1px 2px rgba(16, 17, 20, 0.04),
            0 8px 24px -8px rgba(16, 17, 20, 0.1);
  --lift-2: 0 1px 2px rgba(16, 17, 20, 0.05),
            0 18px 48px -16px rgba(16, 17, 20, 0.16);
}
.card,
.plan,
.cta-band {
  border: 0;
  box-shadow: var(--lift-1);
  transition: box-shadow var(--duration-slow) var(--ease-dive),
              translate var(--duration-slow) var(--ease-dive);
}
.cta-band { box-shadow: var(--lift-2); }
.card:hover {
  box-shadow: var(--lift-2);
  translate: 0 -2px;
}
/* The featured plan is raised, not outlined. It keeps a single accent ring
   because it is the one card making a claim about itself — but the ring now
   sits on top of real elevation instead of standing in for it. */
.plan.featured {
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 26%, transparent),
              var(--lift-2);
  translate: 0 -4px;
}
@media (max-width: 860px) {
  .plan.featured { translate: 0 0; }
}
@media (prefers-reduced-motion: reduce) {
  .card, .plan, .cta-band { transition: none; }
  .card:hover { translate: 0 0; }
}

/* ---- pricing: the feature mark ------------------------------------------
   U+25C6 BLACK DIAMOND was a geometric shape doing an icon's job — the exact
   pattern this pass exists to remove, and the one place on the site where the
   accent was being spent fourteen times on a single screen. A check mark says
   "included", which is the only thing a bullet in a plan list ever means, and
   it is set in the muted ramp so the plan's own accent stays on its CTA. */
.plan li {
  align-items: flex-start;
  gap: 12px;
}
.plan li::before {
  content: "";
  flex: none;
  width: 16px;
  height: 16px;
  margin-top: 3px;
  font-size: inherit;
  background: var(--text-faint);
  -webkit-mask: var(--ico-check) center / 16px 16px no-repeat;
  mask: var(--ico-check) center / 16px 16px no-repeat;
}
.plan.featured li::before { background: var(--accent); }

/* ---- pricing: the comparison table --------------------------------------
   U+2713 renders from a different font on every platform and lands at a
   different size and baseline in each. The mask draws the same mark as the
   plan list, so "included" looks identical in both places — which is the
   point of a comparison table. */
.compare-table .tick,
.compare-table .nope {
  display: inline-block;
  width: 16px;
  height: 16px;
  vertical-align: -3px;
}
.compare-table .tick {
  background: var(--accent);
  -webkit-mask: var(--ico-check) center / 16px 16px no-repeat;
  mask: var(--ico-check) center / 16px 16px no-repeat;
}
/* Absence is a rule, not a character. An em dash sets at a different width in
   every column; a 10px hairline is the same absence every time. */
.compare-table .nope {
  position: relative;
}
.compare-table .nope::after {
  content: "";
  position: absolute;
  left: 3px;
  right: 3px;
  top: 50%;
  height: 1px;
  background: var(--border-strong);
}
.compare-table tbody tr:hover td { background: rgba(16, 17, 20, 0.02); }
.compare-table td:first-child { color: var(--text-hi); }

/* ---- docs: the sidebar earns its stickiness -----------------------------
   .docs-nav a.active was styled and never applied — nothing in the site set
   the class, so the rail was six identical grey links for the whole page. It
   is a scrollspy now (see the inline script in docs.html), and the rail reads
   as a position rather than as a list. */
.docs-nav {
  border-left: 1px solid var(--border);
  padding-left: 2px;
}
.docs-nav a {
  border-left: 2px solid transparent;
  margin-left: -3px;
  padding: 10px 0 10px 14px;
}
.docs-nav a.active {
  color: var(--text-hi);
  font-weight: 600;
  border-color: var(--accent);
}
@media (max-width: 860px) {
  .docs-nav {
    display: flex;
    flex-wrap: wrap;
    gap: 0 4px;
    border-left: 0;
    border-bottom: 1px solid var(--border);
    padding: 0 0 4px;
    position: static;
  }
  .docs-nav a {
    border-left: 0;
    border-bottom: 2px solid transparent;
    margin-left: 0;
    padding: 10px 10px 8px;
  }
}
/* The docs body was body-size grey prose at the site's editorial leading with
   no measure on it — at 1120px minus a 220px rail that is a 95-character
   line, well past the 75 the eye can track back from. */
.docs-content p,
.docs-content li { max-width: 68ch; }
/* Type moved up to the single .docs-content h2 declaration (step h3). What this
   block contributes is the rule above each section and the colour. */
.docs-content h2 {
  color: var(--text-hi);
  border-top: 1px solid var(--border);
  padding-top: 22px;
}
.docs-content h2:first-child { border-top: 0; padding-top: 0; }
.docs-content ol { counter-reset: docstep; list-style: none; padding-left: 0; }
.docs-content ol li {
  counter-increment: docstep;
  position: relative;
  padding-left: 34px;
  margin-bottom: 12px;
}
.docs-content ol li::before {
  content: counter(docstep);
  position: absolute;
  left: 0;
  top: 1px;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text-lo);
  font-family: var(--font-mono);
  font-size: var(--t-11);
  line-height: var(--lh-flush);
  font-weight: 600;
}
.docs-content a:not(.safety-link) {
  color: var(--text-hi);
  text-decoration: underline;
  text-decoration-color: var(--border-strong);
  text-underline-offset: 3px;
  transition: text-decoration-color var(--duration-fast) ease;
}
.docs-content a:not(.safety-link):hover { text-decoration-color: var(--accent); }

/* ---- page header: the two subpages had no top edge ----------------------
   index.html opens on a full-bleed episode; pricing and docs opened on 96px
   of nothing and then a centred headline, which is the generic template
   opening this site otherwise avoids. The rule under the eyebrow is the same
   structural device .section-head already uses, so the subpages join the
   system instead of each inventing a top. */
.page-header {
  max-width: var(--max-width);
  padding: 128px 24px 8px;
  text-align: left;
}
.page-header .eyebrow { margin-bottom: 20px; }
/* Type moved up to the single .page-header h1 declaration near the top of this
   file (step display). What stays here is the only thing this block ever
   contributed that the other did not: the measure. */
.page-header h1 {
  max-width: 16ch;
}
.page-header .lead {
  margin: 22px 0 0;
  max-width: 52ch;
  text-wrap: pretty;
}
@media (max-width: 600px) {
  .page-header { padding-top: 104px; }
}

/* ---- three corrections, from rendering the page ------------------------
   (a) The comparison table drew 21 accent-purple checks on one screen. The
       brand's own rule is two per viewport, and the table does not need the
       accent to say "yes" — a dark check says it, and the moment every yes is
       purple the purple stops meaning anything. The accent is left where it
       earns its keep on this page: the featured plan's list and its CTA.
   (b) .plan .price is 34px and had no tracking, so "$9.99" set as "$9 . 99" —
       mono figures at display size are exactly the case the -0.02em rule
       exists for. That value now lives in the single .plan .price declaration
       above; the reasoning stays here because it is why the value exists.
   (c) The lead's descender was landing on the "Most popular" badge, which
       overhangs its card by 13px. */
.compare-table .tick { background: var(--text-hi); }
.page-header { padding-bottom: 28px; }




/* ---- the closing CTA on the site layer ---------------------------------
   .cta-band ships with a <p> carrying the gap above its button; this band has
   no paragraph, so the badge states its own. */
.cta-band .eyebrow { margin-bottom: 16px; }
.cta-band .appstore-badge { margin: 30px auto 0; }


/* ==========================================================================
   CAST FAN OVER THE CLOSING CARD.
   The fan drops, and the white closing panel slides up behind it so the cards
   stand ON the panel instead of stopping politely above it.

   Two things make the overlap real rather than a coincidence of margins:

   (1) STACKING. #cast takes a z-index above #download. Without it the closing
       section wins on DOM order alone and paints its white panel over the
       bottom of every card — the exact inverse of what is wanted.
       The panel is a SIBLING of #cast, not a child, so #cast's overflow never
       had any say over where the panel paints. What that overflow does decide
       is whether the cards can reach it at all — see (3).

   (2) HEADROOM IN THE PANEL. The overlap is deeper than .cta-band's 56px top
       padding, so without extra padding the cards would land on top of the
       "Only on iOS" label. The panel opens its top so the overlap lands on
       empty white, which is the only part of it that can afford to be covered.

   The fan is wider than the panel (1440 vs 1120), so the two outer cards
   overhang both of its edges. That is kept: a layer that stops exactly at the
   edge of the layer beneath it reads as a coincidence; one that runs past it
   reads as being in front.
   ========================================================================== */
#cast {
  position: relative;
  z-index: 2;
  margin-top: 72px;
  /* (3) THE CARDS HAVE TO BE ABLE TO LEAVE THE SECTION.
     #cast carried `overflow: clip`, which exists because the two end cards
     swing 15-35px past the section box horizontally once they are rotated
     about an origin below themselves. But it clips BOTH axes, and the four
     outer cards are translated 64-120px downward — so they were being sliced
     flat at the section's bottom edge, all four at the same y, exactly where
     the panel begins. That does not read as a card lying on a panel; it reads
     as a card the panel cut off.
     Only the horizontal clip was ever wanted. `overflow-x: clip` with
     `overflow-y: visible` is a legal pair (unlike hidden + visible, which the
     UA silently rewrites to auto and would add a scrollbar). */
  overflow: visible;
  overflow-x: clip;
  overflow-y: visible;
}
#download {
  position: relative;
  z-index: 1;
  /* Shallower than it looks: the cards now genuinely hang ~100px below the
     section box on their own, so the panel only has to come up far enough to
     meet them. Pulling the full 96px on top of that buried a third of the fan. */
  margin-top: -40px;
}
/* Deep enough to clear the lowest card, so the overlap always lands on empty
   white and never on the "Only on iOS" label. */
#download .cta-band { padding-top: 172px; }

@media (max-width: 900px) {
  /* the fan is shorter here, so the same numbers would eat proportionally more
     of it — the overlap scales with the cards, not with the viewport */
  #cast { margin-top: 56px; }
  #download { margin-top: -32px; }
  #download .cta-band { padding-top: 128px; }
}
@media (max-width: 600px) {
  /* at 290px of fan height and a 44px outer drop there is barely any overhang
     to work with, so the overlap becomes a touch rather than a lean */
  #cast { margin-top: 36px; }
  #download { margin-top: -20px; }
  #download .cta-band { padding-top: 84px; }
}

/* ---- Pre-launch "Coming soon" dialog (assets/coming-soon.js) ----
   Opened by every Download / App Store control until the app ships. Reuses
   .cta-band's surface, radius and type so it reads as the same component,
   just lifted off the page. */
.coming-soon {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: grid;
  place-items: center;
  padding: 24px;
}
.coming-soon__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(10, 10, 12, 0.55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.coming-soon__panel {
  position: relative;
  width: min(440px, 100%);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  padding: 36px 32px 32px;
  text-align: center;
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35);
  animation: coming-soon-in 220ms ease-out;
}
.coming-soon__panel .eyebrow { margin-bottom: 12px; }
.coming-soon__panel h2 {
  font-family: var(--font-display);
  font-weight: var(--fw-h2);
  font-variation-settings: "opsz" var(--opsz-h2), "wght" var(--fw-h2);
  font-size: clamp(24px, 3.5vw, 32px);
  letter-spacing: var(--tr-h2);
  line-height: var(--lh-h2);
  margin: 0 0 10px;
  color: var(--text-hi);
}
.coming-soon__panel p {
  color: var(--text-lo);
  margin: 0 0 24px;
  text-wrap: pretty;
}
.coming-soon__actions {
  display: flex;
  gap: 12px;
  justify-content: center;
  flex-wrap: wrap;
}
.coming-soon__close {
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  border: 0;
  border-radius: 999px;
  padding: 12px 22px;
  background: var(--accent);
  color: var(--accent-ink);
}
.coming-soon__close:hover { filter: brightness(1.08); }
.coming-soon__close:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.coming-soon[hidden] { display: none; }
.coming-soon-open { overflow: hidden; }
@keyframes coming-soon-in {
  from { opacity: 0; transform: translateY(8px) scale(0.98); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .coming-soon__panel { animation: none; }
}
