/* ==========================================================================
   Cinematic scroll-film portfolio — styles.css (Task B2)
   Source of truth: /Design.md §2 tokens & type, §3 anatomy/states,
   §4 motion tokens, §5 a11y. DOM source of truth: /index.html.

   z-index stack (established, ascending):
     0 media (canvas/video) < 1 scrim < 2 grain < 3 content
     < 4 chrome (rail/counter/cursor/progressTop) < 5 preloader
     < 6 skip-link on focus (must beat the preloader overlay)

   Progressive enhancement: entrance-hidden default states (hero letters,
   pillars, reveal targets) are scoped under body.js — main.js's first
   statement is document.body.classList.add('js'), so without JS every
   letter/pillar/reveal target renders in its final, fully-visible state.
   ========================================================================== */

/* ---------- 1. Tokens ---------- */
:root {
  /* color */
  --ink-950: #0a0b12;
  --ink-900: #10121b;
  --ink-800: #161826;
  --gold-300: #e0bc84;
  --gold-500: #cda15e;
  --gold-700: #a97f41;
  --cream: #f1ede4;
  --cream-dim: rgba(241, 237, 228, .72);
  --line: rgba(241, 237, 228, .14);

  /* type */
  --font-display: 'Anton', 'Arial Narrow', 'Impact', sans-serif;
  --font-body: 'Plus Jakarta Sans', -apple-system, 'Segoe UI', sans-serif;

  /* motion */
  --ease-brand: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-move: cubic-bezier(0.77, 0, 0.175, 1);
  --dur-press: 140ms;
  --dur-ui: 240ms;
  --dur-reveal: 800ms;
  --dur-loader-exit: 400ms;

  /* layout */
  --margin: clamp(20px, 5vw, 72px);
  --rhythm: clamp(96px, 14vh, 160px);

  /* spacing — 13-step scale, nothing outside this set */
  --sp-0: 0px;
  --sp-2: 2px;
  --sp-4: 4px;
  --sp-8: 8px;
  --sp-12: 12px;
  --sp-16: 16px;
  --sp-24: 24px;
  --sp-32: 32px;
  --sp-40: 40px;
  --sp-48: 48px;
  --sp-64: 64px;
  --sp-80: 80px;
  --sp-96: 96px;

  /* radius — only these two; anything circular (dots/cursor) uses 50% */
  --radius-card: 16px;
  --radius-pill: 9999px;
}

/* ---------- 2. Base / reset-lite ---------- */
*, *::before, *::after { box-sizing: border-box; }

html {
  background: var(--ink-950);
  scroll-behavior: auto; /* Lenis/JS own smoothing; no native smooth-scroll double-up */
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--ink-950);
  color: var(--cream);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 26px;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

h1, h2, p, ul, li, figure { margin: 0; padding: 0; }
ul { list-style: none; }
a { color: inherit; text-decoration: none; }
img, video, canvas { display: block; max-width: 100%; }

h1, h2 { text-wrap: balance; }
p { text-wrap: pretty; }

::selection { background: var(--gold-500); color: var(--ink-950); }

:focus-visible {
  outline: 2px solid var(--gold-500);
  outline-offset: 4px;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip {
  position: absolute;
  top: var(--sp-16);
  left: var(--sp-16);
  z-index: 6; /* above even the preloader — must always be reachable */
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 1px solid var(--gold-500);
  background: var(--ink-900);
  color: var(--cream);
  font: 600 14px/1.4 var(--font-body);
}

.skip:focus {
  width: auto;
  height: auto;
  padding: var(--sp-12) var(--sp-24);
  margin: 0;
  overflow: visible;
  clip: auto;
  white-space: normal;
}

/* ---------- 3. Film grain (body::after) ---------- */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: .06;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
  animation: grain-jitter 1.2s steps(6) infinite;
}

@keyframes grain-jitter {
  0%   { background-position: 0 0; }
  20%  { background-position: -18px 9px; }
  40%  { background-position: 12px -14px; }
  60%  { background-position: -9px -18px; }
  80%  { background-position: 16px 6px; }
  100% { background-position: 0 0; }
}

/* ---------- 4. Preloader ---------- */
#preloader {
  position: fixed;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-16);
  background: var(--ink-950);
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur-loader-exit) var(--ease-brand);
}

#preloader.hidden {
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-loader-exit) var(--ease-brand),
              visibility 0s linear var(--dur-loader-exit);
}

/* No-JS: nothing ever adds .hidden, so without body.js the preloader must
   never render at all — Design.md: preloader "never blocks content
   beneath." (JS-enabled path is unaffected: body.js is added synchronously
   as main.js's first statement, before this could ever flash visibly.) */
body:not(.js) #preloader {
  display: none;
}

.preloader-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border: 1px solid var(--gold-500);
  color: var(--gold-500);
  font: 600 20px/1 var(--font-body);
  letter-spacing: .04em;
}

.preloader-name {
  font: 400 16px/1.4 var(--font-body);
  color: var(--cream);
}

.preloader-pct {
  font: 600 14px/1 var(--font-body);
  color: var(--gold-500);
  font-variant-numeric: tabular-nums;
}

/* ---------- 5. Progress bar (mobile) ---------- */
#progressTop {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 4;
  display: none;
  width: 0;
  height: 2px;
  background: var(--gold-500);
}

/* ---------- 6. Rail + counter (chrome) ---------- */
/* Anchored from the top rather than mathematically centered on the
   viewport: the hero name renders as two ~184px Anton lines inside its
   680px-max-width copy block on effectively every desktop width (that
   680px cap is narrower than "TUSHAR PATHAK" at anything near the clamp's
   upper bound, so it wraps well before the max-width itself becomes the
   limiting factor). A truly centered rail collides with that block on any
   viewport shorter than ~1200px, because centering ignores where the
   bottom-anchored copy actually sits; top-anchoring clears it down to a
   much shorter height instead, and still reads as "vertically centered"
   in spirit — occupying the upper band, not a pinned header. The top
   offset uses a slim --sp-16 rather than the full content --margin: that
   margin is sized for wide text panels, and the extra ~56px it would
   otherwise cost here is exactly what a five-item rail can't spare
   against a two-line hero name at common laptop viewport heights. */
#rail {
  position: fixed;
  left: calc(var(--margin) + env(safe-area-inset-left));
  top: calc(var(--sp-16) + env(safe-area-inset-top));
  z-index: 4;
}

#rail ul {
  display: flex;
  flex-direction: column;
}

#rail a {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  min-width: 44px;
  min-height: 44px;
  padding: var(--sp-8);
}

#rail .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--gold-500);
  opacity: .5;
  transition: opacity var(--dur-ui) var(--ease-move),
              box-shadow var(--dur-ui) var(--ease-move),
              transform var(--dur-ui) var(--ease-move);
}

#rail .label {
  font: 600 12px/1 var(--font-body);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--cream-dim);
  transition: color var(--dur-ui) var(--ease-move);
}

#rail a:hover .dot {
  opacity: 1;
  box-shadow: 0 0 0 4px rgba(205, 161, 94, .25);
}

#rail a:hover .label { color: var(--cream); }

#rail a.active .dot {
  opacity: 1;
  transform: scale(1.5);
  box-shadow: none;
}

#rail a.active .label { color: var(--gold-500); }

#counter {
  position: fixed;
  right: calc(var(--margin) + env(safe-area-inset-right));
  bottom: calc(var(--margin) + env(safe-area-inset-bottom));
  z-index: 4;
  pointer-events: none;
  font: 600 12px/1 var(--font-body);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--cream-dim);
  font-variant-numeric: tabular-nums;
}

/* ---------- 7. Custom cursor ---------- */
#cursorDot {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 4;
  width: 8px;
  height: 8px;
  margin: -4px 0 0 -4px;
  border-radius: 50%;
  background: var(--gold-500);
  pointer-events: none;
}

/* Outer element is a zero-size anchor JS translates every frame; the
   visible ring lives on ::before so its own transform (the .grow scale)
   never fights JS's inline transform:translate() writes on the anchor. */
#cursorRing {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 4;
  width: 0;
  height: 0;
  pointer-events: none;
}

#cursorRing::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 36px;
  height: 36px;
  margin: -18px 0 0 -18px;
  border: 1px solid var(--gold-500);
  border-radius: 50%;
  transition: transform var(--dur-ui) var(--ease-move);
}

#cursorRing.grow::before { transform: scale(1.5); }

body.touch #cursorDot, body.touch #cursorRing,
body.reduced #cursorDot, body.reduced #cursorRing,
body.kb #cursorDot, body.kb #cursorRing {
  opacity: 0;
}

/* Hide the native OS cursor only under the exact conditions main.js runs
   the custom dot/ring (desktop fine-pointer, not touch/reduced/kb) — same
   gate as the opacity:0 rule above, so the two never disagree. body.kb
   (added on first Tab keydown) falls out of the :not() chain and restores
   native cursor semantics for keyboard users. */
@media (hover: hover) and (pointer: fine) {
  body.js:not(.reduced):not(.touch):not(.kb),
  body.js:not(.reduced):not(.touch):not(.kb) * {
    cursor: none;
  }
}

/* ---------- 8. Chapter / stage scaffolding ---------- */
.chapter { position: relative; }

.stage {
  position: relative;
  overflow: hidden;
  width: 100%;
}

#ch-open .stage, #ch-pillars .stage {
  position: sticky;
  top: 0;
  height: 100vh;
}

#ch-work .stage {
  position: relative;
  min-height: 100vh;
}

.stage canvas, .stage video.loop {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.stage .scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
}

.kicker {
  font: 600 14px/1.4 var(--font-body);
  letter-spacing: .35em;
  text-transform: uppercase;
  color: var(--gold-500);
}

/* ---------- 9. Chapter 01 — OPEN ---------- */
#ch-open { height: 400vh; }

/* No-JS / pre-JS fallback: canvas has no native poster like <video> does,
   so without body.js the stage shows the still frame directly (Design.md
   §3: "No-JS/failed-frames fallback: stage backgrounds posters/hero.jpg"). */
body:not(.js) #ch-open .stage {
  background: url("assets/posters/hero.jpg") center/cover;
}

#ch-open .scrim {
  background: linear-gradient(to top right,
    rgba(10, 11, 18, .92) 0%,
    rgba(10, 11, 18, .55) 34%,
    transparent 62%);
}

.open-copy {
  position: absolute;
  z-index: 3;
  inset-inline-start: var(--margin);
  inset-block-end: var(--margin);
  max-width: 680px;
}

.open-copy .kicker { margin-bottom: var(--sp-16); }

#heroName {
  margin: 0;
  font-family: var(--font-display);
  text-transform: uppercase;
  font-size: clamp(3.5rem, 13vw, 11.5rem);
  line-height: .94;
  background: linear-gradient(90deg, #f1ede4, #bfb49c);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

@supports not (background-clip: text) {
  #heroName {
    background: none;
    color: var(--cream);
  }
}

#heroName span {
  display: inline-block;
  /* 600ms per Design.md §4 "Name track-in" row — not --dur-reveal (800ms),
     which is a different token for a different motion (scroll reveals). */
  transition: opacity 600ms var(--ease-brand),
              transform 600ms var(--ease-brand);
}

/* .word-break holds the single space between TUSHAR/PATHAK. As an
   inline-block whose only content is one collapsible space, it is both the
   leading AND trailing whitespace of its own internal line box, so an
   unstyled width would collapse to 0 — rendering "TUSHARPATHAK" whenever the
   name fits one line. An explicit width overrides that collapse. */
#heroName .word-break {
  display: inline-block;
  width: .2em;
}

/* Entrance-hidden default lives ONLY under body.js — without it (no JS),
   letters render in their natural, fully-visible state. */
body.js #heroName span { opacity: 0; transform: translateY(.35em); }
body.js #heroName span.on { opacity: 1; transform: translateY(0); }

.role {
  margin-top: var(--sp-16);
  font: 600 20px/1.4 var(--font-body);
  color: var(--gold-500);
}

.tagline {
  margin-top: var(--sp-8);
  font: 400 18px/28px var(--font-body);
  color: var(--cream-dim);
}

#scrollCue {
  position: absolute;
  z-index: 3;
  left: 50%;
  bottom: var(--margin);
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-8);
  animation: cue-float 2.4s ease-in-out infinite alternate;
}

#scrollCue .label {
  font: 600 12px/1 var(--font-body);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--gold-500);
}

#scrollCue .rule {
  width: 1px;
  height: 24px;
  background: var(--gold-500);
}

@keyframes cue-float {
  0%, 100% { transform: translateX(-50%) translateY(-6px); }
  50%      { transform: translateX(-50%) translateY(6px); }
}

/* Short-viewport override: at reduced viewport heights (e.g. 1440x748
   laptops with browser chrome) the base clamp's two stacked Anton lines
   push .open-copy's top edge up into #rail's fixed upper-band items —
   live-measured overlap at 1440x748. Capping the display scale here
   (never editing the base tall-viewport clamp above) shrinks #heroName;
   since .open-copy is bottom-anchored via inset-block-end, a shorter
   block's top edge moves DOWN, away from the rail, without moving or
   hiding the rail itself. */
@media (max-height: 820px) {
  #heroName {
    font-size: clamp(3rem, 10vw, 8.5rem);
  }
}

/* ---------- 10. Chapter 02 — NUMBERS ---------- */
#ch-numbers {
  position: relative;
  z-index: 3;
  background: var(--ink-900);
  padding-block: var(--rhythm);
  padding-inline: var(--margin);
}

#ch-numbers .kicker { margin-bottom: var(--sp-24); }

.stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--sp-48);
}

.stat {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.stat-n {
  display: inline-block; /* required for JS's inline min-width to take effect */
  font-family: var(--font-display);
  font-size: clamp(3rem, 8vw, 6rem);
  line-height: 1;
  color: var(--cream);
  font-variant-numeric: tabular-nums;
}

.stat-suffix {
  font-family: var(--font-display);
  font-size: clamp(3rem, 8vw, 6rem);
  line-height: 1;
  color: var(--gold-500);
}

.stat-label {
  margin-top: var(--sp-8);
  font: 400 18px/28px var(--font-body);
  color: var(--cream-dim);
}

/* ---------- 11. Chapter 03 — PILLARS ---------- */
#ch-pillars { height: 300vh; }

/* flex-end (not center): rail is anchored in the upper band of the
   viewport (see #rail above). Centering this content vertically would
   pull it into that same band on common viewport heights — bottom-
   anchoring keeps every sticky chapter's primary content consistent
   with the hero's bottom-left placement and clear of the rail. */
#ch-pillars .stage {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding-inline: var(--margin);
  padding-block: var(--rhythm);
}

#ch-pillars .scrim { background: rgba(10, 11, 18, .6); }

#ch-pillars .kicker {
  position: relative;
  z-index: 3;
  margin-bottom: var(--sp-24);
}

/* gap: 40px — Design.md gives "40–48px between rows" as a range; the
   lower bound is used here (rather than the 48px chosen for .stats) to
   claim back a little vertical room from the rail's fixed upper band. */
.pillars {
  position: relative;
  z-index: 3;
  display: flex;
  flex-direction: column;
  gap: var(--sp-40);
}

.pillar {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* 700ms per Design.md §4 "Pillars" row — not --dur-reveal (800ms). */
  transition: opacity 700ms var(--ease-brand),
              transform 700ms var(--ease-brand);
}

/* Entrance-hidden default lives ONLY under body.js — without it (no JS),
   all three pillars render fully visible/readable immediately. */
body.js .pillar { opacity: .32; transform: translateY(16px); }
body.js .pillar.on { opacity: 1; transform: translateY(0); }

.pillar-index {
  font: 600 12px/1 var(--font-body);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--gold-500);
}

.pillar-word {
  margin-top: var(--sp-8);
  font-family: var(--font-display);
  text-transform: uppercase;
  font-size: clamp(2.75rem, 7vw, 5.5rem);
  line-height: 1.05;
  color: var(--cream);
}

.pillar-subline {
  margin-top: var(--sp-8);
  font: 400 18px/28px var(--font-body);
  color: var(--cream-dim);
}

/* ---------- 12. Chapter 04 — WORK ---------- */
/* flex-end for the same reason as #ch-pillars .stage above: stays clear
   of the rail's fixed upper-band position. */
#ch-work .stage {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding-inline: var(--margin);
  padding-block: var(--rhythm);
}

#ch-work .scrim { background: rgba(10, 11, 18, .55); }

#ch-work .kicker {
  position: relative;
  z-index: 3;
  margin-bottom: var(--sp-32);
}

.work-grid {
  position: relative;
  z-index: 3;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--sp-24);
}

.work-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: var(--sp-24);
  background: rgba(22, 24, 38, .85);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  transition: transform var(--dur-ui) var(--ease-brand),
              border-color var(--dur-ui) var(--ease-brand),
              box-shadow var(--dur-ui) var(--ease-brand);
}

.work-index {
  font: 600 12px/1 var(--font-body);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--gold-500);
}

.work-title {
  margin-top: var(--sp-8);
  font-family: var(--font-display);
  text-transform: uppercase;
  font-size: 24px;
  line-height: 32px;
  color: var(--cream);
  transition: color var(--dur-ui) var(--ease-brand);
}

.work-pitch {
  margin-top: var(--sp-12);
  font: 400 16px/26px var(--font-body);
  color: var(--cream-dim);
}

.work-hint {
  margin-top: var(--sp-16);
  font: 600 12px/1 var(--font-body);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--gold-500);
}

@media (hover: hover) and (pointer: fine) {
  .work-card:hover {
    transform: translateY(-6px);
    border-color: var(--gold-700);
    box-shadow: 0 24px 48px -24px rgba(10, 11, 18, .6);
  }

  .work-card:hover .work-title { color: var(--gold-300); }
}

/* 140ms (--dur-press), not the base 240ms hover transition — Design.md
   §4 "Card / CTA press": "physical feedback" is deliberately snappier. */
.work-card:active { transform: scale(.98); transition-duration: var(--dur-press); }

/* ---------- 13. Chapter 05 — CONTACT ---------- */
#ch-contact {
  position: relative;
  z-index: 3;
  background: var(--ink-900);
  padding-block: var(--rhythm);
  padding-inline: var(--margin);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

#ch-contact .kicker { margin-bottom: var(--sp-24); }

#ch-contact h2 {
  max-width: 680px;
  font-family: var(--font-display);
  text-transform: uppercase;
  font-size: clamp(2.5rem, 6vw, 5.5rem);
  line-height: 1.05;
  color: var(--cream);
}

.cta {
  margin-top: var(--sp-40);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-12) var(--sp-32);
  min-height: 48px;
  border-radius: var(--radius-pill);
  background: var(--gold-500);
  color: var(--ink-950);
  font: 600 16px/1 var(--font-body);
  transition: background-color var(--dur-ui) var(--ease-brand),
              transform var(--dur-ui) var(--ease-brand);
}

.cta:hover { background: var(--gold-300); transform: translateY(-2px); }
.cta:active { transform: scale(.97); transition-duration: var(--dur-press); }

.links {
  margin-top: var(--sp-40);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-24);
}

.links a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font: 400 16px/1.4 var(--font-body);
  color: var(--cream);
}

.links a:hover { text-decoration: underline; }

#ch-contact footer {
  margin-top: var(--sp-64);
  font: 400 13px/1.5 var(--font-body);
  color: var(--cream-dim);
}

/* ---------- 14. Reveal utility (stats, cards, contact CTA/headline) ---------- */
.reveal {
  transition: opacity var(--dur-reveal) var(--ease-brand),
              transform var(--dur-reveal) var(--ease-brand),
              filter var(--dur-reveal) var(--ease-brand);
}

/* Entrance-hidden default lives ONLY under body.js. In practice .reveal is
   only ever added by main.js (never present in static HTML), so this is
   inert without JS either way — scoped for consistency with the pattern
   above and explicit documentation of the rule. */
body.js .reveal { opacity: 0; transform: translateY(24px); filter: blur(6px); }
body.js .reveal.on { opacity: 1; transform: translateY(0); filter: none; }

/* ---------- 15. Mobile (≤768px) ---------- */
@media (max-width: 768px) {
  #rail { display: none; }
  #progressTop { display: block; }
  #cursorDot, #cursorRing { opacity: 0; }

  .stats { grid-template-columns: repeat(2, 1fr); }
  .work-grid { grid-template-columns: 1fr; }

  /* Base clearance (--margin) alone puts .open-copy's last text line at the
     same bottom offset as #scrollCue (44px tall) and #counter — both also
     bottom: var(--margin) — so at narrow widths (where this block's shrink-
     to-fit width approaches full viewport width, having no inset-inline-end)
     its last line collides with both. +sp-64 lifts the block clear of
     scrollCue's 44px footprint with a margin to spare; harmless at 768px
     where there was already no collision (live-measured: 0 vs 225px gap). */
  .open-copy {
    inset-block-end: calc(var(--margin) + var(--sp-64) + env(safe-area-inset-bottom));
  }

  body.js .reveal, body.js .reveal.on {
    filter: none;
  }
}

/* ---------- 16. Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  body::after { animation: none; }
  #scrollCue { animation: none; }

  #heroCanvas { display: none; }
  #ch-open .stage {
    background: url("assets/posters/hero.jpg") center/cover;
  }

  body.js #heroName span, body.js #heroName span.on {
    transition: none;
    opacity: 1;
    transform: none;
  }

  body.js .pillar, body.js .pillar.on {
    transition: none;
    opacity: 1;
    transform: none;
  }

  body.js .reveal, body.js .reveal.on {
    transition: none;
    opacity: 1;
    transform: none;
    filter: none;
  }

  #rail .dot, #rail .label { transition: none; }
}

/* body.reduced mirrors the prefers-reduced-motion block above, for the
   JS-detected path (belt-and-suspenders alongside the native media query). */
body.reduced::after { animation: none; }

body.reduced #scrollCue { animation: none; }

body.reduced #heroCanvas { display: none; }

body.reduced #ch-open .stage {
  background: url("assets/posters/hero.jpg") center/cover;
}

body.reduced #heroName span,
body.reduced #heroName span.on {
  transition: none;
  opacity: 1;
  transform: none;
}

body.reduced .pillar,
body.reduced .pillar.on {
  transition: none;
  opacity: 1;
  transform: none;
}

body.reduced .reveal,
body.reduced .reveal.on {
  transition: none;
  opacity: 1;
  transform: none;
  filter: none;
}

body.reduced #rail .dot,
body.reduced #rail .label {
  transition: none;
}
