/* ════════════════════════════════════════════════════════════════════
   VEXLOTEX — THE VERIFICATION LENS · scroll-film (Lane A, pure code)
   One pinned cinematic stage, six scrubbed beats read as one shot:
   01 Cold open · 02 The crack · 03 The scan · 04 The file
   05 The pullback (network) · 06 The crossing (dark → warm paper)
   Depends on css/shared.css for tokens, reset and the closing-section
   component styles (.btn, .stat-pill, .trust-bar, data-reveal, etc.)
   ════════════════════════════════════════════════════════════════════ */

:root {
  --cinema:   #0C0C10;   /* deepest cinema black (favicon backdrop) */
  --atmos:    1;         /* atmosphere master (grain/vignette), driven by JS */
}

/* ════ PAGE BASE — dark until the film crosses to paper ════ */
.film-page {
  background: var(--cinema);
  color: var(--m-text);
  /* the film owns vertical scroll; guard horizontal bleed */
  overflow-x: clip;
}
.film-page ::selection { background: rgba(255,106,43,0.28); }

/* ════ ATMOSPHERE OVERLAYS ════ */
.film-grain,
.film-scan,
.film-vignette {
  position: fixed; inset: 0;
  pointer-events: none;
  z-index: 80;
}
/* Cursor-reactive ember glow — a soft light that lerps toward the pointer
   (driven in film.js). Sits above the page black but below the beat content,
   and fades out with --atmos as the film crosses to warm paper. */
.cursor-glow {
  position: fixed; left: 0; top: 0; z-index: 6;
  width: 640px; height: 640px; border-radius: 50%;
  pointer-events: none; will-change: transform;
  background: radial-gradient(circle,
    rgba(255,120,60,0.17), rgba(255,106,43,0.05) 46%, transparent 70%);
  mix-blend-mode: screen;
  opacity: calc(var(--atmos) * 0.72);
}
/* Animated film grain (CSS-only; no video asset) */
.film-grain {
  opacity: calc(var(--atmos) * 0.5);
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  animation: grainShift 0.6s steps(2) infinite;
}
@keyframes grainShift {
  0%   { transform: translate(0, 0); }
  50%  { transform: translate(-4%, 3%); }
  100% { transform: translate(3%, -2%); }
}
.film-scan {
  opacity: calc(var(--atmos) * 0.5);
  background: repeating-linear-gradient(
    0deg, rgba(0,0,0,0.24) 0, rgba(0,0,0,0.24) 1px,
    transparent 1px, transparent 3px);
  z-index: 81;
}
.film-vignette {
  opacity: var(--atmos);
  background: radial-gradient(120% 100% at 50% 42%,
    transparent 42%, rgba(0,0,0,0.5) 78%, rgba(0,0,0,0.9) 100%);
  z-index: 82;
}

/* ════ PRELOADER ════ */
.film-preloader {
  position: fixed; inset: 0; z-index: 200;
  background: var(--cinema);
  display: grid; place-items: center;
  transition: opacity 700ms var(--ease-out-expo), visibility 700ms;
}
.film-preloader.is-done { opacity: 0; visibility: hidden; pointer-events: none; }
.film-preloader-inner { display: grid; justify-items: center; gap: 18px; }
.film-preloader-mark { animation: markPulse 1.5s var(--ease-in-out) infinite; }
@keyframes markPulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.86); } }
.film-preloader-label {
  font-family: var(--font-mono); font-size: var(--fs-tiny);
  letter-spacing: 0.28em; text-transform: uppercase; color: var(--m-muted);
}

/* ════ FIXED CHROME — brand · chapter readout · badge ════ */
.film-top {
  position: fixed; top: 0; left: 0; right: 0; z-index: 100;
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 18px clamp(16px, 4vw, 40px);
  color: var(--m-text);
  transition: color 500ms var(--ease-in-out);
}
.film-top.is-light { color: var(--text); }   /* flips when palette warms (beat 6) */
.film-brand { display: inline-flex; align-items: center; text-decoration: none; }
/* Two logo files, not one currentColor mark — the wordmark itself has fixed
   ink, so it can't recolor the way the old inline-SVG mark did when the
   header flips light/dark (.is-light, beat 6's warm palette). Swap the
   whole image instead, same pattern as .nav-logo-img/--light elsewhere. */
.film-brand-mark { display: block; height: 24px; width: auto; }
.film-brand-mark--dark { display: none; }
.film-top.is-light .film-brand-mark { display: none; }
.film-top.is-light .film-brand-mark--dark { display: block; }

.film-chapter {
  margin-inline: auto;
  display: flex; align-items: center; gap: 12px;
  font-family: var(--font-mono); font-size: var(--fs-tiny);
  letter-spacing: 0.14em; text-transform: uppercase;
  color: currentColor;
}
.film-chapter-num { opacity: 0.6; }
.film-chapter-name { min-width: 8ch; }
.film-chapter-bar {
  width: 84px; height: 2px; border-radius: 2px;
  background: currentColor; opacity: 0.2; overflow: hidden;
}
.film-chapter-bar i {
  display: block; height: 100%; width: 100%;
  background: var(--m-ember); transform: scaleX(0); transform-origin: 0 50%;
}
.film-top.is-light .film-chapter-bar i { background: var(--ember); }

/* Skip control — replaces the concept badge now the film opens the homepage.
   Points at #gateway; kept visible at every width (it's the escape hatch). */
.film-skip {
  font-family: var(--font-mono); font-size: 10px;
  letter-spacing: 0.16em; text-transform: uppercase;
  padding: 6px 12px; border-radius: var(--radius-pill);
  border: 1px solid currentColor; opacity: 0.72;
  white-space: nowrap; color: currentColor; text-decoration: none;
  transition: opacity var(--motion-hover) var(--ease-in-out);
}
.film-skip:hover, .film-skip:focus-visible { opacity: 1; }
/* At the gateway the film is over — the skip has nothing left to skip */
.film-top.is-light .film-skip { opacity: 0; pointer-events: none; }
@media (max-width: 720px) {
  .film-chapter-name { display: none; }
}
@media (max-width: 480px) {
  /* The brand + chapter rail + escape control cannot share a phone header.
     Keep the escape hatch fully visible; the static phone layout already
     presents the film beats in order without needing the chapter rail. */
  .film-chapter { display: none; }
}

/* ════ THE FILM DRIVER + PINNED STAGE ════ */
.film { height: 1150vh; position: relative; }
/* Compact (narrow/short) viewports get a shorter pinned scroll: 1150vh of
   swiping to clear the intro reads as sluggish on a phone, where the beats
   already collapse to the is-compact stacked layouts. Every beat window in
   js/film.js's B object is a fraction of 0..1, so shortening the container
   speeds through the whole film proportionally — no beat retuning needed. */
.is-cine.is-compact .film { height: 720vh; }
.film-stage {
  position: relative;      /* GSAP pins this; sticky fallback below for no-JS */
  height: 100vh; width: 100%;
  overflow: hidden;
  background: var(--cinema);
  perspective: 1400px;
}

/* Generic scene layer */
.scene, .claims, .scan-beam, .scan-readout, .file-doc, .net-stats, .warm-wash, .cross {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  will-change: transform, opacity;
}

/* ════ BEAT 1 · COLD OPEN — backlit interview portrait ════ */
.s-open { z-index: 10; pointer-events: none; }   /* decorative; never blocks hit-testing when faded */
.portrait {
  position: absolute; inset: 0;
  display: grid; place-items: end center;
  transform-origin: 50% 62%;
}
.portrait-light {
  position: absolute; left: 50%; top: 40%;
  width: 78vmin; height: 78vmin; transform: translate(-50%, -50%);
  background: radial-gradient(circle at 50% 46%,
    rgba(255,106,43,0.42) 0%, rgba(255,106,43,0.14) 34%,
    rgba(255,106,43,0.04) 52%, transparent 70%);
  filter: blur(6px);
}
.portrait-figure {
  position: relative;
  width: min(72vmin, 660px); height: min(88vh, 820px);
  object-fit: cover; object-position: 50% 44%;
  border-radius: 48% 48% 8px 8px / 18% 18% 8px 8px;
  filter: blur(var(--portrait-blur, 0px)) saturate(0.82) contrast(1.08) brightness(0.72)
          drop-shadow(0 24px 44px rgba(0,0,0,0.88));
  opacity: 0.86;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 10%, #000 78%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 10%, #000 78%, transparent 100%);
}
.portrait-rim {
  position: absolute; left: 50%; top: 40%;
  width: min(64vmin, 560px); aspect-ratio: 1;
  transform: translate(-49.4%, -20%);
  background: radial-gradient(closest-side at 58% 40%,
    rgba(255,150,90,0.5), transparent 62%);
  mix-blend-mode: screen; opacity: 0.7; pointer-events: none;
}
.ghost-word {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  white-space: nowrap; pointer-events: none; line-height: 1;
}
.ghost-word span {
  display: inline-block;
  font-family: var(--font-display); font-weight: 800;
  /* "TRUST?" renders at a fixed ~6.46x its own font-size in this font/
     tracking (measured, stable across sizes), so sizing off raw vw (the old
     18vw) let the word grow wider than the viewport with nowhere to go but
     clipped off both edges — on a 375px phone it rendered ~576px wide, so
     you only ever saw the middle few letters. Solving for "88% of viewport
     width" instead keeps the whole word on-screen, fully readable, at any
     viewport width, while still reading as a big, dramatic ghost mark.
     Re-measure this ratio (getBoundingClientRect().width / font-size) if the
     ghost word text ever changes again — it's per-string, not a font constant. */
  font-size: clamp(1.6rem, calc(88vw / 6.46), 15rem); letter-spacing: -0.03em;
  color: transparent;
  -webkit-text-stroke: 1.5px rgba(255,106,43,0.34);
  text-stroke: 1.5px rgba(255,106,43,0.34);
  /* flicker lives on the inner span so JS keeps full control of the
     outer element's opacity/scale (a CSS animation would win over inline) */
  animation: ghostFlicker 3.4s steps(1) infinite;
}
@keyframes ghostFlicker {
  0%, 18%, 22%, 60%, 64%, 100% { opacity: 0.9; }
  20%, 62%, 78% { opacity: 0.3; }
  80% { opacity: 1; }
}
.s-open-copy {
  position: relative; z-index: 3;
  text-align: center; padding-inline: var(--sp-3);
  /* Give the display headline room to lay out on its own two lines.
     A 22ch cap (the old value) was ~193px — far narrower than a 96px
     Syne headline, which forced every word onto its own left-flush line.
     "The interview" measures ~990px at the 6rem cap, so the box has to be
     wide enough for it; the mono sub is capped separately below. Because
     the font is 8vw until the 6rem cap, this width scales in step. */
  width: 100%;
  max-width: min(96vw, 1120px);
  margin-inline: auto;
}
.film-eyebrow {
  font-family: var(--font-mono); font-size: var(--fs-tiny);
  letter-spacing: 0.28em; text-transform: uppercase;
  color: var(--m-ember); margin-bottom: 18px;
}
.s-open-head {
  font-family: var(--font-display); font-weight: 800;
  font-size: clamp(2.6rem, 8vw, 6rem); line-height: 1.02;
  letter-spacing: -0.03em; color: #F4EEE6;
  text-shadow: 0 4px 40px rgba(0,0,0,0.6);
}
/* The "People change. / Your screening / should too." reveal — three lines
   staggered in by render(p) as the headline exits (see js/film.js), each
   starting faded/dropped so JS has something to animate from. Default
   opacity 0 only makes sense in cine mode; the .is-static override below
   forces all three visible for the no-scroll fallback. */
.s-open-message {
  margin-top: 22px; display: grid; gap: 4px;
}
.s-open-msg-lead {
  opacity: 0; transform: translateY(14px);
  font-family: var(--font-mono); font-size: clamp(0.85rem, 1.8vw, 1.1rem);
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--m-muted); margin-bottom: 6px;
}
.s-open-msg-line {
  opacity: 0; transform: translateY(16px);
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1.3rem, 3.6vw, 2.15rem); line-height: 1.16;
  letter-spacing: -0.01em; color: #F4EEE6;
  text-shadow: 0 4px 30px rgba(0,0,0,0.55);
}
.s-open-msg-line--ember { color: var(--m-ember); }
.open-proof {
  position: absolute; left: clamp(18px, 4vw, 54px); bottom: clamp(24px, 5vh, 52px);
  display: flex; gap: clamp(16px, 3vw, 34px); align-items: center;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.08em;
  text-transform: uppercase; color: rgba(244,238,230,0.58);
}
.open-proof span { display: grid; gap: 4px; }
.open-proof b {
  font-family: var(--font-display); font-size: 1rem; color: #FFE0C8;
  letter-spacing: 0;
}
.scroll-hint {
  position: absolute; bottom: 34px; left: 50%;
  transform: translateX(-50%);
  display: grid; justify-items: center; gap: var(--sp-1);
  font-family: var(--font-mono); font-size: 10px;
  letter-spacing: 0.24em; text-transform: uppercase; color: var(--m-muted);
}
/* Below 720px, .open-proof (below) drops into a centered row sharing this
   same bottom strip. Rather than dropping the scroll cue outright (a phone
   visitor still benefits from it — the pinned beat gives no native scroll
   affordance like a scrollbar does), stack it above the stats row instead.
   .open-proof's own mobile offset (below, 132px) puts its top edge ~194px
   up from the viewport bottom, so the cue needs at least that plus its own
   ~64px block height to clear it — 150px leaves a visible gap instead of
   the two rows running into each other. */
@media (max-width: 720px) {
  .is-cine .scroll-hint { bottom: 150px; }
}
.scroll-hint-line {
  width: 1px; height: 40px; overflow: hidden;
  background: rgba(181,175,169,0.28); position: relative;
}
.scroll-hint-line::after {
  content: ''; position: absolute; left: 0; top: 0;
  width: 100%; height: 40%; background: var(--m-ember);
  animation: hintRun 1.8s var(--ease-in-out) infinite;
}
@keyframes hintRun { 0% { transform: translateY(-100%); } 100% { transform: translateY(250%); } }

/* Persistent scroll cue — covers the long middle stretch .scroll-hint (above)
   doesn't: that one is scoped to the cold-open beat and fades within the
   first few percent of scroll. This is fixed chrome (like .film-top), driven
   by render(p) in film.js: fades in once .scroll-hint fades out, holds
   through the whole film, fades out before beat 8 warms the palette (so it
   never needs an .is-light color flip). */
.film-scroll-cue {
  position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%);
  z-index: 90;
  display: flex; align-items: center; gap: 9px;
  /* This is the whole navigation model on the intro — there's no scrollbar,
     no visible next section, nothing else on screen tells a first-time
     visitor "scroll to pick your path." It has to actually be noticed, not
     just technically present, so it carries a pill backdrop (legible over
     any frame of the film, not just the darker ones) and the brand ember
     color instead of the muted gray this used to blend into the footage
     with. */
  padding: 9px 16px 9px 18px; border-radius: 999px;
  background: rgba(12,12,16,0.44); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  border: 1px solid rgba(255,150,90,0.28);
  font-family: var(--font-mono); font-size: 11px; font-weight: 500;
  letter-spacing: 0.2em; text-transform: uppercase; color: #FFD9BE;
  opacity: 0; pointer-events: none;
}
.film-scroll-cue svg { flex: none; color: var(--m-ember); animation: filmScrollCueBounce 1.6s ease-in-out infinite; }
@keyframes filmScrollCueBounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(4px); } }
@media (max-width: 720px) { .film-scroll-cue { bottom: 84px; } }

/* ════ TYPEWRITER — scroll-scrubbed type-on (driven by render(p)) ════
   The .type-text content is written char-by-char from scroll progress in
   JS (deterministic → ?jump settles it). The caret blink is the only
   time-based motion here, and it is stilled for reduced-motion/static. */
.type-caret {
  display: inline-block; width: 0.62ch; height: 1em;
  margin-left: 2px; vertical-align: -0.12em;
  background: var(--m-ember);
  animation: caretBlink 1s steps(1) infinite;
}
@keyframes caretBlink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

/* ════ BEAT 2 · EXPLAINER — "what we actually check" (rides the crack) ════ */
.beat-explain {
  z-index: 13; place-items: start center; align-content: start;
  padding-top: clamp(74px, 13vh, 128px);
  text-align: center; opacity: 0; pointer-events: none;
}
.beat-explain > * { max-width: min(92vw, 720px); }
.explain-motif { margin-bottom: var(--sp-2); filter: drop-shadow(0 0 16px rgba(255,106,43,0.4)); }
.explain-eyebrow {
  font-family: var(--font-mono); font-size: var(--fs-small);
  letter-spacing: 0.28em; text-transform: uppercase;
  color: var(--m-ember); margin-bottom: 18px;
}
/* The beat's punchline — "anyone can lie, we verify what's true" — sized to
   read as a headline (not the small mono eyebrow it replaced), so it lands
   as the beat's main statement rather than a caption above it. */
.beat-explain > .explain-head { max-width: min(94vw, 900px); }
.explain-head {
  font-family: var(--font-display); font-weight: 800;
  /* Trimmed from a 3.6rem cap — big enough to read as the beat's headline,
     compact enough (with the claim fan's own downward push in film.js) to
     leave clear air between this block and the cards below it. */
  font-size: clamp(1.7rem, 4.8vw, 3.1rem); line-height: 1.08;
  letter-spacing: -0.02em; color: #F4EEE6;
  text-shadow: 0 4px 30px rgba(0,0,0,0.55);
  margin-bottom: 16px;
}
.explain-head--ember { color: var(--m-ember); }
.explain-line {
  font-family: var(--font-mono); font-weight: 500;
  font-size: clamp(1.2rem, 3vw, 2rem);
  letter-spacing: 0.01em; color: #FFE0C8; line-height: 1.35;
  min-height: 1.35em;   /* reserve height so the type-on doesn't reflow */
  text-shadow: 0 2px 24px rgba(0,0,0,0.6);
}
.explain-sub {
  margin-top: 14px; font-family: var(--font-mono);
  font-size: clamp(1rem, 1.9vw, 1.25rem);
  color: var(--m-muted); letter-spacing: 0.02em; line-height: 1.5;
}

/* ════ BEATS 2–4 · CLAIM STACK ════ */
.claims {
  z-index: 12; transform-style: preserve-3d;
  opacity: 0; pointer-events: none;
}
.claim {
  position: absolute; left: 50%; top: 50%;
  display: inline-flex; align-items: center; gap: 12px;
  padding: 16px 26px; min-width: min(78vw, 400px);
  border: 1px solid rgba(255,106,43,0.22);
  border-radius: 13px;
  background: rgba(20,18,17,0.66);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  font-family: var(--font-mono); font-size: 1.08rem;
  color: var(--m-text); white-space: nowrap;
  box-shadow: 0 20px 50px rgba(0,0,0,0.5);
  transform: translate(-50%, -50%);
  will-change: transform, opacity;
}
.claim-check {
  flex: none; width: 20px; height: 20px; border-radius: 50%;
  border: 1.5px solid rgba(181,175,169,0.4);
  position: relative; transition: none;
}
.claim.is-verified {
  border-color: rgba(255,106,43,0.7);
  background: rgba(40,20,10,0.72);
}
.claim.is-verified .claim-check {
  border-color: var(--m-ember); background: var(--m-ember);
}
.claim.is-verified .claim-check::after {
  content: ''; position: absolute; left: 5px; top: 2px;
  width: 5px; height: 10px; border: solid #16130F;
  border-width: 0 2px 2px 0; transform: rotate(42deg);
}
.claim.is-verified .claim-text { color: #FFD9BE; }
/* Compact (narrow/short) viewports: the settled scan-phase stack is a fixed
   vertical list (see GAP_S in sizeFan()), so a smaller card here is what
   keeps that 5-row stack short enough to clear .scan-readout parked above
   it — see the .is-compact .scan-readout rule below for the other half of
   that fix. */
.is-cine.is-compact .claim {
  min-width: min(74vw, 300px); padding: 11px 18px; font-size: 0.92rem;
}

/* ════ BEAT 3 · SCAN BEAM (X logomark) + readout ════ */
.scan-beam {
  z-index: 14; place-items: stretch;
  opacity: 0; pointer-events: none;
}
.scan-beam svg {
  position: absolute; left: 50%; top: 50%;
  width: min(78vw, 420px); height: 70px;
  transform: translate(-50%, -50%);
}
.scan-beam line {
  stroke: var(--m-ember); stroke-width: 3; stroke-linecap: round;
  filter: drop-shadow(0 0 14px rgba(255,106,43,0.95))
          drop-shadow(0 0 40px rgba(255,106,43,0.55));
}
.scan-readout {
  z-index: 15; place-items: end start;
  padding: clamp(20px, 6vw, 72px);
  opacity: 0; pointer-events: none;
  align-content: end;
}
.scan-readout > * { grid-column: 1; }
.readout-status {
  font-family: var(--font-mono); font-size: 1.05rem;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--m-ember); margin-bottom: var(--sp-2);
}
.readout-line {
  display: flex; justify-content: space-between; gap: var(--sp-4);
  min-width: min(88vw, 320px);
  font-family: var(--font-mono); font-size: 1.02rem;
  padding: 9px 0; border-bottom: 1px solid var(--m-border);
  color: var(--m-muted);
}
.readout-line .readout-v { color: var(--m-text); }
/* Compact (narrow/short) viewports: bottom-left anchoring only clears the
   centered claim stack on a wide desktop frame, where the stack's ~400px
   cards leave the corners free. On a phone the stack runs nearly full-width
   and the two panels' visibility windows overlap (see render()'s scanVis
   vs. claimsVis) — so on compact, park the readout above the stack instead
   of behind it: top-anchored and shrunk, clear of both the header and the
   (now-shorter, see .is-compact .claim) settled card stack below it. */
.is-cine.is-compact .scan-readout {
  place-items: start center; align-content: start;
  padding: clamp(78px, 15vh, 118px) 20px 0; text-align: center;
}
.is-cine.is-compact .readout-status { font-size: 0.94rem; }
.is-cine.is-compact .readout-line { min-width: 0; width: min(80vw, 300px); font-size: 0.92rem; }

/* ════ BEAT 4 · THE FILE + STAMP ════ */
.file-doc {
  z-index: 16; place-items: center;
  opacity: 0; pointer-events: none;
}
.file-doc {
  --w: min(72vw, 420px);
}
.file-doc {
  /* the document itself */
}
.file-head, .file-lines, .file-stamp { position: absolute; }
.file-doc::before {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: min(66vw, 400px); height: min(80vh, 520px);
  transform: translate(-50%, -50%);
  background: #F4EEE6; border-radius: 6px;
  box-shadow: 0 40px 100px rgba(0,0,0,0.6);
}
.file-head {
  left: 50%; top: 50%;
  transform: translate(-50%, calc(-50% - min(36vh, 224px)));
  display: inline-flex; align-items: center; gap: 10px;
  font-family: var(--font-mono); font-size: 11px;
  letter-spacing: 0.14em; color: #8A6A55;
}
.file-mark {
  width: 12px; height: 12px; background: var(--ember);
  clip-path: polygon(50% 0, 100% 38%, 82% 100%, 18% 100%, 0 38%);
}
.file-lines {
  left: 50%; top: 50%;
  transform: translate(-50%, calc(-50% - 8px));
  width: min(50vw, 300px); display: grid; gap: 15px;
}
.file-lines i {
  height: 8px; border-radius: 4px; background: rgba(26,23,20,0.14);
}
.file-lines i:nth-child(2) { width: 82%; }
.file-lines i:nth-child(4) { width: 68%; }
.file-lines i:nth-child(6) { width: 74%; }
.file-stamp {
  left: 50%; top: 50%;
  transform: translate(-50%, calc(-50% + min(24vh, 150px))) rotate(-11deg);
  display: grid; justify-items: center; gap: 3px;
  padding: 12px 22px; border: 3px solid var(--ember-ink);
  border-radius: 8px; color: var(--ember-ink);
  font-family: var(--font-display); font-weight: 800;
  opacity: 0; transform-origin: 50% 50%;
  mix-blend-mode: multiply;
}
.file-stamp-top { font-size: clamp(1.1rem, 3vw, 1.7rem); letter-spacing: 0.06em; line-height: 1; }
.file-stamp-bottom { font-family: var(--font-mono); font-weight: 500; font-size: 10px; letter-spacing: 0.22em; }

/* ════ BEAT 5 · THE NETWORK ════ */
.s-net {
  z-index: 18; display: block; place-items: initial;
  width: 100%; height: 100%;
  opacity: 0; pointer-events: none;
}
.net-stats {
  z-index: 19; place-items: end center; align-content: end;
  padding-bottom: clamp(40px, 9vh, 90px);
  opacity: 0; pointer-events: none;
  will-change: transform, opacity, filter;
}
.net-caption {
  font-family: var(--font-mono); font-size: 1.05rem;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--m-ember); margin-bottom: 26px; text-align: center;
}
.net-grid {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: clamp(24px, 6vw, 72px);
}
.net-stat { display: grid; justify-items: center; gap: 6px; text-align: center; }
.net-num {
  font-family: var(--font-mono); font-weight: 500;
  font-size: clamp(2.6rem, 6.6vw, 4.6rem); line-height: 1;
  color: #FFE0C8; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 30px rgba(255,106,43,0.4);
}
.net-label {
  font-family: var(--font-mono); font-size: 12px;
  letter-spacing: 0.18em; text-transform: uppercase; color: var(--m-muted);
}
.net-foot {
  margin-top: 30px; text-align: center;
  font-family: var(--font-mono); font-size: var(--fs-small);
  letter-spacing: 0.14em; color: var(--m-muted);
}
.net-foot::before,
.net-foot::after { content: '·'; margin: 0 10px; color: var(--m-ember); opacity: 0.7; }

/* Below 720px the persistent .film-scroll-cue docks at bottom:84px as a
   ~38px pill (see its own rule). Every bottom-anchored beat readout that
   can be on screen at the same time needs enough clearance to never sit
   under it — .open-proof (cold open, fades out ~4-9% scroll, right as the
   cue fades in), .scan-readout ("Compliance" line, mid-scan) and .net-stats
   (the "1-24h turnaround" foot line) all measured overlapping the cue's
   footprint before this fix. Placed after each element's own rule so it
   wins the cascade (a same-specificity override earlier in the file would
   be clobbered by the base rule below it). */
@media (max-width: 720px) {
  .open-proof { bottom: 132px; }
  .scan-readout { padding-bottom: 132px; }
  .net-stats { padding-bottom: 132px; }
}

/* ════ BEAT 6 · THE MENU — breadth of checks (dark, ember accents) ════
   z-index climbs one per beat (5=18/19, 6=20, 7=21, 8=22/23) so a crossfade
   between adjacent beats always has the incoming one on top, not whichever
   happened to paint last — was previously 19, tied with beat 5's .net-stats
   and beat 7's .beat-measure, correct only by luck of DOM order + opacity
   timing never overlapping enough to show it. */
.beat-menu {
  position: absolute; inset: 0;
  z-index: 20; display: grid; place-items: center; align-content: center;
  gap: clamp(18px, 2.8vh, 30px);
  padding: clamp(24px, 4vh, 56px) var(--sp-3);
  text-align: center; opacity: 0; pointer-events: none;
  will-change: transform, opacity;
}
.menu-eyebrow {
  font-family: var(--font-mono); font-size: clamp(1rem, 1.9vw, 1.2rem);
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--m-ember); text-shadow: 0 2px 20px rgba(0,0,0,0.6);
}
.menu-group { display: grid; gap: var(--sp-2); max-width: min(94vw, 940px); }
.menu-group-label {
  font-family: var(--font-mono); font-size: var(--fs-small);
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--m-muted);
}
.menu-group--surprise .menu-group-label { color: #FFB27A; }
.menu-chips {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; justify-content: center; gap: 12px;
}
.menu-chip {
  display: grid; gap: 5px; text-align: left;
  padding: 14px 20px; border-radius: 13px;
  border: 1px solid rgba(255,106,43,0.22);
  background: rgba(20,18,17,0.66);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  box-shadow: 0 14px 36px rgba(0,0,0,0.4);
  will-change: transform, opacity;
}
.menu-group--surprise .menu-chip { border-color: rgba(255,150,90,0.42); }
.menu-chip-k {
  font-family: var(--font-display); font-weight: 800; font-size: 1.2rem;
  color: #FFE0C8; line-height: 1.12;
}
.menu-chip-v {
  font-family: var(--font-mono); font-size: 0.9rem;
  letter-spacing: 0.01em; color: var(--m-muted); line-height: 1.35;
}
.menu-close {
  max-width: min(92vw, 720px);
  font-family: var(--font-mono); font-size: clamp(1.05rem, 2.2vw, 1.4rem);
  letter-spacing: 0.01em; color: #FFD9BE; line-height: 1.5;
  opacity: 0;
}

/* ── Menu · CONSTELLATION MODE (body.is-cine only) ──
   The chips leave the flow and become nodes on two rings around a hub;
   the network canvas persists dimly underneath as connective tissue.
   Static/reduced-motion never gets .is-cine, so it keeps the flow layout. */
.menu-hub {
  display: none;   /* cinematic-only element */
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: min(86vw, 380px);
  text-align: center; pointer-events: none;
}
.is-cine .menu-hub { display: grid; justify-items: center; }
.menu-hub-node {
  grid-row: 1; grid-column: 1;
  width: 13px; height: 13px; border-radius: 50%;
  background: var(--m-ember); margin-bottom: 18px;
  box-shadow: 0 0 16px var(--m-ember), 0 0 56px rgba(255,106,43,0.5);
}
.menu-hub-line {
  grid-row: 2; grid-column: 1;   /* the three intertitles stack in place */
  font-family: var(--font-mono); font-size: clamp(1rem, 1.9vw, 1.2rem);
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--m-muted); line-height: 1.5; opacity: 0;
  text-shadow: 0 2px 20px rgba(0,0,0,0.7);
}
.menu-hub-line--ember { color: var(--m-ember); }
.menu-hub-line--surprise { color: #FFB27A; }

.is-cine .beat-menu { display: block; will-change: transform, opacity, filter; }
.is-cine .menu-eyebrow,
.is-cine .menu-group-label { display: none; }   /* the hub plays these as intertitles */
.is-cine .menu-group, .is-cine .menu-chips { display: contents; }
.is-cine .menu-chip {
  position: absolute; left: 50%; top: 50%;
  max-width: min(40vw, 300px);
  padding: 11px 16px;
  opacity: 0; white-space: normal;
}
.is-cine .menu-chip-k { font-size: 1.05rem; }
.is-cine .menu-chip-v { font-size: 0.82rem; }
.is-cine .menu-chip.is-lit {
  border-color: rgba(255,150,90,0.55);
  box-shadow: 0 0 26px rgba(255,106,43,0.26), 0 14px 36px rgba(0,0,0,0.45);
}
.is-cine .menu-close {
  position: absolute; left: 50%; bottom: clamp(16px, 3vh, 36px);
  transform: translateX(-50%); width: max-content;
  max-width: min(90vw, 880px);
  font-size: clamp(0.9rem, 1.6vw, 1.15rem); line-height: 1.45;
  text-shadow: 0 2px 20px rgba(0,0,0,0.7);
}

/* ── Menu · COMPACT CINEMATIC (body.is-cine.is-compact) ──
   The ring constellation needs ~1100x688 to lay out without chips colliding
   (see MIN_CINE_W/H in film.js); below that the film still runs, but the beat
   is still PINNED — the pin clips to one viewport height (overflow:hidden on
   .film-stage), so unlike the flow-layout static fallback, compact-cinematic
   content that's taller than the screen doesn't get more room, it gets cut
   off top and bottom. All 9 full description cards (~1560px stacked) don't
   fit any phone. So compact mode drops the subtitle and shows a wrapping row
   of short label tags instead of stacked cards — same scroll-staggered
   entrance (film.js branches on `narrow` for the transform only), but short
   enough to sit inside one frame with room to spare. */
.is-cine.is-compact .beat-menu { display: grid; }
.is-cine.is-compact .menu-eyebrow,
.is-cine.is-compact .menu-group-label { display: block; }
.is-cine.is-compact .menu-hub { display: none; }
.is-cine.is-compact .menu-group,
.is-cine.is-compact .menu-chips { display: revert; }
.is-cine.is-compact .menu-chips {
  display: flex; flex-wrap: wrap; justify-content: center; gap: var(--sp-1);
}
.is-cine.is-compact .menu-chip {
  position: relative; left: auto; top: auto;
  max-width: none; width: auto;
  padding: 9px 14px;
}
.is-cine.is-compact .menu-chip-k { font-size: 0.92rem; }
.is-cine.is-compact .menu-chip-v { display: none; }

/* ════ BEAT 7 · THE MEASURE — psychometric assessment (dark, ember accents) ════ */
.beat-measure {
  position: absolute; inset: 0;
  z-index: 21; display: grid; place-items: center; align-content: center;
  gap: clamp(18px, 2.6vh, 30px);
  padding: clamp(28px, 5vh, 64px) var(--sp-3);
  text-align: center; opacity: 0; pointer-events: none;
  will-change: transform, opacity, filter;
}
.measure-eyebrow {
  font-family: var(--font-mono); font-size: clamp(1rem, 1.9vw, 1.2rem);
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--m-ember);
  text-shadow: 0 2px 20px rgba(0,0,0,0.6);
}
.measure-head {
  font-family: var(--font-display); font-weight: 800;
  font-size: clamp(1.55rem, 4vw, 2.9rem); line-height: 1.09;
  letter-spacing: -0.01em; color: #FFF3EA; max-width: min(94vw, 900px);
  text-shadow: 0 2px 30px rgba(0,0,0,0.6);
}
/* The measure body: [2 facet cards | the returning silhouette | 2 facet cards].
   Static mode, and compact cinematic (phone widths), stack the three cells
   into one readable column instead — the 3-column arrangement needs the same
   wide frame the menu ring does. */
.measure-body {
  display: grid; grid-template-columns: 1fr;
  gap: clamp(14px, 2vw, 24px);
  width: 100%; max-width: min(96vw, 1080px);
  align-items: center;
}
.is-cine .measure-body { grid-template-columns: 1fr auto 1fr; }
.is-cine.is-compact .measure-body { grid-template-columns: 1fr; }
/* Same fit problem as the menu: the pin clips to one viewport height, and
   head + eyebrow + 4 full facet cards + figure + badge stack to ~1300px —
   nowhere near a phone screen. Facets drop to short label tags like the
   menu chips; the figure shrinks so the (now shorter) stack clears frame. */
.is-cine.is-compact .measure-facets {
  display: flex; flex-wrap: wrap; justify-content: center; gap: var(--sp-1);
}
.is-cine.is-compact .measure-facet { padding: 9px 14px; }
.is-cine.is-compact .mf-k { font-size: 0.92rem; }
.is-cine.is-compact .mf-v { display: none; }
.is-cine.is-compact .measure-figure svg { width: clamp(170px, 26vh, 240px); }
.measure-facets {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: 14px; width: 100%;
}
.measure-figure { position: relative; opacity: 0; will-change: transform, opacity; }
.film-page.is-static .measure-figure { opacity: 1; }
.measure-figure-photo {
  position: absolute; left: 50%; bottom: 0; z-index: 0;
  width: 90%; height: 100%; transform: translateX(-50%);
  object-fit: cover; object-position: 50% 28%;
  border-radius: 46% 46% 8px 8px / 16% 16% 8px 8px;
  filter: saturate(0.68) contrast(1.12) brightness(0.56)
          drop-shadow(0 0 30px rgba(255,106,43,0.18));
  opacity: 0.9;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 12%, #000 72%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 12%, #000 72%, transparent 100%);
}
.measure-figure svg {
  position: relative; z-index: 1; display: block; margin-inline: auto;
  /* Tracks viewport HEIGHT, which is what actually constrains this figure — it sits
     centre-frame between the two facet columns, so width was never the binding axis.
     34vmin capped at 320px left it small on tall screens (245px at 720h) and stopped
     growing entirely past 941px of height. vh keeps it proportionate to the frame it
     stands in; the 300px floor and 430px ceiling keep it clear of the facet columns. */
  width: clamp(300px, 42vh, 430px); height: auto;
  overflow: visible;
}
.measure-dot { opacity: 0; }
.measure-dot line {
  stroke: rgba(255,150,90,0.42); stroke-width: 1.5;
  stroke-dasharray: 3 5;
}
.measure-dot .md-halo { fill: rgba(255,106,43,0.22); }
.measure-dot .md-core { fill: #FFE0C8; }
.measure-ghost {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  white-space: nowrap; pointer-events: none; line-height: 1;
  opacity: 0; z-index: -1;
}
.measure-ghost span {
  font-family: var(--font-display); font-weight: 800;
  /* Same fix as .ghost-word span above — "TRUST." measures ~5.42x its own
     font-size here, so size off 88vw/ratio instead of raw vw so it never
     grows wider than the viewport and clips off-screen. */
  font-size: clamp(1.6rem, calc(88vw / 5.42), 12rem); letter-spacing: -0.03em;
  color: transparent;
  -webkit-text-stroke: 1.5px rgba(255,106,43,0.30);
  text-stroke: 1.5px rgba(255,106,43,0.30);
}
.measure-facet {
  display: grid; gap: 6px; text-align: left;
  padding: 16px 22px; border-radius: 14px;
  border: 1px solid rgba(255,150,90,0.34);
  background: rgba(24,19,17,0.7);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  box-shadow: 0 16px 40px rgba(0,0,0,0.42);
  will-change: transform, opacity;
}
.mf-k { font-family: var(--font-display); font-weight: 800; font-size: 1.2rem; color: #FFE0C8; line-height: 1.12; }
.mf-v { font-family: var(--font-mono); font-size: 0.9rem; letter-spacing: 0.01em; color: var(--m-muted); line-height: 1.35; }
.measure-badge {
  display: inline-flex; align-items: center; gap: 10px;
  font-family: var(--font-mono); font-size: clamp(0.9rem, 1.6vw, 1.1rem);
  letter-spacing: 0.06em; color: #FFD9BE; opacity: 0;
  padding: 11px 22px; border-radius: 999px;
  border: 1px solid rgba(255,106,43,0.4); background: rgba(255,106,43,0.08);
}
.mb-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--m-ember); box-shadow: 0 0 12px var(--m-ember); }

/* ════ BEAT 8 · THE CROSSING (dark → warm paper) ════ */
.warm-wash {
  z-index: 22; background: var(--bg);
  opacity: 0; pointer-events: none;
}
.cross {
  z-index: 23; align-content: center; gap: 0;
  opacity: 0; pointer-events: none;
}
.cross-caption {
  font-family: var(--font-mono); font-size: var(--fs-small);
  letter-spacing: 0.24em; text-transform: uppercase;
  color: var(--ember-ink); margin-bottom: var(--sp-4); text-align: center;
}
/* The instruction lede — the crossing's only job is to say the choice is next.
   Driven by render(p); the static path shows it resolved. */
.cross-lede {
  font-family: var(--font-display); font-weight: 800;
  font-size: clamp(1.7rem, 3.6vw, 2.7rem); line-height: 1.12;
  letter-spacing: -0.015em; text-align: center; text-wrap: balance;
  color: var(--text); max-width: 22ch; margin-inline: auto;
  opacity: 0;
}

/* Continuation hint under the lede — the cold open's scroll hint, returned
   as a bookend on warm paper pointing into the gateway. Hidden static. */
.cross-hint {
  margin-top: var(--sp-4);
  display: grid; justify-items: center; gap: var(--sp-1);
  font-family: var(--font-mono); font-size: 10px;
  letter-spacing: 0.24em; text-transform: uppercase; color: var(--ember-ink);
  opacity: 0;
}
.cross-hint-line {
  width: 1px; height: 36px; overflow: hidden;
  background: rgba(26,23,20,0.18); position: relative;
}
.cross-hint-line::after {
  content: ''; position: absolute; left: 0; top: 0;
  width: 100%; height: 40%; background: var(--ember-ink);
  animation: hintRun 1.8s var(--ease-in-out) infinite;
}

/* ════════════════════════════════════════════════════════════════════
   STATIC / REDUCED-MOTION FALLBACK — a still, legible scrolled layout.
   Set by JS (.is-static on <body>) when reduced-motion, small screen,
   or GSAP fails to load. Every beat becomes a stacked, readable panel.
   ════════════════════════════════════════════════════════════════════ */
.film-page.is-static .film-grain,
.film-page.is-static .film-scan { display: none; }
.film-page.is-static .film-vignette { opacity: 0.5; }
/* The chrome scrolls away with the film — fixed light-on-dark text would
   otherwise float unreadably over the warm gateway further down the page */
.film-page.is-static .film-top { position: absolute; }

.film-page.is-static .film { height: auto; }
.film-page.is-static .cursor-glow { display: none; }
.film-page.is-static .film-stage {
  position: static; height: auto; overflow: visible;
  perspective: none;
  display: grid; gap: 0;
}
.film-page.is-static .scene,
.film-page.is-static .claims,
.film-page.is-static .beat-explain,
.film-page.is-static .scan-beam,
.film-page.is-static .scan-readout,
.film-page.is-static .file-doc,
.film-page.is-static .net-stats,
.film-page.is-static .beat-menu,
.film-page.is-static .beat-menu .menu-chip,
.film-page.is-static .beat-menu .menu-close,
.film-page.is-static .beat-measure,
.film-page.is-static .beat-measure .measure-facet,
.film-page.is-static .beat-measure .measure-badge,
.film-page.is-static .warm-wash,
.film-page.is-static .cross {
  position: relative; inset: auto;
  opacity: 1 !important; transform: none !important;
  min-height: auto;
}

/* Explainer becomes a compact centered panel in the static flow */
.film-page.is-static .beat-explain {
  place-items: center; align-content: center;
  padding: var(--sp-8) var(--sp-3);
}
.film-page.is-static .type-caret { display: none; }

/* Cold open panel */
.film-page.is-static .s-open {
  min-height: 92vh; display: grid; place-items: center;
}
.film-page.is-static .ghost-word { opacity: 0.5; }
.film-page.is-static .ghost-word span { animation: none; }
.film-page.is-static .portrait { position: absolute; opacity: 0.7; }
.film-page.is-static .s-open-head,
.film-page.is-static .s-open-msg-lead,
.film-page.is-static .s-open-msg-line { opacity: 1 !important; transform: none !important; }
.film-page.is-static .scroll-hint { display: none; }
.film-page.is-static .film-scroll-cue { display: none; }   /* static path has a native scrollbar */
.film-page.is-static .open-proof { bottom: 22px; }

@media (max-width: 720px) {
  /* left:4% + width:92% centers this without a transform — render() sets
     .open-proof's transform directly (translateY only, for its entrance
     slide) on every scroll tick in cine mode, and would clobber a
     translateX(-50%) centering transform set here. */
  .open-proof {
    left: 4%; width: 92%;
    justify-content: center; text-align: center;
  }
  .open-proof span { flex: 1; }
}

/* Claims → a tidy verified list */
.film-page.is-static .claims {
  display: grid; gap: 12px; place-items: center;
  padding: var(--sp-8) var(--sp-3); pointer-events: auto;
}
.film-page.is-static .claim {
  position: relative; left: auto; top: auto; transform: none;
  border-color: rgba(255,106,43,0.7); background: rgba(40,20,10,0.72);
}
.film-page.is-static .claim .claim-check { border-color: var(--m-ember); background: var(--m-ember); }
.film-page.is-static .claim .claim-check::after {
  content: ''; position: absolute; left: 5px; top: 2px;
  width: 5px; height: 10px; border: solid #16130F; border-width: 0 2px 2px 0; transform: rotate(42deg);
}
.film-page.is-static .claim .claim-text { color: #FFD9BE; }

.film-page.is-static .scan-beam { display: none; }
.film-page.is-static .scan-readout {
  place-items: center; padding: 0 var(--sp-3) var(--sp-8);
}
.film-page.is-static .scan-readout > * { text-align: center; }

/* File panel */
.film-page.is-static .file-doc {
  min-height: 80vh; place-items: center;
}
.film-page.is-static .file-stamp { opacity: 1; }

/* Network → the scale stats on a dark band (canvas hidden) */
.film-page.is-static .s-net { display: none; }
.film-page.is-static .net-stats {
  padding-block: var(--sp-12); background: var(--m-bg);
}

/* Menu → stacked dark panel */
.film-page.is-static .beat-menu {
  place-items: center; padding-block: var(--sp-12); background: var(--m-bg);
}

/* Measure → stacked dark panel: figure shown lit, dots on, no ghost/stubs */
.film-page.is-static .beat-measure {
  place-items: center; padding-block: var(--sp-12); background: var(--m-bg);
}
.film-page.is-static .measure-ghost { display: none; }
.film-page.is-static .measure-dot { opacity: 1 !important; }
.film-page.is-static .measure-dot line { display: none; }

/* Crossing → a dark→warm dawn bridge into the real gateway: the caption
   and lede read as the gateway's intro lines, and the gateway cards sit
   directly below. */
.film-page.is-static .warm-wash { display: none; }
.film-page.is-static .cross {
  background: linear-gradient(180deg, var(--cinema) 0%, var(--bg) 38%);
  padding-block: var(--sp-12) var(--sp-2);
}
.film-page.is-static .cross-caption { margin-bottom: var(--sp-2); }
.film-page.is-static .cross-lede { opacity: 1; transform: none; }
.film-page.is-static .cross-hint { display: none; }
.film-page.is-static .beat-measure { padding-block: var(--sp-12) var(--sp-8); }

/* ── Rack focus: drop nested backdrop blur while a container blur is up ──
   render() puts filter: blur() on whole beat containers whose children each carry
   an always-on backdrop-filter. That nesting is the expensive case for GPU raster
   and composite: the child resolves its own backdrop blur and the parent then
   blurs the result. While the parent blur is active the child's contributes
   nothing visible, so js/film.js toggles .rack-blur on the container for exactly
   that window and we suppress the children's here. Purely a cost removal — the
   panels keep their translucent background, which is what reads on screen. */
.rack-blur .claim,
.rack-blur .menu-chip,
.rack-blur .measure-facet {
  backdrop-filter: none; -webkit-backdrop-filter: none;
}

/* Reduced-motion also stills the atmosphere and infinite loops */
@media (prefers-reduced-motion: reduce) {
  .film-grain { animation: none; }
  .ghost-word { opacity: 0.5; }
  .ghost-word span { animation: none; }
  .scroll-hint-line::after { animation: none; }
  .film-scroll-cue svg { animation: none; }
  .film-preloader-mark { animation: none; }
  .type-caret { animation: none; opacity: 0.6; }
  .cursor-glow { display: none; }
}
