/* ---- Hero: Assassins Convention "living diorama" (5 bands, cursor-parallax depth, staggered entrance, ambient life) ---- */
.hero-scene { position: absolute; inset: 0; z-index: 1; pointer-events: none; overflow: hidden; --vig-x: 60%; --vig-y: 40%; }
.hero-scene::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: min(26%, 190px); z-index: 8;
  background: linear-gradient(to top, var(--seam), transparent);
}
/* Cinematic grade over the assembled scene (z6: above the art + mist, below the scrim + seam).
   A soft flat drop dims the whole scene a notch (reaper + moon included), while the vignette darkens
   the four corners toward the frame. The transparent hole is centred on the moon+reaper so the focal
   light stays lit; radii sit at the box so the corner stops are actually felt, not clipped outside it. */
.hero-scene::before {
  content: ""; position: absolute; inset: 0; z-index: 6; pointer-events: none;
  background:
    radial-gradient(90% 95% at var(--vig-x) var(--vig-y), transparent 40%, rgba(5,6,14,.34) 70%, rgba(4,5,12,.6) 92%),
    linear-gradient(rgba(4,5,12,.09), rgba(4,5,12,.09));
}
/* Layer = parallax (cursor depth) + bleed scale; .a-art = entrance; img = flip. Split so the three transforms never collide. */
.a-layer { position: absolute; inset: 0; will-change: transform;
  transform: translate3d(calc(var(--px, 0) * var(--depth, 0) * -1px), calc(var(--py, 0) * var(--depth, 0) * -.5px), 0) scale(1.06);
  transition: transform .3s cubic-bezier(.22, 1, .36, 1); }
.a-art { position: absolute; inset: 0; opacity: 1; will-change: transform, opacity; }
.a-art img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center 46%; display: block; }
.a-art img { transform: scaleX(-1); }  /* flip so the reaper lands on the right */
.a-art picture, .pl picture { display: contents; }
/* z back-to-front + parallax depth. Three moving planes: moon (far), foreground = landscape + pile
   (they share a depth so the pile stays locked to the path it rests on), assassin (near, moves most).
   Sky is pinned (depth 0). */
.a-sky       { z-index: 0; --depth: 0; }
.a-landscape { z-index: 1; --depth: 12; }
.a-moon      { z-index: 2; --depth: 4; }
.a-pile      { z-index: 3; --depth: 12; }
.a-assassin  { z-index: 4; --depth: 24; }
/* breathing moonlight glow behind the moon */
.a-moonglow { position: absolute; z-index: 1; left: 50%; top: 20%; width: 46vw; max-width: 620px; aspect-ratio: 1;
  transform: translate(-50%, -50%); opacity: .62; will-change: opacity, transform;
  background: radial-gradient(circle, rgba(224,231,255,.30), rgba(150,170,220,.10) 42%, transparent 66%); }
/* low mist drifting at the base */
.a-mist { position: absolute; z-index: 5; left: -25%; right: -25%; bottom: 0; height: 44%; mix-blend-mode: screen; will-change: transform;
  background: radial-gradient(120% 92% at 50% 118%, rgba(122,134,178,.18), transparent 62%); }
/* left-weighted scrim so the headline reads over the scene, fading to reveal the reaper on the right */
.a-scrim { position: absolute; inset: 0; z-index: 7;
  background: linear-gradient(90deg, rgba(7,8,16,.97) 0%, rgba(7,8,16,.92) 32%, rgba(7,8,16,.66) 48%, rgba(7,8,16,.2) 64%, transparent 78%); }
/* --- Cinematic entrance, each beat its own motion. The scene births first and settles into a still
   tableau; the question then holds ALONE over it before any UI arrives:
   moon drops in from the top -> moonlight reveals the land -> the victims settle -> the killer looms
   in slowly -> "Who can you trust?" resolves from fog and HOLDS -> nav + the rest assemble top-down --- */
.js .a-sky       .a-art { animation: aFade .5s ease-out both; }
.js .a-moon      .a-art { animation: aMoonDrop 1s cubic-bezier(.33, .9, .4, 1) both; }
.js .a-landscape .a-art  {
  -webkit-mask: radial-gradient(circle at 50% 22%, #000 0, #000 var(--mr, 150%), transparent calc(var(--mr, 150%) + 20%));
          mask: radial-gradient(circle at 50% 22%, #000 0, #000 var(--mr, 150%), transparent calc(var(--mr, 150%) + 20%));
  animation: aReveal .95s cubic-bezier(.22, 1, .36, 1) .24s both;
}
.js .a-pile      .a-art  { animation: aSettle .7s cubic-bezier(.22, 1, .36, 1) .6s both; }
.js .a-assassin  .a-art  { animation: aLoom 1.15s cubic-bezier(.16, 1, .3, 1) .9s both; }
/* the scene births fast but the reaper looms in slowly (~2.05s); the headline resolves SLOWLY from fog
   (~1.35s), then the UI assembles top-down: nav drops in (landing.css, .92s), then the tags (Online Werewolf
   Game / Free to play), then lede (.5s after the headline's own delay -- was 2.08s, read as "~2s" and too
   long), actions, proof. Same relative order/spacing as the original, just compressed onto a much shorter
   anchor -- was nav 2.5s / tags 2.6s / lede 2.8s / actions 2.95s / proof 3.1s. */
.js .hero-copy h1 { animation: aFog 1.35s   cubic-bezier(.22, 1, .36, 1) .72s  both; }
.js .hero-tags    { animation: aCopyUp .6s  cubic-bezier(.25, 1, .5, 1)  1.02s both; }
.js .lede         { animation: aCopyUp .7s  cubic-bezier(.25, 1, .5, 1)  1.22s both; }
.js .hero-actions { animation: aCopyUp .7s  cubic-bezier(.25, 1, .5, 1)  1.37s both; }
.js .proof-strip  { animation: aCopyUp .7s  cubic-bezier(.25, 1, .5, 1)  1.52s both; }
/* ambient loops: the moon glow ignites, then breathes; mist drifts */
.js .a-moonglow { animation: aGlowIn 1.1s cubic-bezier(.22, 1, .36, 1) both, aBreathe 6s ease-in-out 2.8s infinite; }
.js .a-mist     { animation: aMist 26s ease-in-out 2s infinite alternate; }
/* registered so the moonlight mask radius interpolates (falls back to fully-revealed if unsupported) */
@property --mr { syntax: '<length-percentage>'; inherits: false; initial-value: 150%; }
@keyframes aFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes aReveal { from { --mr: 0%; opacity: .15; } to { --mr: 150%; opacity: 1; } }
@keyframes aMoonDrop { from { opacity: 0; transform: translateY(-92px) scale(.94); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes aGlowIn { from { opacity: 0; transform: translate(-50%, -50%) scale(.38); } to { opacity: .55; transform: translate(-50%, -50%) scale(1); } }
@keyframes aFog { from { opacity: 0; filter: blur(14px); transform: translateY(16px); } to { opacity: 1; filter: blur(0); transform: translateY(0); } }
@keyframes aSettle { from { opacity: 0; transform: translateY(42px) scale(1.05); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes aLoom { from { opacity: 0; transform: translateX(74px) scale(1.06); } to { opacity: 1; transform: translateX(0) scale(1); } }
@keyframes aRise { from { opacity: 0; transform: translateY(46px) scale(1.03); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes aCopyUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
@keyframes aBreathe { 0%, 100% { opacity: .55; transform: translate(-50%, -50%) scale(1); } 50% { opacity: .9; transform: translate(-50%, -50%) scale(1.07); } }
@keyframes aMist { from { transform: translateX(-4%); } to { transform: translateX(5%); } }
/* mobile: portrait bands, no cursor parallax, vertical scrim */
@media (max-width: 767px) {
  /* fill the viewport; the copy stretches to full height (landing.css) so the pitch sits at the top and the
     action band drops to the bottom, with the scene breathing through the clear middle band of the scrim */
  .hero { min-height: 100svh; align-items: stretch; }
  .a-art img { transform: none; object-position: center; }
  .js .a-assassin .a-art { animation-name: aRise; }
  .a-layer { transform: scale(1.04); }
  .hero-scene { --vig-x: 50%; --vig-y: 44%; }
  /* letterbox scrim: dark top (pitch) -> clear middle (moon + villagers) -> gently darker bottom. Kept LIGHT
     through the reaper zone (56-74%) so its red eye + fire glow through; the ::after seam gradient + ::before
     vignette already carry the very-bottom darkening, so this must not pile a third heavy layer on top (that
     crushed the reaper). Action-band text legibility is handled by a text-shadow in landing.css, not scrim. */
  .a-scrim { background: linear-gradient(180deg, rgba(7,8,16,.94) 0%, rgba(7,8,16,.8) 24%, rgba(7,8,16,.3) 42%, rgba(7,8,16,.22) 56%, rgba(7,8,16,.28) 74%, rgba(7,8,16,.48) 90%, rgba(7,8,16,.5) 100%); }
  .a-moonglow { top: 15%; width: 72vw; }
  /* ease the vignette on portrait so the dark foreground hill on the lower-left reads as form, not a crushed
     black void (the letterbox scrim already carries the mood; the corners don't need extra crush). */
  .hero-scene::before { background: radial-gradient(96% 104% at var(--vig-x) var(--vig-y), transparent 46%, rgba(5,6,14,.2) 74%, rgba(4,5,12,.4) 96%), linear-gradient(rgba(4,5,12,.06), rgba(4,5,12,.06)); }
}
@media (prefers-reduced-motion: reduce) {
  .a-layer { transition: none !important; transform: scale(1.03) !important; }
  .a-art { animation: none !important; opacity: 1 !important; transform: none !important; -webkit-mask: none !important; mask: none !important; }
  .a-mist { animation: none !important; }
  .a-moonglow { animation: none !important; opacity: .7 !important; transform: translate(-50%, -50%) !important; }
  .hero-copy > * { animation: none !important; opacity: 1 !important; transform: none !important; filter: none !important; }
}

.footer-village { position: relative; isolation: isolate; }
.footer-village > .section { position: relative; z-index: 2; }
.village-scene {
  position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; --vp: 1;
  background: linear-gradient(180deg,
     var(--bg) 0%, var(--bg) 20%, #20335c 33%, #3f6fb0 43%,
     #69b4e6 53%, #8addf6 62%, #9BE7F9 70%, #7aa84e 81%, #608419 90%);
}
.village-frame {
  position: absolute; left: 50%; bottom: 0; transform: translateX(-50%);
  width: max(101%, clamp(1180px, 164%, 2560px)); aspect-ratio: 16 / 9;
  background: linear-gradient(to top, #608419 0, #608419 7%, transparent 13%);
}
.vlayer {
  position: absolute; inset: 0; will-change: transform;
  transform: translate3d(0, calc((1 - var(--vp)) * var(--depth, 0) * 1px), 0);
}
.vart { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: fill; display: block; }
.v-fill     { --depth: 38; z-index: 0; }
.v-fill .vart { background: linear-gradient(to top, #608419 0%, #608419 38%, #66992a 46%, transparent 52%); }
.v-clouds   { --depth: 38; z-index: 1; }
.v-sun      { --depth: 38; z-index: 2; }
.v-hills    { --depth: 38; z-index: 3; }
.v-treeline { --depth: 38; z-index: 4; }
.v-mound    { --depth: 98; z-index: 5; }
.v-chars    { --depth: 98; z-index: 6; }
.js .vart       { opacity: 0; }
.js .v-fill     .vart { opacity: clamp(0, calc((var(--vp) - .16) * 5.5), 1); }
.js .v-clouds   .vart { opacity: clamp(0, calc( var(--vp)        * 8.5), 1); }
.js .v-sun      .vart { opacity: clamp(0, calc((var(--vp) - .08) * 6.2), 1); }
.js .v-hills    .vart { opacity: clamp(0, calc((var(--vp) - .18) * 5.5), 1); }
.js .v-treeline .vart { opacity: clamp(0, calc((var(--vp) - .30) * 4.5), 1); }
.js .v-mound    .vart { opacity: clamp(0, calc((var(--vp) - .42) * 5),   1); }
.js .v-chars    .vart { opacity: clamp(0, calc((var(--vp) - .52) * 5),   1); }
.village-scene.is-in .v-clouds .vart { animation: vDrift 26s ease-in-out infinite alternate; }
.village-scene.is-in .v-sun    .vart { animation: vSun 7s ease-in-out infinite; transform-origin: 50% 42%; }
@keyframes vDrift { from { transform: translateX(-1.6%); } to { transform: translateX(1.8%); } }
@keyframes vSun   { 0%,100% { transform: scale(1); } 50% { transform: scale(1.05); } }
.footer-village .faq .section-head h2 { color: #16243f; text-shadow: 0 1px 1px rgba(255,255,255,.4); }
.footer-village .site-footer {
  position: relative; z-index: 2; margin-top: 0; border-top: none;
  padding-block: clamp(150px, 19vw, 230px) clamp(10px, 1.4vw, 16px);
  background: linear-gradient(to top, rgba(9,18,6,.66), rgba(9,18,6,.2) 54%, transparent 100%);
}
/* ============ Cinematic chapter (Apple-style pinned scrub) ============ */
/* Each chapter is TALL and its .chapter-pin sticks full-screen while you scroll through the extra height,
   so the scene assembles in lockstep with scroll. We never block or lock scrolling (that fights momentum);
   the pin just gives the animation dedicated scroll distance. landing.js reads scroll and sets --p 0..1
   across the pinned range, MONOTONICALLY: --p only ever climbs, so scrolling back up leaves the scene fully
   built instead of rewinding it. --scrub is how much scroll equals one full assembly (the pace dial). */
@property --p { syntax: '<number>'; inherits: true; initial-value: 0; }
/* --p defaults to 1 (fully-assembled) as a safety fallback: if landing.js never runs, the scroll-driven
   chapters render visible instead of stranded at opacity 0. In the normal case landing.js writes --p=0 on
   its first animation frame (onScroll() at init) before the chapter is scrolled into view, so this default
   is never visibly rendered. */
.chapter { position: relative; --p: 1; --scrub: 100svh; --scene-gap: clamp(64px, 12vh, 168px); height: 200vh; height: calc(100svh + var(--scrub)); margin-top: var(--scene-gap); }
/* chapterHow's pin took too much scroll to fully play out (--scrub is the pace dial, see above). Compressed
   to 2/3 of the shared default -- every --p threshold inside it (crowd/light, teeth/eyes, content) is a
   fraction of --scrub, so this proportionally compresses the whole sequence without needing to touch any of
   them individually. Desktop only: mobile's pacing is a separate, shared-across-chapters JS constant
   (MOBILE_SCRUB), not this custom property. */
.chapter--how { --scrub: calc(100svh * 2 / 3); }
/* Dark breather between consecutive scenes (desktop): a --seam strip in the top margin so scenes get a bit of
   room to breathe. Margin is excluded from offsetHeight, so the scrub/is-built math is untouched. Reset on mobile. */
.chapter::before { content: ""; position: absolute; left: 0; right: 0; top: calc(-1 * var(--scene-gap)); height: var(--scene-gap); background: var(--seam); pointer-events: none; }
/* Once the scene is fully built, landing.js drops the scrub spacer (and compensates scroll so nothing
   moves): the chapter becomes a normal one-screen section, so scrolling back up isn't the whole pinned
   distance again. Latched, so it stays built. */
.chapter.is-built { height: 100vh; height: 100svh; }
/* The pin is the full-screen stage. svh (not vh) so it matches the visible viewport on mobile, where a
   100vh box is taller than the visible area while the URL bar shows. Atmospheric backdrop (not flat black):
   it's what shows as the stage slides up, before the scene assembles over it. Per-scene overrides replace it. */
.chapter-pin { position: sticky; top: 0; height: 100vh; height: 100svh; overflow: hidden; isolation: isolate;
  background: radial-gradient(130% 100% at 50% 22%, #14151f 0%, #0a0a11 58%, #070709 100%); }
/* Scene seam: fade the top + bottom edge of every scene to the shared deep base so adjacent chapters meet
   in the same darkness (a soft cinematic dip) instead of a hard color cut. Sits above the bands + scrim
   (z8) but below the content (z10); pointer-through. Order-independent, so it blends every seam the same. */
.chapter-pin::after {
  content: ""; position: absolute; inset: 0; z-index: 9; pointer-events: none;
  background: linear-gradient(to bottom, var(--seam) 0, transparent 12%, transparent 88%, var(--seam) 100%);
}
.chapter-bg { position: absolute; inset: 0; z-index: 0; }
.pl {
  position: absolute; inset: 0; will-change: transform, opacity;
  transform: translate3d(0, calc((var(--p) - .38) * var(--d, 0) * -1px), 0) scale(1.08);
}
.pl img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.p-sky        { --d: 0;  z-index: 0; }
.p-moonforest { --d: 8;  z-index: 1; }
.p-crowdback  { --d: 18; z-index: 2; }
.p-crowdfront { --d: 28; z-index: 4; }
.p-teeth-front{ --d: 28; z-index: 5; }
.p-foreground { --d: 44; z-index: 6; }
.p-eyes       { --d: 26; z-index: 7; }
.js .pl { opacity: 0; }
/* Three smooth stages on a base that matches the villager torsos, so the crowd blends into the dark
   and is revealed (not popped): 1) crowd (front-to-back, blends in)  2) the moon backlight + red
   world rise, lifting the silhouettes out of the black  3) the grins and glowing eyes come alive.
   Stage 2 used to wait until --p=.12 and take until .34 to fully light (moonforest/sky (var(--p)-.12)*4.5)
   -- crowd (dark silhouettes) was already on screen well before any light, reading as a long dark scroll
   before anything brighter showed. Starting the light at 0 alongside the crowd (full by .20) removes that
   unlit stretch instead of just shortening it. */
.js .p-crowdfront { opacity: clamp(0, calc( var(--p)        * 12), 1); }
.js .p-foreground { opacity: clamp(0, calc( var(--p)        * 12), 1); }
.js .p-crowdback  { opacity: clamp(0, calc((var(--p) - .14) *  7), 1); }
.js .p-moonforest { opacity: clamp(0, calc( var(--p)        * 5), 1); }
.js .p-sky        { opacity: clamp(0, calc( var(--p)        * 5), 1); }
.js .p-teeth-front{ opacity: clamp(0, calc((var(--p) - .44) * 6), 1); }
/* Was start .50/full .667 -- by the time eyes finished, content (starts .66) was already fading in on top of
   it, so the payoff was essentially never seen uncovered. Pulled to start alongside the teeth beat and
   finish by .551 -- the same completion point AND the same .109 buffer before content (.66) that roles'
   wolf+paws already has (confirmed working), so eyes now gets a comparable moment fully lit before anything
   overlays it. --scrub (the chapter's overall scroll length) is untouched. */
.js .p-eyes       { opacity: clamp(0, calc((var(--p) - .44) * 9), 1); }
.chapter-scrim {
  position: absolute; inset: 0; z-index: 8; pointer-events: none; opacity: 0;
  background: radial-gradient(125% 100% at 50% 42%, rgba(7,3,5,.86), rgba(7,3,5,.55) 58%, rgba(7,3,5,.35));
}
.js .chapter-scrim { opacity: clamp(0, calc((var(--p) - .62) * 4), .9); }
.chapter-fg { position: absolute; inset: 0; z-index: 10; display: flex; align-items: center; justify-content: center; }
/* Was .66/.803: content was only fully visible for .197 of --p (~19.7vh) before the pin releases at 1.0 --
   on a fast scroll that window compresses to a near-instant flash. Pulled earlier (still the same shape, just
   an earlier start) so the same fully-visible stretch is ~35.7vh instead, without touching --scrub or
   is-built -- i.e. without holding or slowing the user's actual scroll input at all, just giving the content
   more of the existing scroll distance to sit still and readable in. */
.js .chapter-fg {
  opacity: clamp(0, calc((var(--p) - .50) * 7), 1);
  transform: translateY(calc((1 - clamp(0, calc((var(--p) - .50) * 7), 1)) * 26px));
}
.chapter-fg .section-head h2, .chapter-fg .section-head p, .chapter-fg .how-more { text-shadow: 0 2px 16px rgba(0,0,0,.8), 0 1px 3px rgba(0,0,0,.7); }
/* embers: realistic sparks rising behind the crowd (canvas particle sim in landing.js). z1 sits above the
   sky + moonforest backdrop but under the crowd, so the sparks emerge from behind the villagers. Fades in
   with the scene via --p. */
.ember-fx { position: absolute; inset: 0; z-index: 1; overflow: hidden; pointer-events: none; }
.js .ember-fx { opacity: clamp(0, calc((var(--p) - .12) * 5), 1); }
.ember-fx canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
/* the few front embers sit above the crowd + scrim (but under the text) so they read as foreground sparks */
.ember-fx--front { z-index: 9; }
/* the crowd scene carries the embers, so keep chapterHow's readability scrim lighter than the default .9
   (the step cards back their own text; heading + bridge line lean on text-shadow) so the sparks survive at rest. */
.js .chapter--how .chapter-scrim { opacity: clamp(0, calc((var(--p) - .58) * 3), .66); }
/* ---- How chapter: the round plays out in story order. Heading lands as the eyes finish glowing, then the
   three step cards arrive night -> day -> win, and the bridge line closes. Each card exposes its own reveal
   progress as --t (0..1, derived from the chapter's --p) so the art inside can run beats on that timeline:
   scene first, then the marker detail stamps on (pointing hand / victory ribbon). ---- */
/* Foreground gate: was .70/.843, giving only .157 of --p (~10.5vh, since --scrub is compressed to 2/3 here)
   before release -- the tightest of the three chapters, and the one most complained about. Anchored to right
   after the eyes beat finishes (.551) instead of .80: still holds content back until the scene's payoff has
   landed (same intent as before), but doubles the fully-visible stretch to ~20.5vh. Pulling further would
   mean content starts appearing before eyes finishes, undoing that separate fix, so this stays the tightest
   of the three by necessity, not by oversight. */
.js .chapter--how .chapter-fg {
  opacity: clamp(0, calc((var(--p) - .55) * 7), 1);
  transform: translateY(calc((1 - clamp(0, calc((var(--p) - .55) * 7), 1)) * 26px));
}
/* Button-cued arrival ("How it works"): the programmatic scroll drives --p fast, so the scroll-linked reveal
   above pops. Instead hold the copy hidden while the scene assembles, then fade it in on a gentle TIME
   transition once landed. Only landing.js's anchor handler adds these classes, so manual scrolling keeps its
   (fine) --p-driven timing. Higher specificity than the rule above, so it wins while cued. */
.js .chapter.fg-cue .chapter-fg {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity .9s ease .4s, transform .9s ease .12s;
}
.js .chapter.fg-cue.fg-in .chapter-fg {
  opacity: 1;
  transform: none;
}
/* The readability scrim dims the scene FOR the copy, so on button use it must arrive WITH the copy, not track
   the scroll (which darkens the scene before the held-back text shows). Match the copy's fade timing above. */
.js .chapter--how.fg-cue .chapter-scrim {
  opacity: 0;
  transition: opacity .9s ease .4s;
}
.js .chapter--how.fg-cue.fg-in .chapter-scrim {
  opacity: .66;
}
.js .chapter--how .step { opacity: var(--t, 1); transform: translateY(calc((1 - var(--t, 1)) * 30px)); }
.js .chapter--how .step--night { --t: clamp(0, calc((var(--p) - .64) * 9), 1); }
.js .chapter--how .step--day   { --t: clamp(0, calc((var(--p) - .72) * 9), 1); }
.js .chapter--how .step--win   { --t: clamp(0, calc((var(--p) - .80) * 9), 1); }
/* night: the moon swells in, then the wolf rises in front of it */
.js .chapter--how .art-moon { --m: clamp(0, calc(var(--t, 1) * 1.6), 1);
  opacity: calc(var(--m) * .95); transform: scale(calc(.9 + var(--m) * .1)); }
.js .chapter--how .art-wolf { --w: clamp(0, calc((var(--t, 1) - .25) * 2), 1);
  opacity: var(--w); transform: translateY(calc((1 - var(--w)) * 22px)); }
/* day: the mob marches up, then the accusing finger lands along its pointing axis */
.js .chapter--how .art-mob { --w: clamp(0, calc((var(--t, 1) - .2) * 2), 1);
  opacity: var(--w); transform: translateY(calc((1 - var(--w)) * 20px)); }
.js .chapter--how .art-hand { --k: clamp(0, calc((var(--t, 1) - .75) * 5), 1);
  opacity: var(--k); transform: rotate(38deg) translateX(calc((1 - var(--k)) * -14px)); }
/* win: the villager rises into frame, then the victory ribbon lands across his lower body */
.js .chapter--how .art-winner { --w: clamp(0, calc((var(--t, 1) - .15) * 2.4), 1);
  opacity: var(--w); transform: translate(-50%, calc((1 - var(--w)) * 20px)); }
.js .chapter--how .art-ribbon { --k: clamp(0, calc((var(--t, 1) - .45) * 3), 1);
  opacity: var(--k); transform: translate(-50%, calc((1 - var(--k)) * -10px)); }
.js .chapter--how .how-more { --b: clamp(0, calc((var(--p) - .88) * 9), 1);
  opacity: var(--b); transform: translateY(calc((1 - var(--b)) * 18px)); }
/* idle life once assembled (gated on .is-in like the beast/fireflies; none of these touch the imgs'
   reveal transforms: the moon breathes via a pseudo glow, the torches glow via filter, the ribbon
   glints via its own pseudo) */
.step--night .step-art::before { content: ""; position: absolute; width: 240px; height: 240px; right: -52px; top: -62px;
  border-radius: 50%; background: radial-gradient(circle, rgba(190,205,255,.34), rgba(150,170,220,.12) 48%, transparent 70%); opacity: .6; }
.chapter--how.is-in .step--night .step-art::before { animation: howMoonBreathe 7s ease-in-out infinite; }
.chapter--how.is-in .art-mob { animation: howTorch 3.4s ease-in-out infinite; }
.art-ribbon b::after { content: ""; position: absolute; top: 0; bottom: 0; left: -40%; width: 34%;
  transform: skewX(-18deg); background: linear-gradient(90deg, transparent, rgba(255,255,255,.30), transparent); }
.chapter--how.is-in .art-ribbon b::after { animation: howGlint 6s ease-in-out infinite; }
@keyframes howMoonBreathe { 0%, 100% { opacity: .5; transform: scale(1); } 50% { opacity: .9; transform: scale(1.08); } }
@keyframes howTorch { 0%, 100% { filter: drop-shadow(0 -2px 10px rgba(255,140,40,.16)); } 50% { filter: drop-shadow(0 -2px 18px rgba(255,150,50,.40)); } }
@keyframes howGlint { 0%, 72% { transform: skewX(-18deg) translateX(0); } 88%, 100% { transform: skewX(-18deg) translateX(430%); } }
/* ---- Creatures chapter (More than a game): same engine, q-* layers shared by wide + portrait ---- */
/* moonlight pooled at the bottom edge so the dark foreground grass reads from the very start */
.chapter--creatures .chapter-pin { background: radial-gradient(125% 78% at 50% 104%, #244a62 0%, #112c3e 40%, #06121b 76%); }
/* This scene deliberately pools moonlight at the bottom edge (so the dark grass reads from the start), which
   the shared bottom seam would crush. Keep the top seam but shrink the bottom fade to a thin sliver (6%). */
.chapter--creatures .chapter-pin::after { background: linear-gradient(to bottom, var(--seam) 0, transparent 12%, transparent 94%, var(--seam) 100%); }
.chapter--creatures .chapter-scrim { background: radial-gradient(125% 100% at 50% 44%, rgba(3,8,14,.86), rgba(3,8,14,.55) 58%, rgba(3,8,14,.35)); }
/* z back-to-front: trees, foliage, beast, then foreground grass + mushrooms */
.q-forest    { --d: 14; z-index: 0; }
.q-foliage   { --d: 26; z-index: 1; }
.q-beast     { --d: 28; z-index: 2; }
.q-grass     { --d: 46; z-index: 3; }
.q-mushrooms { --d: 42; z-index: 4; }
/* assemble front-to-back, details last: grass (present) -> trees -> mushrooms -> beast. The beast (the
   scene's bright payoff) used to wait until --p=.43 (full by .58) with a long unlit run-up before it -- too
   much dark scroll before anything bright, then only ~.08 of --p (content starts at .66) before content
   arrived on top of it. Pulled every stage forward so the beast is full by .223: content still starts at the
   unchanged .66, so it now gets to sit fully lit, alone, for a much longer stretch (.437 of --p) before
   content arrives. */
.js .q-grass     { opacity: clamp(0, calc(.82 + var(--p) * 20), 1); }
.js .q-forest    { opacity: clamp(0, calc( var(--p)        * 8), 1); }
.js .q-foliage   { opacity: clamp(0, calc( var(--p)        * 8), 1); }
.js .q-mushrooms { opacity: clamp(0, calc((var(--p) - .05) * 8), 1); }
.js .q-beast     { opacity: clamp(0, calc((var(--p) - .08) * 7), 1); }
.chapter--creatures.is-in .q-beast img { animation: beastFloat 7s ease-in-out infinite; }
@keyframes beastFloat { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-1.1%); } }
/* fireflies: drifting glow motes (JS-seeded), inside the scene BEHIND the beast (z1: above the forest +
   foliage backdrop, under the monster and foreground). fade in with the scene via --p; each mote hovers
   (bounded float) + twinkles only while the chapter is on screen (.is-in). Reworked hero-ember effect,
   recoloured warm-green. */
.firefly-fx { position: absolute; inset: 0; z-index: 1; overflow: hidden; pointer-events: none; }
.js .firefly-fx { opacity: clamp(0, calc((var(--p) - .05) * 8), 1); }
.firefly-fx i { position: absolute; border-radius: 50%; opacity: 0; will-change: transform, opacity;
  background: radial-gradient(circle, rgba(238,255,190,1), rgba(165,232,95,.55) 52%, transparent 72%);
  box-shadow: 0 0 12px 2px rgba(196,248,128,.7); }
.chapter--creatures.is-in .firefly-fx i {
  animation: ffFloat var(--fdur, 9s) ease-in-out var(--fdelay, 0s) infinite alternate,
             ffPulse var(--pdur, 3s) ease-in-out var(--pdelay, 0s) infinite; }
@keyframes ffFloat { from { transform: translate(0, 0); } to { transform: translate(var(--dx, 12px), var(--dy, -24px)); } }
@keyframes ffPulse { 0%, 100% { opacity: 0; } 45%, 55% { opacity: var(--max, .9); } }
/* ---- Roles chapter (tread carefully): scene assembles on scroll, then the carousel appears. Shared .chapter engine ---- */
.chapter--roles .chapter-pin { background: radial-gradient(125% 92% at 50% 32%, #251018 0%, #140a0f 46%, #07060a 100%); }
.chapter--roles .chapter-scrim { background: radial-gradient(125% 100% at 50% 42%, rgba(6,4,8,.86), rgba(6,4,8,.5) 58%, rgba(6,4,8,.32)); }
/* cursor-parallax depth (same as the hero): layers translate by --px/--py * their --depth; scale(1.05) overscan hides the shifting edges */
.chapter--roles .pl { will-change: transform;
  transform: translate3d(calc(var(--px, 0) * var(--depth, 0) * -1px), calc(var(--py, 0) * var(--depth, 0) * -.5px), 0) scale(1.05);
  transition: transform .3s cubic-bezier(.22, 1, .36, 1); }
.chapter--roles .pl img { object-position: center; }
/* z-order back-to-front (wolf behind lady, paws in front) + parallax depth (front layers move most); reveal order: background -> gray zombies -> green zombies -> lady -> werewolf + paws close in together */
.t-bg     { z-index: 0; --depth: 2; }
.t-zgray  { z-index: 1; --depth: 5; }
.t-zgreen { z-index: 2; --depth: 9; }
.t-wolf   { z-index: 3; --depth: 12; }
.t-lady   { z-index: 4; --depth: 16; }
.t-paws   { z-index: 5; --depth: 22; }
.js .t-bg     { opacity: clamp(0, calc( var(--p)        * 14), 1); }
.js .t-zgray  { opacity: clamp(0, calc((var(--p) - .10) * 9), 1); }
/* was start .20/full .311, leaving just the plain road on screen from .071 (bg done) to .20 -- too long empty.
   Now starts alongside the gray zombies (matching t-zgray's own start/rate) instead of waiting for them to
   finish, so the crowd fills in as one beat right after the road appears. --scrub and the lady are untouched. */
.js .t-zgreen { opacity: clamp(0, calc((var(--p) - .10) * 9), 1); }
/* the lady used to wait until zgreen was nearly done (start .32, full .431); brought forward to appear
   alongside the zombie crowd still assembling instead of after it. */
.js .t-lady   { opacity: clamp(0, calc((var(--p) - .12) * 9), 1); }
.js .t-wolf   { opacity: clamp(0, calc((var(--p) - .44) * 9), 1); }
.js .t-paws   { opacity: clamp(0, calc((var(--p) - .44) * 9), 1); }
/* ---- Mobile: the desktop pinned scrub holds a one-screen content overlay, which clips taller-than-screen
   content on a phone. So the scene becomes a full-viewport STICKY BACKDROP and the content scrolls OVER it,
   while --p stays SCROLL-SCRUBBED (landing.js, welded to the finger then latched): the existing --p reveal
   rules assemble the scene layer-by-layer as you scroll in, exactly like desktop, then the content scrolls
   over the assembled scene. The -100svh margin cancels the sticky's flow height so bg + scrim + content
   stack from the pin top; the content's padding-top is the scene's assemble-scroll room (matches MOBILE_SCRUB
   in landing.js) so the scene finishes assembling right as the content frames. Paired bands pick their portrait
   file via <picture>; pl-wide marks the wide-only bands, hidden below. ---- */
@media (max-width: 767px) {
  .village-frame { min-width: 0; width: 172%; }
  .pl-wide { display: none; }

  .chapter, .chapter.is-built { height: auto; margin-top: 0; }     /* no desktop between-scene gap on mobile */
  .chapter::before { display: none; }
  .chapter-pin { position: relative; height: auto; overflow: visible; }
  .chapter-pin::after { display: none; }                          /* seam pseudo assumes a 100svh box */
  /* The scene is the ONLY sticky element and keeps NORMAL flow (a real 100svh margin box), so it releases
     one screen before the chapter ends and scrolls UP as the content finishes, like desktop. (A -100svh
     margin-bottom would zero its margin box and pin it the whole chapter, letting the next scene scroll up
     over it.) The content is pulled up OVER the scene with a negative margin-TOP (content isn't sticky, so
     its zeroed margin box is harmless). The scrim rides along as a bg pseudo so it releases with the scene. */
  .chapter-bg { position: sticky; top: 0; height: 100svh; z-index: 0; }
  .chapter-scrim { display: none; }
  /* Seam: fade the scene's top + bottom edge to the shared dark so adjacent scenes meet in darkness as one
     scrolls up into the next (a soft dip, not a hard line). Always on, like the desktop seam; z8 sits above
     the bands. */
  .chapter-bg::before {
    content: ""; position: absolute; inset: 0; z-index: 8; pointer-events: none;
    background: linear-gradient(to bottom, var(--seam) 0, transparent 13%, transparent 87%, var(--seam) 100%);
  }
  /* Readability scrim, fades in with the scene (--p); z9 above the seam, below the content (z10). */
  .chapter-bg::after {
    content: ""; position: absolute; inset: 0; z-index: 9; pointer-events: none;
    background: linear-gradient(to bottom, transparent 34%, rgba(6,5,10,.42));
    opacity: clamp(0, calc((var(--p) - .3) * 3), 1);
  }
  .ember-fx--front { display: none; }                             /* front canvas would span the whole tall pin */
  .pl { transform: none !important; }                             /* no cursor-parallax / overscan on mobile */

  /* Content is pulled up over the sticky scene (margin-top) then flows/scrolls over it. Content's own
     opacity is driven by --p, which reaches 1 after MOBILE_SCRUB (landing.js) * one viewport of scroll --
     70svh. padding-top used to match that exactly (also 70svh), which meant full-opacity and "heading's own
     top edge reaches y=0, already exiting" happened at the SAME scroll position: the text was only ever
     fully legible right as it left the screen. padding-top is deliberately no longer equal to
     MOBILE_SCRUB*viewport: opacity timing (still 70svh of scroll, untouched) now finishes with the heading
     positioned mid-screen instead of at the very top edge, giving real dwell time before it scrolls past.
     min-height keeps its "padding-top + one screen" shape so the scene stays pinned behind the now-later-
     framing content for the same relative beat. */
  .chapter-fg {
    position: relative; z-index: 10; display: block; transform: none !important;
    margin-top: -100svh; padding-top: 120svh; min-height: 220svh;
  }
  .chapter-fg .container { padding-block: calc(var(--header-h) + 4vh) 16vh; }
  /* The scene gradient (.chapter-bg::after) only darkens the bottom, so the moon (top third) stays bright and
     the how heading that lands over it washes out. Give just the heading a soft radial readability pool that
     fades into the scene (no hard box). Step cards back their own text, so they don't need this. */
  .chapter--how .section-head { position: relative; }
  .chapter--how .section-head::before {
    content: ""; position: absolute; z-index: -1; inset: -20px -10% -12px; pointer-events: none;
    background: radial-gradient(115% 100% at 50% 42%, rgba(5,4,11,.74), rgba(5,4,11,.4) 46%, transparent 76%);
  }
}
@media (prefers-reduced-motion: reduce) {
  .vlayer { transform: none !important; }
  .vart { animation: none !important; opacity: 1 !important; transform: none !important; }
  .chapter { height: auto !important; }
  .chapter-pin { position: relative; }
  .pl { opacity: 1 !important; transform: none !important; }
  .pl img { animation: none !important; transform: none !important; }
  .ember-fx, .firefly-fx { display: none !important; }
  .chapter-scrim { opacity: .72 !important; }
  .chapter-fg { position: relative; opacity: 1 !important; transform: none !important; padding-block: 12vh; }
  /* steps: forcing --t to 1 settles every derived art beat into its final pose (keeps the static
     rotate/centering transforms intact, unlike a blanket transform: none) */
  .chapter--how .step { --t: 1 !important; }
  .chapter--how .how-more { opacity: 1 !important; transform: none !important; }
  .chapter--how .step-art::before, .chapter--how .art-mob, .chapter--how .art-ribbon b::after { animation: none !important; }
}
