/* Story homepage (20 Oct 2026). Chapters pin while you scroll; story.js supplies --p (pinned progress, 0→1)
   and --in (entrance, 0→1). Everything here animates transform and opacity only, except the one stretching
   headline in the finale (DESIGN.md: Archivo width at signature moments). .story-static = motion off. */

.story { --p: 0; --in: 0; --mx: 0; --my: 0; overflow-x: clip; }  /* clip, not hidden: keeps the sticky chapters working */
.story-ch { position: relative; height: calc((var(--len) + 1) * 100svh); background: var(--surface); color: var(--text); }
.story-pin { position: sticky; top: 0; height: 100svh; overflow: clip; }
.story-static .story-ch { height: auto; }
.story-static .story-pin { position: relative; height: auto; min-height: 100svh; }

.story-hud { position: absolute; z-index: 3; top: calc(var(--header-height) + var(--space-3)); left: var(--gutter); right: var(--gutter); display: flex; justify-content: space-between; gap: var(--space-4); font: var(--weight-medium) var(--type-label-s)/1.4 var(--font-mono); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--text-secondary); pointer-events: none; }

.story-copy h2, .story-words, .story-rail-head h2 { margin: 0; font: var(--weight-bold) var(--type-d1)/var(--leading-display) var(--font-display); text-transform: uppercase; letter-spacing: var(--tracking-display); }
.story-copy p { max-width: var(--measure); color: var(--text-secondary); font-size: var(--type-lead); margin: var(--space-4) 0 0; }
.story-copy .actions { margin-top: var(--space-6); }
.story-copy-bottom { position: absolute; z-index: 3; left: 0; right: 0; bottom: var(--space-9); }
/* Frank's handwritten asides */
.story-aside { font: var(--weight-regular) clamp(28px, 3vw, 46px)/1 var(--font-hand) !important; color: var(--text) !important; transform: rotate(-3deg); transform-origin: left; margin: var(--space-4) 0 0 !important; }

/* 002 · Frank: three words slam in over his face (white page to match the portrait's background) */
.story-frank { --surface: var(--bone-100); }
.story-frank .story-pin { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); align-items: center; }
.story-frank-art { grid-column: 2; grid-row: 1; height: 100svh; transform: scale(calc(1.1 - .1 * var(--p))); transform-origin: 50% 30%; }
.story-frank-art img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 20%; }
.story-frank-copy { grid-column: 1 / 3; grid-row: 1; position: relative; z-index: 2; padding: var(--space-9) var(--gutter) 0; }
.story-words { font-size: clamp(88px, 15vw, 260px); line-height: .76; }
.story-words .w { display: block; --o: 1; opacity: var(--o); transform: translateY(calc((1 - var(--o)) * .5em)) scale(calc(1.25 - .25 * var(--o))); transform-origin: left bottom; }
.story-words .w1 { --o: clamp(0, (var(--p) - .02) * 7, 1); }
.story-words .w2 { --o: clamp(0, (var(--p) - .24) * 7, 1); color: var(--accent); }
.story-words .w3 { --o: clamp(0, (var(--p) - .46) * 7, 1); }
.story-frank .story-aside { opacity: clamp(0, (var(--p) - .66) * 6, 1); }

/* 003 · Dunhollow: a stitched panel grows into the city */
.story-dunhollow { background: var(--ink-1000); color: var(--text-on-media); }
.story-panel { --g: clamp(0, var(--p) * 1.7, 1); position: absolute; inset: 0; transform: scale(calc(.46 + .54 * var(--g))); }
.story-panel-art { position: absolute; inset: 0; overflow: clip; }
.story-layer { position: absolute; inset: -8% 0; transform-origin: var(--focus, 50%) 50%; transform: translate(calc(var(--mx) * var(--drift, 0) * 100vw / 1674), calc(var(--p) * var(--speed) * -10svh + var(--my) * var(--drift, 0) * .5 * 100vw / 1674)) scale(calc(1.18 - .1 * var(--g))); }
.story-layer img { width: 100%; height: 100%; object-fit: cover; object-position: var(--focus, 50%) 50%; }
.story-rain { position: absolute; inset: -40% 0 0; background: repeating-linear-gradient(104deg, transparent 0 46px, var(--line-on-media) 46px 47px, transparent 47px 140px); opacity: .5; animation: story-rain 900ms linear infinite; }
@keyframes story-rain { to { transform: translate3d(-8px, 40%, 0); } }
.story-seam { position: absolute; left: 0; top: 52%; width: 100%; height: 40px; fill: none; stroke: var(--bone-100); stroke-width: 2.5; stroke-dasharray: 14 8; opacity: calc(1 - var(--g)); transform: rotate(-3deg) scaleX(calc(1 + var(--g))); }
.story-seam path + path { stroke-dasharray: none; }
.story-dunhollow .story-copy { opacity: clamp(0, (var(--p) - .58) * 5, 1); transform: translateY(calc((1 - clamp(0, (var(--p) - .58) * 5, 1)) * 30px)); }
.story-dunhollow .story-copy p, .story-dunhollow .story-aside { color: var(--text-on-media) !important; }
.story-dunhollow .story-pin::after { content: ''; position: absolute; inset: 0; z-index: 2; background: linear-gradient(transparent 45%, var(--scrim-strong)); opacity: clamp(0, (var(--p) - .5) * 4, 1); pointer-events: none; }
.story-dunhollow .story-hud { color: var(--text-on-media); }
/* Lightning flash (story.js flash()): bright in the sky behind the city, a faint wash over the whole screen */
.story-lightning { position: absolute; inset: 0; pointer-events: none; opacity: 0; mix-blend-mode: screen; }
.story-lightning-sky { background: radial-gradient(110% 80% at 55% 10%, var(--bone-100), color-mix(in srgb, var(--bone-100) 45%, transparent) 55%, transparent 85%); }
.story-lightning-screen { z-index: 4; background: color-mix(in srgb, var(--bone-100) 22%, transparent); }

/* 004 · The escape: comic panels slam in one per step */
.story-escape-grid { height: 100svh; display: grid; grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); gap: var(--space-8); align-items: center; padding-top: var(--header-height); }
.story-comic { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: var(--space-3); height: min(72svh, 640px); }
.story-comic-panel { position: relative; margin: 0; overflow: clip; border: 2px solid var(--text); --o: 1; opacity: var(--o); transform: scale(calc(1.18 - .18 * var(--o))) rotate(calc((1 - var(--o)) * -4deg)); }
.story-comic-panel img { width: 100%; height: 100%; object-fit: cover; }
.story-panel-no { position: absolute; top: var(--space-2); left: var(--space-2); font: var(--weight-medium) var(--type-label-s)/1 var(--font-mono); background: var(--surface); color: var(--text); padding: var(--space-1) var(--space-2); }
/* Comic caption box: Frank's narration, one line per panel */
.story-caption { position: absolute; left: var(--space-3); bottom: var(--space-3); max-width: calc(100% - var(--space-6)); padding: var(--space-2) var(--space-3); background: var(--bone-100); color: var(--ink-1000); border: 2px solid var(--ink-1000); font: var(--weight-regular) clamp(18px, 1.6vw, 26px)/1.1 var(--font-hand); transform: rotate(-1.5deg); }
.story-comic .p1 { --o: clamp(0, (var(--p) - .04) * 6, 1); }
.story-comic .p2 { --o: clamp(0, (var(--p) - .24) * 6, 1); }
.story-comic .p3 { --o: clamp(0, (var(--p) - .44) * 6, 1); }
.story-comic .p4 { --o: clamp(0, (var(--p) - .64) * 6, 1); }

/* 005 · Episode One */
.story-episode { color: var(--text-on-media); background: var(--ink-1000); }
.story-episode-art { position: absolute; inset: 0; transform: scale(calc(1.12 - .12 * var(--in))); }
.story-episode-art img, .story-episode-art video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 30%; }
video[data-story-loop] { opacity: 0; transition: opacity var(--dur-slow) var(--ease-out); }
video[data-story-loop].playing { opacity: 1; }
.story-episode .story-pin::after { content: ''; position: absolute; inset: 0; z-index: 1; background: linear-gradient(transparent 30%, var(--scrim-strong)); pointer-events: none; }
.story-episode .story-copy p { color: var(--text-on-media); }
.story-episode .countdown { margin-top: var(--space-5); }
.story-episode .story-hud { color: var(--text-on-media); }

/* 006 · Wake Frank: the doorway to the basement. Music video plays on the TV; the sleeve drops in. */
.story-song.signature-release { padding: 0; overflow: visible; background: var(--ink-1000); color: var(--text-on-media); --cta-ink: var(--bone-100); --cta-on-ink: var(--ink-1000); --cta-hover-fill: var(--bone-hover); --cta-border: var(--line-strong); }
.story-room { position: absolute; inset: 0; overflow: clip; }
.story-room-scene { position: absolute; left: 50%; top: 50%; width: max(100%, calc(100svh * 1672 / 941)); aspect-ratio: 1672 / 941; transform: translate(-50%, -50%) scale(calc(1.08 - .08 * var(--in))); }
.story-room-scene > img { width: 100%; height: 100%; opacity: .55; }
.story-tv { position: absolute; left: 0; top: 0; width: 320px; height: 256px; transform-origin: 0 0; overflow: hidden; border-radius: 8% / 10%; background: var(--ink-1000); }
.story-tv img, .story-tv video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.story-room::after { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, transparent 30%, var(--scrim-strong) 62%); pointer-events: none; }
.story-song-inner { position: relative; z-index: 2; height: 100svh; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; padding-top: var(--header-height); }
.story-song-stack { grid-column: 2; }
.story-song .story-copy p, .story-song .story-aside { color: var(--text-on-media) !important; }
.story-song .story-hud { color: var(--text-on-media); }

/* 007 / 008 · One collection rail for the band and the merch */
.story-rail-ch .story-pin { display: grid; grid-template-rows: auto 1fr; align-content: center; gap: var(--space-6); padding-top: var(--header-height); }
.story-rail-head { display: flex; justify-content: space-between; align-items: end; gap: var(--space-6); }
.story-rail-head h2 { font-size: var(--type-d2); }
.story-rail-ch .story-pin > *, .story-rail-head > div { min-width: 0; }
.story-rail-head > a { flex: none; }
.story-rail-head p { color: var(--text-secondary); margin: var(--space-3) 0 0; font-size: var(--type-lead); }
.story-rail { overflow: clip; padding: var(--space-4) 0 var(--space-6); }
.story-track { display: flex; gap: var(--space-5); padding: 0 max(var(--gutter), calc((100vw - var(--content-max)) / 2)); width: max-content; transform: translateX(calc(var(--p) * -1 * var(--overflow, 0px))); }
.story-card { position: relative; flex: none; width: clamp(220px, min(24vw, 36svh), 380px); color: inherit; text-decoration: none; transition: transform var(--dur-medium) var(--ease-out); }
.story-card:nth-child(odd) { transform: rotate(-1.5deg); }
.story-card:nth-child(even) { transform: rotate(1.5deg) translateY(var(--space-5)); }
.story-card-art { position: relative; display: block; aspect-ratio: 4 / 5; overflow: clip; background: var(--surface-raised); border: 1px solid var(--line); }
.story-card-art img, .story-card-art video, .story-card-far { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.story-card-art video { opacity: 0; transition: opacity var(--dur-base) var(--ease-out); }
.story-card-art video.playing { opacity: 1; }
/* Merch push-in: far shot in front, close-up behind; in the spotlight the far shot zooms toward you and dissolves */
.story-merch-card .story-close { transform: scale(1.12); transition: transform var(--dur-slow) var(--ease-out); }
.story-card-far { z-index: 1; transform-origin: 50% 40%; transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out); }
.story-merch-card.is-active .story-close { transform: scale(1); }
.story-merch-card.is-active .story-card-far { opacity: 0; transform: scale(1.35); }
.story-card-meta { display: flex; justify-content: space-between; gap: var(--space-3); padding-top: var(--space-3); }
.story-card-title { font: var(--weight-bold) var(--type-d5)/1 var(--font-display); text-transform: uppercase; }
.story-card-sub { font: var(--weight-medium) var(--type-label-s)/1.6 var(--font-mono); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--text-secondary); }
.story-card:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 6px; }
@media (hover: hover) and (pointer: fine) {
  .story-card:hover, .story-card:focus-visible { transform: rotate(0) translateY(calc(var(--lift-m) * -1)); }
}

/* Spotlight: one card at a time steps forward; the rest wait in the dark */
.story-card { transition: transform var(--dur-slow) var(--ease-out); }
.story-card.is-active { transform: rotate(0) scale(1.04); z-index: 1; }
.story-card.is-active .story-card-art { border-color: var(--accent); }
.story-card-bio { display: block; max-width: 34ch; margin-top: var(--space-2); font-size: var(--type-small); line-height: var(--leading-body); color: var(--text-secondary); opacity: 0; transform: translateY(var(--space-2)); transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out); }
.story-card.is-active .story-card-bio { opacity: 1; transform: none; }
.story-static .story-card-bio { opacity: 1; transform: none; }
.story-static .story-card-far { opacity: 0; }
.story-static .story-merch-card .story-close { transform: none; }

/* 009 · Finale: the dead crowd rises in three layers, pinned to the bottom so the front row always shows.
   --p lifts the layers at different speeds; --mx / --my (pointer, -1→1, fine pointers only) shift them by the
   README distances (3 / 9 / 18 px at 1686 px wide). This replaces the footer's crowd on the story homepage. */
.story-finale { background: var(--ink-1000); color: var(--text-on-media); }
.story-finale .story-pin { display: grid; grid-template-rows: auto 1fr; }
.story-finale-inner { position: relative; z-index: 3; grid-row: 1; padding: calc(var(--header-height) + var(--space-3)) 0 var(--space-4); }
.story-finale h2 { margin: var(--space-3) 0 var(--space-4); font: var(--weight-bold) clamp(48px, 8vw, 150px)/var(--leading-display) var(--font-display); text-transform: uppercase; letter-spacing: var(--tracking-display); }
.story-crowd { position: relative; grid-row: 2; min-height: 0; overflow: clip; container-type: size; }
/* At least as wide as the stage and as tall as it; anchored at the bottom, so any excess crops the sky, never the front row */
.story-crowd-scene { position: absolute; left: 50%; bottom: 0; width: max(100cqw, calc(100cqh * 1686 / 933)); aspect-ratio: 1686 / 933; transform: translateX(-50%) scale(1.05); transform-origin: 50% 100%; --u: calc(100vw / 1686); }
.crowd-layer { position: absolute; left: 0; top: 0; width: 100%; height: 100%; }
.crowd-layer.l1 { transform: translate(calc(var(--mx) * 3 * var(--u)), calc((1 - var(--p)) * 6% + var(--my) * 2 * var(--u))); }
.crowd-layer.l2 { transform: translate(calc(var(--mx) * 9 * var(--u)), calc((1 - var(--p)) * 14% + var(--my) * 5 * var(--u))); }
.crowd-layer.l3 { transform: translate(calc(var(--mx) * 18 * var(--u)), calc((1 - var(--p)) * 26% + var(--my) * 10 * var(--u))); }
.story-crowd::after { content: ''; position: absolute; inset: 0 0 auto; height: 22%; z-index: 2; background: linear-gradient(var(--ink-1000), transparent); pointer-events: none; }
.crowd-finale { display: none; }  /* the story homepage's crowd is chapter 009 */

/* Motion off: everything visible, nothing pinned or moving */
.story-static .story-rain { animation: none; }
.story-static .story-track { transform: none; width: auto; overflow-x: auto; scroll-snap-type: x mandatory; padding-bottom: var(--space-4); }
.story-static .story-card { scroll-snap-align: start; }
.story-static .story-finale .story-pin { min-height: 100svh; }
.story-static .story-dunhollow .story-pin::after { opacity: 1; }
.frank-motion-off .story-rain { animation: none; }

/* Tablet */
@media (max-width: 1100px) {
  .story-escape-grid { grid-template-columns: 1fr; gap: var(--space-6); align-content: center; }
  .story-comic { height: 46svh; }
  .story-song-stack { grid-column: 1 / 3; justify-self: end; max-width: 640px; }
}

/* Phones: lighter. No horizontal pinning, no rain, fewer moving parts. */
@media (max-width: 760px) {
  .story-hud span + span { display: none; }
  /* Phones: the words sit above Frank's head, the portrait fills the rest of the screen */
  .story-frank .story-pin { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); align-items: stretch; }
  .story-frank-copy { grid-column: 1; grid-row: 1; padding: calc(var(--header-height) + var(--space-2)) var(--gutter) var(--space-3); }
  .story-frank-art { grid-column: 1; grid-row: 2; height: 100%; }
  .story-frank-art img { object-position: 50% 12%; }
  .story-frank .story-aside { margin-top: var(--space-2) !important; }
  .story-frank .story-hud { display: none; }
  .story-words { font-size: clamp(72px, 26vw, 120px); }
  .story-rain { display: none; }
  .story-escape { height: auto; }
  .story-escape .story-pin { position: relative; height: auto; }
  .story-escape-grid { height: auto; padding: calc(var(--header-height) + var(--space-8)) var(--gutter) var(--space-9); }
  .story-comic { height: auto; grid-template-columns: 1fr; grid-template-rows: none; }
  .story-comic-panel { --o: 1 !important; aspect-ratio: 16 / 10; grid-row: auto !important; grid-column: auto !important; }
  .story-comic .p4 { display: block; }
  .story-rail-ch { height: auto; }
  .story-rail-ch .story-pin { position: relative; height: auto; padding-bottom: var(--space-8); }
  .story-rail-head { flex-direction: column; align-items: start; }
  .story-track { transform: none; width: auto; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; touch-action: pan-x pan-y; scroll-snap-type: x mandatory; padding-block: var(--space-3) var(--space-6); }
  .story-card { width: 72vw; scroll-snap-align: center; }
  .story-song { height: auto; }
  .story-song .story-pin { position: relative; height: auto; }
  .story-room { position: relative; height: 56svh; }
  .story-room-scene { width: 300%; left: -10%; top: -6%; transform: none; }
  .story-room::after { background: linear-gradient(transparent 60%, var(--ink-1000)); }
  .story-song-inner { height: auto; grid-template-columns: 1fr; padding: 0 var(--gutter) var(--space-9); margin-top: calc(var(--space-9) * -1); }
  .story-song-stack { grid-column: 1; justify-self: stretch; }
  .story-caption { font-size: 18px; }
}
@media (prefers-reduced-motion: reduce) { .story-rain { animation: none; } }
.story-song .story-aside { max-width: none; }
@media (min-width: 761px) { .story-song .story-aside { white-space: nowrap; } }
/* CTA rows: one height, centred on one line (the scheduled video wrapper otherwise stretched the row) */
.story .actions { align-items: center; flex-wrap: wrap; }
.story .actions .scheduled-video { display: inline-flex; }
/* Song CTAs: the two buttons side by side, "Listen on Spotify" always on its own line beneath them */
.story-song .actions { display: grid; grid-template-columns: max-content max-content; justify-content: start; }
.story-song .actions .text-link { grid-column: 1 / -1; justify-self: start; }
@media (max-width: 760px) { .story-song .actions { grid-template-columns: max-content; } }
@media (max-width: 760px) {
  .story-finale { height: auto; }
  .story-finale .story-pin { position: relative; height: 100svh; }
  /* Phones: scroll-linked layers stutter on iOS, so the crowd rises once, on the compositor, when it arrives */
  .crowd-layer { transition: transform var(--dur-scene) var(--ease-out); }
  .crowd-layer.l1 { transform: translateY(6%); }
  .crowd-layer.l2 { transform: translateY(14%); }
  .crowd-layer.l3 { transform: translateY(26%); }
  .story-finale.is-in .crowd-layer { transform: none; }
  .story-static .crowd-layer { transform: none; transition: none; }
}

/* Spacing pass (21 Oct, iPhone review) */
/* Small label above a big title: give the title room (tight display leading puts caps right under the label) */
.story-copy .eyebrow + h2, .story-copy .scheduled + h2, .story-copy > div > .eyebrow + h2, .story-copy .status + h2, .story-rail-head .eyebrow + h2 { margin-top: var(--space-4); }
.story-copy .eyebrow + .eyebrow { margin-top: var(--space-2); }
/* The countdown keeps its own small mono size (the story paragraph size was enlarging it) */
.story-copy p.countdown { font-size: var(--type-label); max-width: none; margin-top: var(--space-5); }
@media (max-width: 760px) {
  /* Stacked buttons: same width, clear gap */
  .story-ch .actions { display: grid; grid-template-columns: 1fr; gap: var(--space-4); max-width: 360px; }
  .story-ch .actions > * { margin: 0; }
  .story-ch .actions .scheduled-video { display: flex; flex-direction: column; }
  .story-ch .actions .button, .story-ch .actions .scheduled-video, .story-ch .actions .scheduled-video .button { width: 100%; }
  .story-ch .actions .text-link { justify-self: start; margin-top: var(--space-1); }
  /* The corner label repeats the section label on these chapters */
  .story-episode .story-hud, .story-song .story-hud { display: none; }
  /* Wake Frank: copy starts below the TV picture rather than over it */
  .story-song-inner { margin-top: 0; padding-top: var(--space-5); }
  /* Less dead space after the rails */
  .story-rail-ch .story-pin { padding-bottom: var(--space-6); gap: var(--space-5); }
}
/* 003 · Frank sits on the left of the Dunhollow art, so the copy takes the open rooftops on the right */
.story-dunhollow .story-copy-bottom { display: flex; flex-direction: column; align-items: flex-end; text-align: right; }
.story-dunhollow .story-aside { transform-origin: right; }
@media (max-width: 760px) { .story-dunhollow .story-copy-bottom { align-items: flex-start; text-align: left; } .story-dunhollow .story-aside { transform-origin: left; } }

/* Motion pass (21 Oct): chapter copy arrives the same way everywhere — rises 24px and fades in as its chapter enters.
   Chapters with their own staged copy (Frank's words, Dunhollow, the song's GSAP reveal) keep theirs. */
.story-escape .story-copy, .story-episode .story-copy, .story-rail-head, .story-finale-inner {
  --arrive: clamp(0, (var(--in) - .3) * 2.6, 1);
  opacity: var(--arrive); transform: translateY(calc((1 - var(--arrive)) * 24px));
}
.story-static .story-escape .story-copy, .story-static .story-episode .story-copy, .story-static .story-rail-head, .story-static .story-finale-inner { opacity: 1; transform: none; }

/* ── Motion pass 2 (21 Oct) ───────────────────────────────────────────────────────────────── */
/* Frank → Dunhollow: Frank's page ends in a torn paper edge (tear-page-mask.svg) with a strip of rough
   fibres along it (tear-fibre-mask.svg). The page overlaps the top of Dunhollow, so the city shows through the rip. */
.story-frank { z-index: 2; margin-bottom: -56px;
  -webkit-mask: linear-gradient(black, black) top / 100% calc(100% - 63px) no-repeat, url(/assets/tear-page-mask.svg) bottom / 100% 64px no-repeat;
  mask: linear-gradient(black, black) top / 100% calc(100% - 63px) no-repeat, url(/assets/tear-page-mask.svg) bottom / 100% 64px no-repeat; }
.story-frank::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 64px; z-index: 3; background: var(--bone-300); pointer-events: none;
  -webkit-mask: url(/assets/tear-fibre-mask.svg) center / 100% 100% no-repeat; mask: url(/assets/tear-fibre-mask.svg) center / 100% 100% no-repeat; }

/* 2 · The TV powers on (Darren, 22 Oct): a burst of static, the picture fades up underneath, the static clears,
   then a small flicker (no fill, so it can't hold the picture on before the fade). The static is SVG noise jumped to a new spot every 60ms, with faint scanlines. */
.story-tv-picture { position: absolute; inset: 0; }
.story-tv-static { position: absolute; inset: -50%; z-index: 1; opacity: 0; pointer-events: none;
  background: repeating-linear-gradient(0deg, color-mix(in srgb, var(--ink-1000) 22%, transparent) 0 1px, transparent 1px 4px), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='256'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.2' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='.33 .33 .33 0 0 .33 .33 .33 0 0 .33 .33 .33 0 0 0 0 0 0 1'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='4' intercept='-1.1'/%3E%3CfeFuncG type='linear' slope='4' intercept='-1.1'/%3E%3CfeFuncB type='linear' slope='4' intercept='-1.1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E") 0 0 / 320px 256px; }
.story-live .story-song:not(.tv-on) .story-tv-picture { opacity: 0; }
.story-live .story-song.tv-on .story-tv-picture { animation: story-tv-fade 1200ms var(--ease-in-out) 500ms both, story-tv-flicker 600ms linear 1800ms; }
.story-live .story-song.tv-on .story-tv-static { animation: story-tv-static 1900ms linear both, story-tv-jitter 300ms 7 both; }
@keyframes story-tv-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes story-tv-static { 0% { opacity: 0; } 5% { opacity: .95; } 45% { opacity: .85; } 100% { opacity: 0; } }
@keyframes story-tv-jitter { 0% { transform: translate(0, 0); animation-timing-function: steps(1, end); }
  20% { transform: translate(-13%, 7%); animation-timing-function: steps(1, end); } 40% { transform: translate(9%, -11%); animation-timing-function: steps(1, end); }
  60% { transform: translate(-6%, -4%); animation-timing-function: steps(1, end); } 80% { transform: translate(12%, 10%); animation-timing-function: steps(1, end); } 100% { transform: translate(0, 0); } }
@keyframes story-tv-flicker { 0%, 100% { opacity: 1; } 20% { opacity: .72; } 35% { opacity: 1; } 55% { opacity: .86; } }

/* 3 · Headlines slam in word by word: heavy, fast in, long settle, no bounce */
.slam-ready .slam-word { display: inline-block; opacity: 0; transform: translateY(.32em) scale(1.14); transform-origin: 50% 100%;
  transition: opacity var(--dur-medium) var(--ease-out) calc(var(--i) * 70ms), transform var(--dur-slow) var(--ease-out) calc(var(--i) * 70ms); }
.slammed .slam-ready .slam-word { opacity: 1; transform: none; }

/* 4 · Impact: cutting from the opening's black into Frank's face — the dark lifts and the portrait punches in */
.story-flash { position: absolute; inset: 0; z-index: 4; background: var(--ink-1000); opacity: 0; pointer-events: none; }
.story-frank.impact .story-flash { animation: story-flash 460ms var(--ease-out) both; }
.story-frank.impact .story-frank-art img { animation: story-punch 700ms var(--ease-out) both; }
@keyframes story-flash { from { opacity: .92; } to { opacity: 0; } }
@keyframes story-punch { from { transform: scale(1.07); } to { transform: none; } }

/* 5 · Countdown digits flick over (see story.js) */
.story .countdown-time { display: inline-block; }

@media (prefers-reduced-motion: reduce) { .story-flash, .story-tv-static { display: none; } }

/* Frank's portrait bleeds off the right and down into the torn edge; its left edge fades into the page */
@media (min-width: 761px) { .story-frank-art img { -webkit-mask-image: linear-gradient(90deg, transparent, black 24%); mask-image: linear-gradient(90deg, transparent, black 24%); } }

/* 002 with a transparent cut-out of Frank's head (site.storyFrankImage): he stands on the white page, his shoulders
   run down into the torn edge, and "Last." tucks behind his jaw while "Born." and "Week." stay in front. */
.story-frank-cutout .story-frank-art { position: relative; z-index: 2; }
.story-frank-cutout .story-frank-art img { position: absolute; right: 2%; bottom: -14%; width: auto; height: 108%; max-width: none; object-fit: contain; -webkit-mask-image: none; mask-image: none; transform-origin: 50% 60%; }
.story-frank-cutout .story-frank-copy { z-index: auto; }
/* All three words travel behind Frank's head (the head layer is z-index 2) */
.story-frank-cutout .story-words .w { position: relative; z-index: 1; }
@media (max-width: 760px) {
  /* Phones: sized by the space under the words, centred, shoulders running down into the torn edge */
  /* Safari stretches a left+right pinned image to full width, so centre from the middle with `translate`
     (separate from the punch animation's transform) and give the box the image's own ratio. */
  /* The pinned page is as tall as the screen with Safari's toolbar collapsed (lvh), so no white strip opens under
     it, and his neck runs off the bottom of the screen (about 86% down the art before scrolling, 92% after).
     The chapter's own torn edge takes over as it scrolls away. */
  .story:not(.story-static) .story-frank-cutout .story-pin { height: 100lvh; }
  .story-frank-cutout .story-frank-art img { left: 50%; right: auto; top: auto; bottom: -9%; margin: 0; width: auto; height: 112%; aspect-ratio: 1836 / 2048; max-width: none; translate: -50% 0; }
}

/* Darren's layout (21 Oct): "Last." steps in a little; "Big for my age" sits under the middle of "Week." */
.story-frank-copy { --wsize: clamp(88px, 15vw, 260px); }
.story-frank .story-words { font-size: var(--wsize); }
.story-frank .story-words .w2 { margin-left: .8em; }
.story-frank .story-aside { margin-left: calc(var(--wsize) * 1.15) !important; margin-top: var(--space-5) !important; }
@media (max-width: 760px) { .story-frank-copy { --wsize: clamp(72px, 26vw, 120px); } .story-frank .story-words .w2 { margin-left: .6em; } }

/* Sound (21 Oct): an invitation on the opening and a switch in the corner. Nothing plays before a tap. */
.story-sound-prompt { display: flex; align-items: center; justify-content: center; }
/* Desktop: equal outer columns keep the start graphic centred on the screen, whatever sits either side */
@media (min-width: 761px) {
  .story .opening-controls:has(.story-sound-prompt:not([hidden])) { display: grid; grid-template-columns: 1fr auto 1fr; }
  .story .opening-controls:has(.story-sound-prompt:not([hidden])) .actions { justify-self: end; }
}
/* Start graphic: two red tags bridged by the brushed speaker ring (the tag + ring are one button) */
.story-start-sound { display: inline-flex; align-items: center; padding: 0; border: 0; background: none; color: inherit; cursor: pointer; }
/* Both tags share one width (sized to "Scroll to awaken ↓", 18 mono characters) so the ring sits dead centre; --st-in is the end tucked under the ring */
.story-sound-prompt { --st-track: var(--tracking-label); --st-out: var(--space-4); --st-in: 24px; --st-ring: 88px; --st-overlap: 12px; }
.story-start-tag { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); min-width: calc(18 * (1ch + var(--st-track)) + var(--st-out) + var(--st-in)); min-height: 34px; padding: 0 var(--st-in) 0 var(--st-out); background: var(--accent); color: var(--bone-100); font: var(--weight-medium) var(--type-label-s)/1 var(--font-mono); letter-spacing: var(--st-track); text-transform: uppercase; white-space: nowrap; }
.story-start-tag svg { width: 14px; height: 14px; stroke-width: 2; transition: translate var(--dur-fast) var(--ease-out); }
.story-start-ring { position: relative; z-index: 1; display: block; width: var(--st-ring); height: var(--st-ring); margin-inline: calc(var(--st-overlap) * -1); color: var(--accent); transition: scale var(--dur-fast) var(--ease-out); }
.story-start-ring svg { display: block; width: 100%; height: 100%; }
.story-start-scroll { padding: 0 var(--st-out) 0 var(--st-in); }
@media (hover: hover) and (pointer: fine) {
  .story-start-sound:hover .story-start-ring { scale: 1.06; }
  .story-start-sound:hover .story-start-tag svg { translate: 3px 0; }
}
.story-start-sound:focus-visible { outline: none; }
.story-start-sound:focus-visible .story-start-ring { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: 50%; }
.story-start-sound:active .story-start-ring { scale: .96; }
/* After the choice: only the scroll tag stays */
.story-sound-prompt.is-done .story-start-sound { display: none; }
.story-sound-prompt.is-done .story-start-scroll { min-width: 0; padding: 0 var(--st-out); }
/* The graphic carries "Scroll to awaken" on the story homepage; the plain cue only shows if the graphic can't (no Web Audio) */
.story .opening-controls:has(.story-sound-prompt:not([hidden])) .scroll-cue { display: none; }
.story-sound-prompt[hidden] { display: none; }
.story-sound-note { font: var(--weight-medium) var(--type-label-s)/1.4 var(--font-mono); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--text-on-media); opacity: .8; }
.story-sound-toggle { position: fixed; right: var(--gutter); bottom: var(--space-4); z-index: var(--z-raised); display: inline-flex; align-items: center; gap: var(--space-2); min-height: var(--cta-height); padding: 0 var(--space-3); background: var(--scrim-mid); color: var(--text-on-media); border: 0; font: var(--weight-medium) var(--type-label-s)/1 var(--font-mono); letter-spacing: var(--tracking-label); text-transform: uppercase; }
.story-sound-toggle[hidden] { display: none; }
.story-sound-toggle:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px; }
.story-sound-bars { display: inline-flex; align-items: flex-end; gap: 2px; height: 12px; }
.story-sound-bars i { display: block; width: 2px; height: 100%; background: currentColor; transform: scaleY(.25); transform-origin: bottom; }
.story-sound-toggle.is-on .story-sound-bars i { animation: story-bars 900ms var(--ease-in-out) infinite alternate; }
.story-sound-toggle.is-on .story-sound-bars i:nth-child(2) { animation-delay: -300ms; }
.story-sound-toggle.is-on .story-sound-bars i:nth-child(3) { animation-delay: -600ms; }
@keyframes story-bars { from { transform: scaleY(.25); } to { transform: scaleY(1); } }
.frank-motion-off .story-sound-toggle.is-on .story-sound-bars i { animation: none; transform: scaleY(.7); }
@media (prefers-reduced-motion: reduce) { .story-sound-toggle.is-on .story-sound-bars i { animation: none; transform: scaleY(.7); } }
@media (max-width: 760px) {
  .story-sound-prompt { order: -1; width: 100%; margin-bottom: var(--space-3); }
  .story-sound-prompt { --st-track: .04em; --st-out: 10px; --st-in: 14px; --st-ring: 64px; --st-overlap: 8px; }
  .story-start-tag { gap: var(--space-1); min-height: 30px; }
  .story-start-tag svg { width: 12px; height: 12px; }
}
@media (max-width: 360px) { .story-sound-prompt { --st-track: .02em; --st-out: 4px; --st-in: 10px; --st-ring: 50px; --st-overlap: 8px; margin-inline: -8px; width: calc(100% + 16px); } }

/* 003 · The city assembles (with three layers). Scroll windows: sky fades up (a1), the skyline rises (a2),
   Frank and the near roofs rise last (a3), then "Dunhollow." rises from behind those roofs (a4).
   DOM order is sky, skyline, copy, Frank — so the copy sits between the skyline and Frank's layer. */
.story-assemble { --a1: clamp(0, var(--p) / .18, 1); --a2: clamp(0, (var(--p) - .06) / .3, 1); --a3: clamp(0, (var(--p) - .3) / .26, 1); --a4: clamp(0, (var(--p) - .56) / .28, 1); }
.story-assemble .story-scene { position: absolute; inset: 0; overflow: clip; }
.story-assemble .story-layer { transform: translate(calc(var(--mx) * var(--drift, 0) * 100vw / 1674), calc(var(--rise, 0px) + var(--lift, 0px) + var(--p) * var(--speed) * -6svh + var(--my) * var(--drift, 0) * .5 * 100vw / 1674)) scale(1.06); }
/* The red sky comes up while the chapter is still scrolling in (--in), so the tear from Wake Frank never opens onto black */
.story-assemble .story-scene > .story-layer:nth-child(1) { opacity: max(var(--a1), min(1, var(--in) / .6)); }
/* The skyline fades in through the haze and settles a short way, like a view clearing, not a town popping up */
.story-assemble .story-scene > .story-layer:nth-child(2) { opacity: var(--a2); --rise: calc((1 - var(--a2)) * 8svh); }
.story-assemble .story-layer-front { --rise: calc((1 - var(--a3)) * 50svh); }
.story-assemble .story-layer-silhouette { opacity: clamp(0, var(--a3) * 4, 1); }
.story-assemble .story-layer-silhouette img { filter: brightness(0); }
.story-assemble .story-layer-front:not(.story-layer-silhouette) { opacity: clamp(0, (var(--a3) - .55) / .45, 1); }
/* Starts fully below the screen (its own height + the gap), so nothing peeks before the roofs arrive */
.story-assemble .story-copy { z-index: auto; top: auto; bottom: 40svh; opacity: 1; transform: translateY(calc((1 - var(--a4)) * (40svh + 100%))); }
.story-assemble .story-pin::after { display: none; }
@media (max-width: 760px) {
    /* Phones: Frank's rooftop sits a little higher and the copy comes up below his feet (Darren, 21 Oct) */
  .story-assemble .story-layer-front { --lift: -15svh; }
  .story-assemble .story-hud { display: none; }
  .story-assemble .story-copy { z-index: 3; top: auto; bottom: var(--space-4); opacity: var(--a4); transform: translateY(calc((1 - var(--a4)) * 8svh)); }
}
.story-static .story-assemble .story-copy { opacity: 1; transform: none; }
/* Headline behind the roofs, small lines in front of them (same layout, each block hides the other's half) */
.story-assemble .story-copy-back > :not(h2), .story-assemble .story-copy-front > h2 { visibility: hidden; }
.story-assemble .story-copy-front { opacity: clamp(0, (var(--a4) - .7) / .3, 1); transform: none; }
@media (max-width: 760px) { .story-assemble .story-copy-front { transform: none; } }
@media (max-width: 760px) { .story-assemble .story-copy h2 { font-size: min(var(--type-d1), 18.5vw); } }

/* No orphan words: balance story paragraphs, and break Dunhollow's line after "Wet roofs." on phones */
.story-copy p { text-wrap: pretty; }
@media (max-width: 760px) { .story-line { display: block; } }

/* Born. Last. Week. slide in from the right, from behind Frank's head, starting larger and settling as he does
   (Darren, 21 Oct). Each word takes a fifth of the chapter's scroll, one after another. */
.story-frank .story-words .w { transform: translateX(calc((1 - var(--o)) * 52vw)) scale(calc(1.3 - .3 * var(--o))); transform-origin: 50% 60%; opacity: clamp(0, var(--o) * 2.5, 1); }
.story-frank .story-words .w1 { --o: clamp(0, (var(--p) - .02) * 5, 1); }
.story-frank .story-words .w2 { --o: clamp(0, (var(--p) - .16) * 5, 1); }
.story-frank .story-words .w3 { --o: clamp(0, (var(--p) - .30) * 5, 1); }
.story-frank .story-aside { opacity: clamp(0, (var(--p) - .52) * 6, 1); }
@media (max-width: 760px) { .story-frank .story-words .w { transform: translateX(calc((1 - var(--o)) * 40vw)) scale(calc(1.2 - .2 * var(--o))); } }

/* Torn paper handovers (Darren chose these over stitches, 21 Oct): an outgoing chapter ends in a ripped edge that
   overlaps the next chapter, with a pale band of torn fibres. Each tear has its own shape. Dark pages tear grey. */
.story-torn { z-index: 2; margin-bottom: -56px; --tear-page: url(/assets/tear-page-mask-2.svg); --tear-fibre: url(/assets/tear-fibre-mask-2.svg); --fibre: var(--ink-550);
  -webkit-mask: linear-gradient(black, black) top / 100% calc(100% - 63px) no-repeat, var(--tear-page) bottom / 100% 64px no-repeat;
  mask: linear-gradient(black, black) top / 100% calc(100% - 63px) no-repeat, var(--tear-page) bottom / 100% 64px no-repeat; }
.story-torn.tear-3 { --tear-page: url(/assets/tear-page-mask-3.svg); --tear-fibre: url(/assets/tear-fibre-mask-3.svg); }
.story-torn::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 64px; z-index: 3; background: var(--fibre); pointer-events: none;
  -webkit-mask: var(--tear-fibre) center / 100% 100% no-repeat; mask: var(--tear-fibre) center / 100% 100% no-repeat; }
/* Keep content clear of the torn edge (it overlaps the bottom ~64px of a torn chapter) */
.story-escape-grid { padding-bottom: calc(64px + var(--space-6)); }
@media (max-width: 760px) {
  .story-escape-grid { padding-bottom: calc(64px + var(--space-9)); }
  .story-song-inner { padding-bottom: calc(64px + var(--space-9)); }
}
/* Footer spacing tightened (story homepage). On desktop the sound switch parks beside "Join the FRANK list";
   phones keep a small parking spot under it. */
.signup { padding-block: var(--space-6) var(--space-5); }
@media (max-width: 760px) { .signup { margin-bottom: 64px; } }
.story-sound-toggle { transition: translate var(--dur-medium) var(--ease-out); }
/* The crowd on its own closes the story after the band (no headline band) */
.story-finale-crowd .story-pin { grid-template-rows: 1fr; }
.story-finale-crowd .story-crowd { grid-row: 1; }
@media (max-width: 760px) {
  /* Dunhollow on phones: a natural-width button on the left, clear of the sound switch on the right */
  .story-assemble .actions { display: flex; max-width: none; margin-top: var(--space-4); }
  .story-assemble .actions .button { width: auto; min-height: var(--target-min); padding: 10px 16px; }
  .story-assemble .story-copy h2 { font-size: min(var(--type-d1), 16vw); }
  .story-assemble .story-copy p { margin-top: var(--space-2); }
  .story-assemble .story-aside { margin-top: var(--space-2) !important; }
}
/* Narrowest phones: the sound switch shows just its bars (its aria-label still says Turn sound on/off) */
@media (max-width: 360px) {
  .story-sound-toggle { width: var(--cta-height); padding: 0; justify-content: center; }
  .story-sound-toggle [data-sound-label] { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
}

/* Collection → band: a page turn into the music side (the 4th tear), with room under the tees for it */
.story-torn.tear-1 { --tear-page: url(/assets/tear-page-mask.svg); --tear-fibre: url(/assets/tear-fibre-mask.svg); }
.story-merch .story-pin { padding-bottom: calc(64px + var(--space-6)); }

/* Section labels on the story homepage (Darren, 21 Oct): a solid red tag with white uppercase type.
   DESIGN.md exception: red may fill these small labels only. */
.story .eyebrow { display: inline-flex; align-items: center; width: max-content; max-width: 100%; padding: 5px 10px 4px; background: var(--accent); color: var(--bone-100); font: var(--weight-semibold) var(--type-lead)/1.15 var(--font-body); letter-spacing: .01em; text-transform: uppercase; }
.story .eyebrow .dash { display: none; }
.story-copy .eyebrow + h2, .story-rail-head .eyebrow + h2 { margin-top: var(--space-5); }
@media (max-width: 760px) { .story .eyebrow { font-size: var(--type-small); padding: 4px 8px 3px; } }
