/* Generated at import from pandem/chat/render/design_tokens.json — the app
   theme's own artifact. Do not hand-edit values here or in a copy. */
:root {
  --ink900: #0e1116;
  --ink800: #15191f;
  --line: #2a323d;
  --white: #f5f7fa;
  --slate200: #e6eaf0;
  --slate400: #8a93a2;
  --ground: 7, 48, 58;
  --cool: 110, 91, 216;
  --peach: #f7b399;
  --spotlight-from: #f1b5c9;
  --spotlight-to: #f8c99d;
  --spotlight-ink: #33241f;
  --radius-md: 12px;
  --radius-lg: 18px;
}

/* The website's single stylesheet — the app's design system ported to the web.
   Every value here is a port of a real token, not a fresh choice: palette.ts for
   colour, tokens.ts for type/spacing/radii, BrandGlow.tsx for the field wash,
   BrandDot.tsx for the disc that ends the wordmark. Nothing is invented; where the
   web forced an adaptation it is commented with what forced it.

   One stylesheet, not four inline copies: `/site.css` is a route like the pages, so
   the chrome has one owner and a change cannot land on three files out of four. */

/* ── Typeface ────────────────────────────────────────────────────────────────
   Hanken Grotesk, the app's only family, served from pandem/chat/render/fonts/
   (already inside the package the Dockerfile copies, so no duplicate asset).
   RN needs one family PER WEIGHT because it cannot synthesise custom-font
   weights; the browser can, so the three faces collapse into one family with
   three real weights — same faces, fewer names. */
@font-face {
  font-family: "Hanken Grotesk";
  src: url("/fonts/HankenGrotesk-Medium.ttf") format("truetype");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Hanken Grotesk";
  src: url("/fonts/HankenGrotesk-Bold.ttf") format("truetype");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Hanken Grotesk";
  src: url("/fonts/HankenGrotesk-ExtraBold.ttf") format("truetype");
  font-weight: 800;
  font-style: normal;
  font-display: swap;
}

:root {
  color-scheme: dark;

  /* Raw token values (--ink900, --ground, --peach, the spotlight trio, radii)
     arrive from a :root block the route prepends, generated from
     design_tokens.json — see site.py. This file keeps only the web
     ADAPTATIONS, each with the measurement that justifies it. */

  /* Links. The app has no link token at all — accent.azure is the chat PLACED
     stage band — so this is an adaptation, and it is MEASURED, not eyeballed.
     accent.azure itself (#4d84d5) clears 4.5:1 on surface.base at 5.02 but only
     4.24 on the whisper wash and 3.74 on the full one, and links are body-size
     text, so 1.4.3 wants 4.5 wherever they land. This holds accent.azure's hue
     (215.7deg) and saturation (0.618) exactly and lifts lightness by 0.06, which
     is the smallest step that clears 4.5 on EVERY surface this site has:
     base 6.20, raised 5.78, whisper wash 5.24, full wash 4.61. */
  --azure: #6695db;
  --azure-hover: #9bbae7;

  /* Type roles ported from tokens.ts. The app's absolute sizes are phone-at-3x
     (body 14/20); on a desktop reading distance those are too small, so the
     ROLES, weights and letter-spacings carry over exactly and the sizes are
     re-based for the medium. Fixed rem, not fluid: product-register rule. */
  --t-body: 1.0625rem; /* 17px */
  --t-lead: 1.1875rem; /* 19px */
  --t-h2: 1.1875rem;
  --t-h1: 2.5rem;
  --t-label: 0.6875rem;
  --t-small: 0.875rem;
}

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

html {
  background: var(--ink900);
  -webkit-text-size-adjust: 100%;
}

body {
  position: relative;
  min-height: 100vh;
  background: transparent;
  color: var(--slate200);
  font-family: "Hanken Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI",
    Roboto, Helvetica, Arial, sans-serif;
  font-weight: 500;
  font-size: var(--t-body);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ── The field wash ──────────────────────────────────────────────────────────
   A port of GlowBackdrop + BrandGlow: one full-bleed vertical gradient in
   brand.ground, brightest at the top (the horizon), falling to surface.base.
   The three stops are BrandGlow's STOPS array verbatim — one colour at alphas
   1 / 0.45 / 0 — and the intensity ladder is the same pure opacity scale, so
   `full` (the door) and `whisper` (Settings, where /privacy and /terms are
   linked from in the app) are the same k values the app uses.

   ONE deliberate deviation. In the app the wash is viewport-height behind a
   viewport-height screen. A legal document is several thousand pixels tall, and
   `fixed` would drag the bright horizon down the whole scroll — text would sit
   on the brightest part of the wash the entire way, which is both wrong-looking
   and where the azure link colour stops clearing 4.5:1. Anchoring it to the top
   of the DOCUMENT instead reproduces exactly what the app shows on a screenful,
   and lets the long copy below settle onto clean surface.base. */
body::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;
  z-index: 0;
  pointer-events: none;
}

body.wash-full::before {
  background: linear-gradient(
    180deg,
    rgba(var(--ground), 1) 0%,
    rgba(var(--ground), 0.45) 44%,
    rgba(var(--ground), 0) 100%
  );
}

body.wash-whisper::before {
  background: linear-gradient(
    180deg,
    rgba(var(--ground), 0.65) 0%,
    rgba(var(--ground), 0.2925) 44%,
    rgba(var(--ground), 0) 100%
  );
}

body > * {
  position: relative;
  z-index: 1;
}

/* ── Frame ───────────────────────────────────────────────────────────────── */
header.site,
main,
footer {
  /* 38rem at 17px lands ~71ch — inside the 65–75ch reading band. */
  max-width: 38rem;
  margin: 0 auto;
  padding-left: 1.5rem;
  padding-right: 1.5rem;
}

header.site {
  padding-top: 2rem;
}

main {
  padding-top: 3rem;
  padding-bottom: 4rem;
}

/* ── The wordmark and its disc ───────────────────────────────────────────────
   BrandDot.tsx: the brand full stop is a DISC, not the font's period — Hanken
   Grotesk ExtraBold draws a hard-edged square there. The three ratios are that
   glyph's own metrics (advance 240, lsb 48, ink 144x137 at unitsPerEm 1000), so
   the disc lands in the box the square vacated and nothing reflows.

   The RN version needs measured constants to sit the dot on the baseline
   because RN seats inline views below it. CSS does not have that bug: an
   inline-block's bottom margin edge sits on the baseline, which is where the
   period's ink box already starts (y 0…137). So the seating is free here. */
.wordmark {
  font-size: 1.4rem;
  font-weight: 800;
  letter-spacing: -0.026em; /* -0.3 * (size/16), per Wordmark.tsx */
  color: var(--white);
  text-decoration: none;
  display: inline-block;
}

.dot {
  display: inline-block;
  width: 0.1425em;
  height: 0.1425em;
  border-radius: 50%;
  background: var(--peach);
  margin-left: 0.048em; /* the period's left side bearing */
  margin-right: 0.0495em; /* balance of its 0.24em advance */
  vertical-align: baseline;
}

/* ── Type ────────────────────────────────────────────────────────────────── */
h1 {
  color: var(--white);
  font-size: var(--t-h1);
  font-weight: 800;
  line-height: 1.08;
  letter-spacing: -0.025em;
  margin-bottom: 1.25rem;
  text-wrap: balance;
}

h2 {
  color: var(--white);
  font-size: var(--t-h2);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.35;
  margin: 2.5rem 0 0.65rem;
  text-wrap: balance;
}

p {
  margin-bottom: 1rem;
  text-wrap: pretty;
}

.lead {
  font-size: var(--t-lead);
  color: var(--white);
  line-height: 1.55;
}

.muted {
  color: var(--slate400);
}

strong {
  font-weight: 700;
  color: var(--white);
}

/* The deletion page's mailto is wrapped in <strong> for Play's "prominently
   featured" clause. Without this it inherits the white strong colour and stops
   looking like a link at all — which is the one affordance that page exists for. */
a strong {
  color: inherit;
}

ul {
  margin: 0 0 1rem 1.15rem;
}

li {
  margin-bottom: 0.5rem;
  padding-left: 0.35rem;
}

li::marker {
  color: var(--slate400);
}

/* The homepage's "how it works" list is the one place the peach mark belongs:
   these are the player's own actions, which is the hue's meaning (me / value). */
.steps li::marker {
  color: var(--peach);
}

a {
  color: var(--azure);
  text-decoration-color: rgba(102, 149, 219, 0.45);
  text-underline-offset: 0.18em;
  /* motion.duration.fast is 120ms; 160 is inside the app's fast/base band and
     inside the product register's 150-250ms. State feedback only. */
  transition: color 160ms ease-out, text-decoration-color 160ms ease-out;
}

a:hover {
  color: var(--azure-hover);
  text-decoration-color: currentColor;
}

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

/* type.label — the app's uppercase micro-label role (Bold 10, +0.8 tracking). */
.claim {
  color: var(--slate400);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-top: 2.75rem;
}

/* In the footer the claim is small print, not a closing statement: the body
   position's clearance would gap it away from the border it now sits under. */
footer .claim {
  margin-top: 0;
  margin-bottom: 0.85rem;
}

/* ── The one call-to-action button ───────────────────────────────────────────
   The app's spotlight CTA (Button variant="spotlight"): gradient and ink arrive
   from the emitted tokens (spotlightFrom -> spotlightTo, ink spotlightInk —
   dark-on-light, 9.4:1 on the gradient's darker stop, far clear of AA), radius
   from the emitted radii.md. Used by the invite landing page's "Open in the
   Pandem app" link (#319). */
.button {
  display: inline-block;
  background: linear-gradient(135deg, var(--spotlight-from) 0%, var(--spotlight-to) 100%);
  color: var(--spotlight-ink);
  font-weight: 700;
  text-decoration: none;
  border-radius: var(--radius-md);
  padding: 0.85rem 1.6rem;
}

.button:hover {
  filter: brightness(1.05);
}

/* ── The one raised surface ──────────────────────────────────────────────────
   Surface.tsx's neutral raised card: surface.raised + radius + hairline. It is
   opaque on purpose — it is the only content block that sits high enough in a
   document to meet the bright end of the wash, and an opaque ink800 ground is
   what keeps the azure mailto link inside it at 4.65:1. */
.action {
  background: var(--ink800);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 1.5rem 1.5rem 0.5rem;
  margin: 1.75rem 0 2rem;
}

.action h2 {
  margin-top: 0;
}

/* ── Footer ──────────────────────────────────────────────────────────────── */
footer {
  border-top: 1px solid var(--line);
  padding-top: 1.5rem;
  padding-bottom: 3rem;
  color: var(--slate400);
  font-size: var(--t-small);
}

footer nav {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  margin-bottom: 0.85rem;
}

footer a {
  color: var(--slate400);
}

footer a:hover {
  color: var(--slate200);
}

/* ── Small screens ───────────────────────────────────────────────────────── */
@media (max-width: 30rem) {
  :root {
    --t-h1: 1.95rem;
  }
  header.site,
  main,
  footer {
    padding-left: 1.25rem;
    padding-right: 1.25rem;
  }
  main {
    padding-top: 2.25rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  a {
    transition: none;
  }
}

/* ── The scroll page (spec 2026-08-16, one-phone revision 2026-08-16) ────────
   ONE permanent phone (owner ruling: "the ui it displays changes in line with
   the text as you scroll"): a single sticky device on the LEFT holds the full
   screen stack, and every copy block that reaches mid-viewport swaps what it
   shows. Class-toggle crossfades everywhere EXCEPT the market pan, the
   page's ONE owner-sanctioned scroll-driven element (#393 re-ruling) — its
   scrub rules live with the pan styles, not here; the crossfade shape is
   what stays smooth in Safari everywhere else. With JS absent the phone
   shows the matches board and all copy reads in order. */
main.scroll {
  position: relative;
  max-width: none;
  padding: 0;
}

/* the wordmark rides the whole journey (owner, QA round 6) and floats over
   the page rather than sitting in flow — which is also what lets the sticky
   stage start at the document top with no dead scroll (QA round 5) */
body.landing header.site {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
  /* the wordmark tracks the JOURNEY's container (same 64rem cap and gutter),
     so it sits in line with the phone column and scales with it across
     viewport sizes — the true viewport corner drifted too far left on wide
     screens (owner, QA rounds 9-10) */
  max-width: 64rem;
  padding-left: 1.5rem;
  padding-right: 1.5rem;
}

/* ── The permanent stage (#393 owner invariant) ──────────────────────────────
   The Pandem logo and the phone are visible AT ALL TIMES, at every width.
   The old ending — sticky release, footer pushing the phone off, wordmark
   yielding — is gone: the FOOTER JOINS THE JOURNEY instead. The copy column
   runs on by the footer's measured height (--footer-run, set by the script),
   the footer pulls up into that run and scrolls in UNDER the device, and at
   the terminal frame the device breathes down (--footer-scale, symmetric
   with the phone hero breathe) so the whole footer seats beneath the bezel.
   main paints above the footer but is transparent to the pointer, so the
   footer's links stay clickable through the empty copy column. The tradeoff:
   this also disables text selection across main — accepted, because it's
   what keeps the footer's links clickable through the overlaying column,
   and the journey's copy is captions, not selectable prose. */
.journey .copy {
  padding-bottom: var(--footer-run, 14rem);
}

body.landing main.scroll {
  z-index: 2;
  pointer-events: none;
}

body.landing footer {
  margin-top: calc(-1 * var(--footer-run, 14rem));
}

html.js body.landing .device {
  transform-origin: 50% 0;
  /* one eased class-toggle per state change, never scrubbed */
  transition: transform 450ms cubic-bezier(0.4, 0, 0.2, 1);
}

html.js body.landing .journey.footer-live .device {
  transform: scale(var(--footer-scale, 1));
}

/* the desktop ending: no terminal breathe (the script pins --footer-scale
   to 1 above 48rem) — the footer seats in the COPY column instead, the
   space the journey's own grammar leaves free, aligned to the right
   column's edge and tightened to a closing block rather than a full-width
   band. The owner read the desktop breathe as extreme; the phone keeps
   its full size there at all times. */
@media (min-width: 48.0625rem) {
  body.landing footer {
    /* the closing block sits in the COPY column: its left edge is the
       column's own start (centre + the 1.5rem gutter — the same x at
       every window width, capped container or not), its measure the
       column's. NOT percentage padding: % padding resolves against the
       VIEWPORT, which on a wide monitor exceeded the box and crushed the
       content to min-content against the right edge (owner screenshot).
       Borderless, because the element rides up over the journey's tail
       and a page-wide hairline would bisect the pinned phone. */
    border-top: 0;
    max-width: 29rem;
    margin-left: calc(50% + 1.5rem);
    margin-right: auto;
    padding: 1rem 1.5rem 1.5rem 0;
  }
  body.landing footer .claim {
    margin-bottom: 0.5rem;
  }
  body.landing footer nav {
    gap: 1rem;
    margin-bottom: 0.5rem;
  }
  body.landing footer p {
    margin-bottom: 0.35rem;
  }
}

body.landing .wordmark {
  font-size: 2.6rem;
  /* mirror the phone-to-text spacing on the logo's side (owner, QA round 11):
     the device is centred in its column, so gap(phone -> text) = column inset
     + the 3rem grid gap, and gap(logo -> phone) matches it exactly when the
     logo sits 3rem left of the container edge. The min/max clamp stops the
     logo leaving the viewport on widths where the container has less than
     3rem of margin to give. */
  margin-left: calc(-1 * min(3rem, max(0rem, (100vw - 64rem) / 2)));
}

.journey {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 3rem;
  max-width: 64rem;
  margin: 0 auto;
  padding: 0 1.5rem;
}

/* ── The journey's colour arc (owner ruling 2026-08-16) ──────────────────────
   teal horizon (the body wash above) → base → a violet bloom through the
   invite and chat beats → base → a teal bloom through the builder and win
   beats → base into the finale. FULL-BLEED on main.scroll, edge to edge like
   the horizon wash (QA round 5: hung on the width-capped .journey it read as
   a colour lane, not a field). A document-positioned gradient: the scroll
   does the animating, nothing is scrubbed. Peak alphas are MEASURED, not
   eyeballed — violet caps at 0.25, the strongest strength at which every
   text colour (slate400 included) still clears AA 4.5:1 on the composited
   ground; both peaks are pinned in the contrast test. */
main.scroll::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    rgba(var(--cool), 0) 0%,
    rgba(var(--cool), 0) 22%,
    rgba(var(--cool), 0.25) 33%,
    rgba(var(--cool), 0.25) 39%,
    rgba(var(--cool), 0) 50%,
    rgba(var(--ground), 0) 56%,
    rgba(var(--ground), 0.55) 68%,
    rgba(var(--ground), 0.55) 74%,
    rgba(var(--ground), 0) 86%
  );
}

.journey .stage {
  grid-column: 1;
  grid-row: 1;
  position: sticky;
  top: 0;
  height: 100vh;
  display: grid;
  place-items: center;
  /* clears the fixed wordmark: the device centres in the space BELOW it,
     keeping a clean gap between logo and phone (owner, QA round 6) */
  padding-top: 6.5rem;
  --device-w: min(24rem, 36vh);
}


.journey .copy {
  grid-column: 2;
  grid-row: 1;
}

/* ── The device (owner ruling 2026-08-16) ────────────────────────────────────
   The screens render bare and the PAGE draws the phone: rim, island, corner
   clip. That keeps the hardware vector-crisp at any size and tunable (or
   tinted per beat) without touching a PNG — override --rim-hi/--rim-lo to
   restyle the bezel. Everything scales off --device-w so one variable sizes
   the whole device. */
.device {
  /* polished obsidian (#391, owner pick over titanium/teal/gunmetal): the
     rim goes near-black gloss and the specular rim light brightens to carry
     the lift — backdrop glows were explored and rejected, so the finish and
     the seat shadows are ALL the lift there is */
  --rim-hi: #23262b;
  --rim-lo: #050608;
  position: relative;
  width: var(--device-w, 18rem);
  padding: calc(var(--device-w, 18rem) * 0.024);
  border-radius: calc(var(--device-w, 18rem) * 0.155);
  background: linear-gradient(
    155deg,
    var(--rim-hi) 0%,
    #0b0d10 30%,
    var(--rim-lo) 60%,
    #16181c 100%
  );
  /* the two inset lines are the specular rim; the two drops are the seat */
  box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.55),
    inset 0 -1px 1px rgba(255, 255, 255, 0.12), 0 28px 56px rgba(0, 0, 0, 0.6),
    0 6px 16px rgba(0, 0, 0, 0.45);
}

.device .viewport {
  position: relative;
  overflow: hidden;
  border-radius: calc(var(--device-w, 18rem) * 0.125);
  background: var(--ink900);
}

.device .viewport img,
.device .viewport video {
  display: block;
  width: 100%;
  height: auto;
}

/* the screen stack: the board is the base layer, every other screen sits over
   it invisibly until the observer names it */
.device .viewport img.swap,
.device .viewport video.swap,
.device .viewport div.swap {
  position: absolute;
  inset: 0;
  opacity: 0;
  /* motion.duration.moment (600ms) on motion.curves.standard [0.4,0,0.2,1],
     ported from the app's motion tokens — the slow 320ms register read as an
     abrupt cut between screens at this size (owner, QA round 9). */
  transition: opacity 600ms cubic-bezier(0.4, 0, 0.2, 1);
}

.device .viewport img.swap.active,
.device .viewport video.swap.active,
.device .viewport div.swap.active {
  opacity: 1;
}

/* the market pan (#393 owner re-ruling): the ONE scroll-driven element.
   The tall board translates under a fixed-chrome overlay; the driver in
   the page script maps the pan beat's viewport progress to the translate,
   so the viewer's own scroll sets the browsing rhythm. */
.device .viewport div.swap.pan {
  overflow: hidden;
}
.pan-tall {
  display: block;
  width: 100%;
  height: auto;
  will-change: transform;
}
.pan-chrome {
  position: absolute;
  inset: 0;
  background-image: url("/screens/market-pan-chrome.png?v=1");
  background-size: 100% 100%;
}

/* the dynamic island */
.device::after {
  content: "";
  position: absolute;
  top: calc(var(--device-w, 18rem) * 0.055);
  left: 50%;
  transform: translateX(-50%);
  width: calc(var(--device-w, 18rem) * 0.27);
  height: calc(var(--device-w, 18rem) * 0.075);
  border-radius: 999px;
  background: #04070a;
}

.hero-copy {
  min-height: 92vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.journey .step {
  /* two-speed pacing (owner, QA round 12): a step that OPENS a beat gets the
     long dwell, a continuation answers quicker — the page reads as arrival
     then response instead of a metronome. */
  min-height: 88vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  max-width: 26rem;
}

.journey .step.cont {
  min-height: 68vh;
}

/* the pan beat dwells long: its scroll span IS the market board's scrub
   range, and a standard dwell scrubbed 6300px of board through ~1.5
   screens of scroll — too fast to read anything (owner pass) */
.journey .step[data-shot="market-pan"] {
  min-height: 160vh;
}

.journey .step h2 {
  /* #393: every beat speaks in the hero's register — one line, h1 scale */
  font-size: var(--t-h1);
  font-weight: 800;
  letter-spacing: -0.025em;
  line-height: 1.15;
  margin: 0;
}

/* ── The journey on a phone (#391, revising #388; captions fixed under #393) ─
   Phone-first: ONE column. The stage is a sticky band pinned to the viewport
   top — wordmark, 1.25rem of clear air (even with the bezel-to-hero gap),
   then the device centred at 70lvh tall — and the copy beats scroll through the strip
   beneath it as invisible spacers. The phone content is the prestige item
   (owner wording): with JS the caption itself leaves the flow for a fixed
   slot under the bezel (see the html.js rules below), so nothing ever reads
   OVER the screen and the beat text is never aimed at a moving letterbox.
   Stage and copy share one grid cell so the hero sits on the first fold
   while the stage rides above it.

   The final whole-branch review found the prior geometry was calibrated
   against devtools viewports where svh equals the hardware height, which no
   real phone has — svh is the SMALL viewport (chrome expanded), only ~79% of
   an iPhone's screen. Both svh and lvh are STATIC units, so #388's rule still
   holds (Safari's collapsing chrome must not move the layout); only dvh
   jumps mid-scroll and stays banned. The load state accepted by the owner:
   the first fold shows wordmark, phone, and the hero headline's first line
   or two peeking under it (the device pass measured Safari's bar taking
   less than the modelled 21%, so the peek is more generous in practice);
   Safari's chrome collapses on first scroll, which opens the full strip
   (~200px on an 844pt phone) for the rest of the journey. */
@media (max-width: 48rem) {
  body.landing header.site {
    /* the header tracks the journey's tighter gutter */
    padding-left: 1.25rem;
    padding-right: 1.25rem;
  }
  body.landing .wordmark {
    font-size: 2.6rem;
    /* the P's left side bearing (0.06em, measured off the face): the glyph's
       INK sets the header's true left edge. #388's bezel-flush pairing is
       gone (the device is centred now), the metric is still the right one. */
    margin-left: -0.06em;
  }
  .journey {
    grid-template-columns: 1fr;
    gap: 0;
    padding: 0 1.25rem;
    /* single-sourced so the stage, the hero's seating, and the device sizing
       all read off the same two numbers (#391 final review) — #393: the strip
       only needs one line now, so the phone takes the space the sentences gave
       back (~285px wide on a 390pt phone) */
    --stage-top: calc(2rem + 1.65 * 2.6rem + 1.25rem);
    /* #393 owner pass: 75 crowded the URL bar; 70lvh leaves a ~132px strip
       once chrome collapses */
    --phone-h: 70lvh;
  }
  .journey .stage,
  .journey .copy {
    grid-column: 1;
    grid-row: 1;
  }
  .journey .stage {
    height: 100lvh;
    place-items: start center;
    /* wordmark band (2rem header pad + the 2.6rem wordmark's 1.65 line box)
       plus 1.25rem of air to the bezel — the same gap the bezel keeps to the
       hero below, so the phone sits in even air. The mockup round's 60px
       "A3" clearance was retired on the device pass: it was picked before
       the phone grew to readable scale, and its 40px went to the device. */
    padding-top: var(--stage-top);
    /* sized by HEIGHT against the LARGE viewport (--phone-h above) — width
       derived through the bezel's own aspect (1696/780 screen plus
       2 x 2.4% padding = 2.2224 h/w). The rem cap keeps tablet portrait
       phone-sized. */
    --device-w: min(calc(var(--phone-h) / 2.2224), 17rem);
    /* the stage layers over the copy so beats slide behind the device; it
       must never eat the page's scroll or text selection */
    z-index: 2;
    pointer-events: none;
  }
  .journey .copy {
    position: relative;
    z-index: 1;
  }
  body.landing footer {
    /* compressed for the phone's strip (owner ruling): with the terminal
       breathe giving the footer its seat, less shrink is needed from the
       device when the footer itself is tight. Content untouched — the
       statutory particulars and store-gate anchors are pinned by tests;
       this is type scale and rhythm only. */
    padding-top: 0.7rem;
    padding-bottom: 0.9rem;
    font-size: 0.72rem;
  }
  body.landing footer .claim {
    font-size: 0.625rem;
    letter-spacing: 0.07em;
    margin-bottom: 0.3rem;
  }
  body.landing footer nav {
    gap: 1rem;
    margin-bottom: 0.3rem;
  }
  body.landing footer p {
    /* 0.2 rather than a hair tighter: the layout pin bans the dead #388
       device cap by substring, and a fifteen-hundredths margin would
       collide with it */
    margin-bottom: 0.2rem;
  }
  .journey .hero-copy,
  .journey .step {
    text-align: center;
    margin-left: auto;
    margin-right: auto;
  }
  .hero-copy {
    /* the hero's text TOP is pinned just below the bezel by construction:
       stage padding + the phone + a gap. At load (Safari chrome expanded)
       only the first line or two peek under the device — the owner-accepted
       scroll cue — and the first scroll collapses the chrome, opening the
       full strip. Seated from the top, not the bottom, so no screen height
       can put this text behind the device at load. */
    min-height: 100lvh;
    justify-content: flex-start;
    padding-top: calc(var(--stage-top) + var(--phone-h) + 1.25rem);
  }
  .hero-copy h1 {
    font-size: clamp(1.55rem, 6.5vw, 2.4rem);
    /* The hero is the longest line on the page and it now ends in the brand disc,
       whose advance is the period's own 0.24em. At 360px — a live Android width,
       where the clamp's MIN binds — that put it one pixel over its box and wrapped
       it. Bought back with three thousandths of an em of tracking rather than a size
       step: #393's ruling is that every beat speaks in the hero's register, so
       shrinking the hero alone would break the register at exactly the widths where
       the beats do not shrink. Measured, not eyeballed: 321px -> 317px in a 320px
       box. Well inside the -0.04em floor where letters start to touch. */
    letter-spacing: -0.028em;
    margin-bottom: 0.85rem;
  }
  .journey .step {
    /* two-speed pacing kept from #385, compressed: the strip is ~90px on
       an 844pt phone once the chrome collapses and reduced to one line, so
       arrival-then-response rhythm at strip scale — #393 */
    min-height: 55lvh;
  }
  .journey .step.cont {
    min-height: 40lvh;
  }
  .journey .step[data-shot="market-pan"] {
    min-height: 140lvh;
  }
  .journey .step h2 {
    font-size: clamp(1.55rem, 6.5vw, 2.4rem);
    line-height: 1.15;
  }

  /* #393 pacing round: with JS the captions leave the flow and render in a
     fixed slot under the bezel — one line per beat, faded in place, never
     aimed, never over the phone. The flow rules above stay as the no-JS
     layout. The hero's slot and the device's hero scale are set by the
     script from the LOAD viewport, so the headline owns the first fold on
     any device; logo-to-bezel and bezel-to-caption share the wordmark's
     1.25rem air in BOTH device states (owner ruling). */
  html.js .journey .hero-copy h1,
  html.js .journey .step h2 {
    position: fixed;
    top: calc(var(--stage-top) + var(--phone-h) + 1.25rem);
    left: 1.25rem;
    right: 1.25rem;
    margin: 0;
    text-align: center;
    opacity: 0;
    transition: opacity 200ms ease-out;
    pointer-events: none;
    z-index: 3;
  }
  html.js .journey .hero-copy h1 {
    top: var(--hero-caption-top, calc(var(--stage-top) + var(--phone-h) + 1.25rem));
  }
  html.js .journey .hero-copy.live h1,
  html.js .journey .step.live h2 {
    opacity: 1;
  }
  html.js .journey .hero-copy.live h1 {
    /* the returning device needs ~a beat to clear the hero's slot; the
       delay reads from the DESTINATION state, so fade-out stays instant */
    transition-delay: 250ms;
  }
  /* the breathing device's hero state: scaled to fit the LOAD viewport
     (the transition and the terminal footer-live state live with the
     permanent-stage rules above the media queries — the breathe is
     every-width behaviour now, the hero state is phone-only) */
  html.js .journey.hero-live .device {
    transform: scale(var(--hero-scale, 1));
  }
}

@media (prefers-reduced-motion: reduce) {
  .device .viewport img.swap,
  .device .viewport video.swap,
  .device .viewport div.swap,
  .journey .hero-copy,
  .journey .step,
  html.js .journey .hero-copy h1,
  html.js .journey .step h2,
  html.js body.landing .device {
    /* the device selector matches the permanent-stage transition rule's
       exact specificity — a weaker one here silently loses the cascade */
    transition: none;
  }
}
