/* ==========================================================
   Ride — base styles
   Figma frame: 1440 wide, 60px L/R padding, 40px bottom padding
   ========================================================== */

/* ---------- Fonts ---------- */

@font-face {
  font-family: 'New Black';
  src: url('fonts/NewBlackTypeface-Bold.woff2') format('woff2'),
       url('fonts/NewBlackTypeface-Bold.woff')  format('woff');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'New Black';
  src: url('fonts/NewBlackTypeface-Medium.woff2') format('woff2'),
       url('fonts/NewBlackTypeface-Medium.woff')  format('woff');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'New Black';
  src: url('fonts/NewBlackTypeface-Regular.woff2') format('woff2'),
       url('fonts/NewBlackTypeface-Regular.woff')  format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'New Black';
  src: url('fonts/NewBlackTypeface-Light.woff2') format('woff2'),
       url('fonts/NewBlackTypeface-Light.woff')  format('woff');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

/* ---------- Reset ---------- */

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

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;              /* first-fold locked, will unlock later */
  background: #000;
  color: #fff;
  font-family: 'New Black', system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Safari-only scroll workaround — Safari won't fire wheel events on a
   fully non-scrollable page. Fix: give Safari five invisible 100 vh
   sections stacked below the hero, snap-scroll between them, and let
   main.js read scrollTop → active section → active scene. The hero is
   pinned as position: fixed so nothing visually moves; the scrollbar is
   hidden so the trick is invisible. Chrome and Firefox skip all of this
   and stay on the original wheel-event path. */
html.is-safari {
  overflow-y: auto;
  overflow-x: hidden;
  scroll-snap-type: y mandatory;     /* one snap = one scene */
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
html.is-safari::-webkit-scrollbar {
  width: 0;
  height: 0;
  background: transparent;
}
.is-safari .hero {
  position: fixed;                   /* stays visible while body scrolls */
  inset: 0;
}
/* Body must expand to fit the 5 scene-markers (500 vh total), otherwise
   the base rule's `overflow: hidden; height: 100%` clips them and html
   has nothing to scroll. Chrome / Firefox never see this. */
.is-safari body {
  overflow: visible;
  height: auto;
}
/* Lock scroll while a panel or the game modal owns the screen — accidental
   scrolls during those overlays should not advance scenes underneath. */
.is-safari body.is-panel-open,
.is-safari body.is-game-open,
.is-safari body.is-preloading {
  overflow: hidden;
  height: 100vh;                     /* re-clip during overlays */
}

/* Scene-marker scaffolding — Chrome/Firefox skip these entirely. */
.scene-marker { display: none; }
.is-safari .scene-marker {
  display: block;
  width: 100vw;
  height: 100vh;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  visibility: hidden;
  pointer-events: none;
}

img { display: block; max-width: 100%; }
a   { color: inherit; text-decoration: none; }

/* ==========================================================
   HERO
   ========================================================== */

.hero {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  --tx: 0px;                     /* parallax offset (updated by JS) */
  --ty: 0px;
  --rot: 0deg;                   /* camera tilt (updated by JS) */
}

.hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center bottom;
  transform: translate3d(var(--tx), var(--ty), 0) rotate(var(--rot)) scale(1.2);
  transform-origin: 50% 100%;    /* pivot at bottom-center → horizon tilts */
  will-change: transform, opacity;
  transition: opacity 500ms ease; /* also drives day → night hold fade */
}

/* Big-video architecture (onam): only two scene layers, day + night. Both
   play in lockstep; the scene manager seeks between time ranges to switch
   scenes. Press-and-hold on the hero fades the night layer in over 500ms. */
.hero__video--day    { z-index: 1; opacity: 1; }       /* base layer */
/* 0.001 (not 0) so the compositor keeps decoding fresh frames — a fully
   invisible layer gets deprioritised by Chrome/Safari, causing a black flash
   on the first press-and-hold. Visually indistinguishable from 0. */
.hero__video--night  { z-index: 1; opacity: 0.001; }

.hero.is-holding-night .hero__video--night { opacity: 1; }

/* Legacy per-scene rules kept for index.html which still uses the split
   videos. Music.html (big-video) doesn't touch these. */
.hero__video--a            { z-index: 1; opacity: 1; }
.hero__video--a-night      { z-index: 1; opacity: 0; }
.hero__video--b            { z-index: 3; opacity: 0; }
.hero__video--terrain      { z-index: 4; opacity: 0; }
.hero__video--horse-night  { z-index: 4; opacity: 0; }
.hero__video--c            { z-index: 4; opacity: 0; }
.hero__video--c-night      { z-index: 4; opacity: 0; }
.hero__video--valley-night { z-index: 4; opacity: 0; }
.hero__video--snow-night   { z-index: 4; opacity: 0; }
.hero__video--coconut-night{ z-index: 4; opacity: 0; }

.hero.is-a-night      .hero__video--a-night      { opacity: 1; }
.hero.is-c-night      .hero__video--c-night      { opacity: 1; }
.hero.is-horse-night  .hero__video--horse-night  { opacity: 1; }
.hero.is-valley-night .hero__video--valley-night { opacity: 1; }
.hero.is-snow-night   .hero__video--snow-night   { opacity: 1; }
.hero.is-coconut-night .hero__video--coconut-night { opacity: 1; }

/* ---------- Bottom vignette wash ---------- */

.hero::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 5;                    /* above all videos, below chrome */
  pointer-events: none;
  background: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.20) 0%,
    rgba(0, 0, 0, 0)    50%
  );
}

/* ---------- Eyelid blink transition ----------
   Full-viewport black overlay. Its clip-path is a horizontal strip that
   opens/closes vertically from centre — like eyelids. When fully closed,
   the strip covers everything (black). When fully open, it's a 0-height
   line and invisible. Animation runs when `.hero.is-eye-closed` is set. */

.eye-blink {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  z-index: 12000;                /* on top of all layers during transition */
  pointer-events: none;
  display: block;
  opacity: 0;                    /* hidden at rest — no mask edges visible */
  transition: opacity 180ms ease;
}

.hero.is-blinking .eye-blink,
body.is-blinking .eye-blink { opacity: 1; }

/* Animate the ellipse's vertical scale from 1 → 0.
   transform-box: fill-box makes transform-origin refer to the ellipse's
   own bounding box, so scaling collapses toward its centre — the closed
   position looks like a horizontal line, then the surrounding black
   covers everything. */
.eye-blink__pupil {
  transform-box: fill-box;
  transform-origin: center;
  transform: scaleY(1);
  transition: transform 500ms cubic-bezier(0.7, 0, 0.2, 1);
}

.hero.is-eye-closed .eye-blink__pupil,
body.is-eye-closed .eye-blink__pupil {
  transform: scaleY(0);
}

/* Videos gain a slight defocus during the blink — eyelash blur. */
.hero__video { transition: opacity 500ms ease, filter 200ms ease; }
.hero.is-eye-closed .hero__video { filter: blur(4px); }

/* Safari-only: Safari runs CSS filter:blur() and SVG feGaussianBlur on the
   CPU during animations, causing the eye-blink to stutter badly. Strip both
   effects for Safari users. Chrome/Firefox render the effects unchanged.
   Visual cost: the video blur is invisible anyway (eye is fully shut), and
   the eyelid edge just becomes slightly crisper. */
.is-safari .hero.is-eye-closed .hero__video { filter: none; }
.is-safari .hero.is-eye-closed .hero__psy   { filter: none; }

/* Safari uses the same blink timing as Chrome (500 ms) — the base
   .eye-blink__pupil / .eye-blink / .hero__video transitions already
   handle it, no Safari-specific override needed. Only the CPU-heavy
   feGaussianBlur on the pupil is stripped for Safari (below) since it
   stutters when combined with the transform animation. */
.is-safari .eye-blink__pupil { filter: none; }

/* ---------- Corner blur (single-overlay variant) ----------
   One full-viewport-sized overlay with a mask that combines four radial
   gradients — one at each corner. Where any gradient is opaque, the
   backdrop blur is visible; the centre stays sharp. Simpler markup,
   but the browser blurs the whole viewport area under the hood, which
   is more GPU work than four small pool divs. */

.corner-blur {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 6;                     /* above videos + vignette, below chrome (z:10) */
  -webkit-backdrop-filter: blur(16px);
          backdrop-filter: blur(16px);
  -webkit-mask-image:
    radial-gradient(circle at 0% 0%,     black 0px, transparent 240px),
    radial-gradient(circle at 100% 0%,   black 0px, transparent 240px),
    radial-gradient(circle at 0% 100%,   black 0px, transparent 240px),
    radial-gradient(circle at 100% 100%, black 0px, transparent 240px);
          mask-image:
    radial-gradient(circle at 0% 0%,     black 0px, transparent 240px),
    radial-gradient(circle at 100% 0%,   black 0px, transparent 240px),
    radial-gradient(circle at 0% 100%,   black 0px, transparent 240px),
    radial-gradient(circle at 100% 100%, black 0px, transparent 240px);
}

/* ---------- Chrome (bottom row: logo · rule · nav) ---------- */

.hero__chrome {
  position: absolute;
  left: 60px;
  right: 60px;
  bottom: 40px;
  z-index: 1010;                 /* above the about panel (1000) so nav stays visible */
  display: flex;
  align-items: flex-end;
  gap: 24px;
  pointer-events: none;          /* let clicks pass through the row */
}

.hero__chrome > * { pointer-events: auto; }

/* ---------- Logo ---------- */

.brand {
  flex-shrink: 0;
  display: block;
  width: 100px;                  /* logo native 186×148 → scaled */
  aspect-ratio: 186 / 148;
  cursor: pointer;
  /* transform is driven every frame by js/logo-magnet.js — no CSS
     transition on it or the two would fight. */
  transition: opacity 200ms ease, filter 220ms ease;
}

.brand:hover {
  opacity: 0.85;
}

/* Magnetic-field glow: kicks in when the cursor is within ~120 px of the
   logo. Filter is applied before the mix-blend-mode so the shadow follows
   the masked glyph's silhouette and inverts with it. */
.brand.is-magnetic {
  filter:
    drop-shadow(0 0 8px  rgba(255, 255, 255, 0.85))
    drop-shadow(0 0 18px rgba(255, 255, 255, 0.50));
}

/* Logo image renders directly — no CSS mask, no mix-blend-mode. Chrome
   has known compositor bugs with mask + mix-blend-mode over a playing
   <video>; Safari doesn't. Rendering the PNG directly sidesteps both. */
.brand__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* ---------- Corner chrome toggles ----------
   Two icon buttons: night-mode (top-left, moon ↔ sun) and sound (top-right,
   speaker ↔ speaker-muted). Same size, weight and interaction so they read
   as one system. */

.night-toggle,
.sound-toggle {
  position: fixed;
  top: 40px;
  z-index: 1015;                     /* above chrome (1010) so it stays over panels too */
  width: 38px;
  height: 38px;
  padding: 0;
  background: transparent;
  border: none;
  color: #fff;
  cursor: pointer;
  display: grid;
  place-items: center;
  transition: opacity 200ms ease;
}
/* Positions swapped 2026-08-27 per Arun so night-mode sits
   top-right and mute sits top-left across the whole site
   (index, game, and About all follow this convention now). */
.night-toggle { right: 60px; }
.sound-toggle { left:  60px; }

.night-toggle:hover,
.sound-toggle:hover  { opacity: 0.7; }
.night-toggle:focus,
.sound-toggle:focus  { outline: none; }
.night-toggle:focus-visible,
.sound-toggle:focus-visible {
  outline: 1px solid rgba(255, 255, 255, 0.7);
  outline-offset: 6px;
  border-radius: 999px;
}

.night-toggle__icon,
.sound-toggle__icon {
  grid-area: 1 / 1;                  /* stack both icons in the same cell */
  width: 22px;
  height: 22px;
  transition: opacity 300ms ease;
}
.night-toggle__icon--moon { opacity: 1; }
.night-toggle__icon--sun  { opacity: 0; }
body.is-night-mode .night-toggle__icon--moon { opacity: 0; }
body.is-night-mode .night-toggle__icon--sun  { opacity: 1; }

/* Sound icon flips when body.is-muted (set by audio-rule.js). */
.sound-toggle__icon--on  { opacity: 1; }
.sound-toggle__icon--off { opacity: 0; }
body.is-muted .sound-toggle__icon--on  { opacity: 0; }
body.is-muted .sound-toggle__icon--off { opacity: 1; }

/* Night-mode base swap: the night video becomes the default layer. */
/* Night mode: same 0.001 trick so the DAY layer also stays decoded and
   press-and-hold in night mode reveals it without a paint hitch. */
body.is-night-mode .hero__video--day   { opacity: 0.001; }
body.is-night-mode .hero__video--night { opacity: 1; }

/* Press-and-hold in night mode inverts: shows the DAY layer while held. */
body.is-night-mode .hero.is-holding-night .hero__video--day   { opacity: 1; }
body.is-night-mode .hero.is-holding-night .hero__video--night { opacity: 0.001; }

/* ---------- Rule between logo and nav ---------- */

.hero__rule {
  flex: 1 1 auto;
  height: 1px;
  background: #fff;
  opacity: 0.5;
  mix-blend-mode: plus-lighter;
  margin-bottom: 4px;            /* nudged onto CONTACT baseline */
}

/* ---------- Audio-reactive variant (music.html) ---------- */
/* Overrides the flat rule so the SVG waveform stays vertically centered
   on the same baseline as the plain rule.                                */

.hero__rule--audio {
  position: relative;
  height: 1px;                   /* keep layout footprint unchanged */
  background: transparent;       /* SVG paints the line + waveform */
  overflow: visible;             /* let waveform peaks escape the 1px box */
  opacity: 1;                    /* SVG wave reads at full white — no 0.5 dim */
  mix-blend-mode: normal;        /* drop the plus-lighter used on the plain rule */
}

.hero__rule__svg {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 100%;
  height: 40px;                  /* room for taller peaks + click hit area */
  overflow: visible;
  cursor: pointer;               /* clickable: toggles mute */
  pointer-events: auto;
}

.hero__rule__path {
  fill: none;
  stroke: #fff;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;   /* stays this many device px regardless of scale */
  transition: opacity 200ms ease;
}

/* Muted state: waveform stays but reads dimmer */
.hero__rule--audio.is-muted .hero__rule__path { opacity: 0.35; }

/* ---------- Nav ---------- */

.site-nav {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 10px;
}

.site-nav a {
  font-family: 'New Black', sans-serif;
  font-weight: 700;
  font-size: 20px;
  line-height: 1;                /* 100% per Figma */
  letter-spacing: 0;
  color: #fff;
  text-transform: uppercase;
  transition: opacity 200ms ease;
}

.site-nav a:hover { opacity: 0.7; }
.site-nav a:focus { outline: none; }
.site-nav a:focus-visible { outline: 1px solid rgba(255,255,255,0.6); outline-offset: 4px; }

/* ==========================================================
   PSY overlay — logo click swaps hero videos for a still image
   ========================================================== */

.hero__psy {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 5;                       /* above all videos (max z:4), below .eye-blink (z:7) */
  display: none;
  pointer-events: none;
  transition: filter 200ms ease;
}

body.is-psy .hero__psy {
  display: block;
}

/* Match the videos' blink defocus so the eye-blink transition feels native. */
.hero.is-eye-closed .hero__psy { filter: blur(4px); }

/* ==========================================================
   HERO CHAT — anchored bottom-left, Arun texting the visitor
   (welcome → scroll cue → hold cue → acknowledgement, then retires).
   Bubbles reuse the About-panel visual language but flip the flattened
   corner to bottom-LEFT so they feel rooted to the corner they sit in.
   ========================================================== */

.hero-chat {
  position: fixed;
  bottom: 130px;                     /* 40 px gap between last bubble and ABOUT */
  right: 60px;                       /* same right edge as the About panel */
  z-index: 500;                      /* over hero, under panels (1000) */
  display: flex;
  flex-direction: column;
  align-items: flex-end;             /* right-align every bubble */
  gap: 10px;
  max-width: 340px;
  pointer-events: none;              /* click-through — never blocks the hero */
}

/* Hero spotlight — flat black overlay at 70% opacity driven by
   js/hero-spotlight.js.

   Overlay starts VISIBLE from page load (opacity 1) and sits
   BELOW the preloader (z:400 vs preloader's z:9999) so it's
   hidden behind the preloader wash during load. When the
   preloader fades out on "I'm in" click, the overlay is
   already at full opacity behind it — no bright flash of
   video between the two layers. It then holds through the
   welcome chat sequence and fades out 2 s after the last
   welcome bubble lands (or on any real engagement).

   Non-interactive (pointer-events: none) and hidden on mobile.
   Per Arun 2026-08-27: dark from the start, not a fade-in. */
.hero-spotlight {
  position: fixed;
  inset: 0;
  z-index: 400;
  pointer-events: none;
  opacity: 1;
  transition: opacity 500ms ease-out;
  background: rgba(0, 0, 0, 0.7);
}
.hero-spotlight.is-dismissed {
  opacity: 0;
}

/* Intro scroll-lock — while body.is-intro-locked is set,
   scrolling is blocked (belt-and-suspenders alongside the
   preventDefault in intro-lock.js). Set by intro-lock.js on
   preloader:enter, removed on hero-spotlight:dismissed. */
body.is-intro-locked {
  overflow: hidden;
}
/* Small shake on the chat container when the visitor tries to
   scroll during the lock — draws their eye to the thing that
   wants their attention. Kept subtle (6 px, 500 ms). */
@keyframes hero-chat-shake {
  0%, 100% { transform: translateX(0); }
  20%      { transform: translateX(-6px); }
  40%      { transform: translateX(6px); }
  60%      { transform: translateX(-4px); }
  80%      { transform: translateX(4px); }
}
.hero-chat.is-shaking {
  animation: hero-chat-shake 500ms ease-in-out;
}
@media (max-width: 767px) {
  .hero-spotlight { display: none; }
}

.hero-chat__bubble,
.hero-chat__typing {
  max-width: 100%;
  padding: 12px 18px;
  background: #fff;
  color: #111;
  border-radius: 12px;               /* uniform rounded pillow */
  font-family: 'Inter', system-ui, sans-serif;
  font-weight: 400;
  font-size: 14px;                   /* smallest that stays accessible */
  line-height: 1.4;
  text-align: left;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 500ms ease, transform 500ms ease;
  word-wrap: break-word;
}
.hero-chat__bubble.is-in,
.hero-chat__typing.is-in {
  opacity: 1;
  transform: none;
}

/* Fading OUT — same duration as fading in, but no transform revert. */
.hero-chat__bubble.is-out,
.hero-chat__typing.is-out {
  opacity: 0;
  transform: translateY(6px);
}

/* Typing indicator: three dots pulsing inside a bubble shell.
   Reuses the About-panel dot animation. */
.hero-chat__typing {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 14px 16px;
}
.hero-chat__typing span {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.45);   /* dark dots on the white bubble */
  animation: chat-typing-dot 1.15s ease-in-out infinite;
}
.hero-chat__typing span:nth-child(2) { animation-delay: 0.15s; }
.hero-chat__typing span:nth-child(3) { animation-delay: 0.30s; }

/* Hide the hero chat while other overlays own the screen. */
body.is-preloading .hero-chat,
body.is-panel-open .hero-chat,
body.is-game-open  .hero-chat {
  display: none;
}

@media (pointer: coarse) {
  .hero-chat { display: none; }      /* desktop only for now */
}
/* Tablet-landscape (>=1000px): re-enable the desktop chat position so bubbles
   still appear on horizontal iPad. Portrait tablets + phones stay hidden by
   the rule above (mobile block re-shows them with its own centred layout). */
@media (pointer: coarse) and (orientation: landscape) and (min-width: 1000px) {
  .hero-chat { display: flex; }

  /* Anchor everything to the SMALLEST viewport (URL-bar-visible) so the
     footer chrome sits at the same physical position regardless of Safari's
     URL bar state. When Safari collapses its URL bar the viewport grows —
     that extra space appears as a black stripe below .hero rather than
     shifting the footer down. */
  .hero { height: 100svh; }
  .is-safari body { height: 100svh; overflow: hidden; }
  .is-safari .scene-marker { display: none; }   /* kill the scroll-driven section-scroll setup on tablet-landscape */
}

/* ==========================================================
   MUTE CAPSULE — sits at the centre of the audio waveform
   Same glass capsule as the cursor tip, but 11 px font.
   ========================================================== */

.hero__rule--audio { position: relative; }

/* ==========================================================
   HINT (rotating instruction text at top-right)
   ========================================================== */

.hint {
  position: fixed;
  top: 40px;
  right: 60px;
  z-index: 1050;                 /* above chrome (1010), below preloader (9999) */
  color: #000;
  background: rgba(255, 255, 255, 0.35);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
          backdrop-filter: blur(18px) saturate(160%);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 13px;
  letter-spacing: 0.01em;
  line-height: 1.4;
  max-width: 340px;
  padding: 10px 14px;
  display: inline-flex;
  align-items: center;
  gap: 12px;
  pointer-events: none;
  /* Mask-reveal sweep. At rest the whole box is clipped from the left
     (invisible). Adding .is-visible shrinks the left-inset to 0, revealing
     the box + text from the RIGHT edge leftward. Removing .is-visible
     grows the left-inset back to 100% — hides sweeping LEFT→RIGHT. */
  -webkit-clip-path: inset(0 0 0 100%);
          clip-path: inset(0 0 0 100%);
  transition:
    -webkit-clip-path 500ms cubic-bezier(0.7, 0, 0.2, 1),
            clip-path 500ms cubic-bezier(0.7, 0, 0.2, 1);
}

.hint.is-visible {
  -webkit-clip-path: inset(0);
          clip-path: inset(0);
  pointer-events: auto;
}

.hint__text {
  display: inline-block;
}


/* ==========================================================
   PRELOADER
   Full-screen black overlay with an "Enter" button. Captures the first
   real user gesture so every video's play() call is allowed by the
   browser's autoplay policy.
   ========================================================== */

.preloader {
  position: fixed;
  inset: 0;
  z-index: 9999;
  /* Frosted-glass wash: blur the hero video behind the whole overlay, plus
     a 70% black tint so the framed card reads clearly. */
  background: rgba(0, 0, 0, 0.7);
  -webkit-backdrop-filter: blur(24px) saturate(1.1);
          backdrop-filter: blur(24px) saturate(1.1);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity 700ms cubic-bezier(0.16, 1, 0.3, 1);
}

.preloader.is-hidden {
  opacity: 0;
  pointer-events: none;
}

/* Deploy version stamp — sits at bottom-left of the preloader only, tiny
   and muted so it's readable but never a design element. */
.deploy-version {
  position: absolute;
  left: 20px;
  bottom: 16px;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 10px;
  letter-spacing: 0.03em;
  color: rgba(255, 255, 255, 0.5);
  pointer-events: none;
  user-select: none;
}

/* Framed card centred on the hero. Transparent interior — the preloader
   wash behind is already doing the darken + blur. */
.preloader__card {
  padding: 60px;
  color: #fff;
  display: flex;
  flex-direction: column;
  /* Uniform gap between every row (headline → caption → button)
     so the spacing above and below "Coming along?" is equal. */
  gap: 20px;
  align-items: center;              /* centre every row horizontally */
  max-width: 92vw;
}

/* While the preloader is up, hide the bottom chrome (logo + waveform + nav)
   and the top-right night-mode toggle so the framed card sits on a clean
   blurred background. Both restore automatically when preloader.js removes
   the class on entry. */
body.is-preloading .hero__chrome,
body.is-preloading .night-toggle,
body.is-preloading .sound-toggle {
  display: none;
}

/* Logo entrance — fades in as its own moment the instant the preloader
   dismisses, so the mark arrives first (before Arun starts typing). Uses
   a keyframe animation rather than a transition so it doesn't fight with
   the .brand's short 200 ms opacity transition used for hover feedback.
   Chrome (.hero__chrome) becomes display:flex the moment is-preloading
   drops from body, at which point this animation runs exactly once. */
body:not(.is-preloading) .brand {
  animation: brand-in 800ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes brand-in {
  0%   { opacity: 0; }
  100% { opacity: 1; }
}

/* Aligned to design system's --type-display-m: same clamp on
   desktop, same 1.05 line-height, same +0.005em tracking, same
   NewBlack 700 weight. Colour + text-align stay preloader-
   specific. Mobile size is forced to 28px in the media query
   below to match .type--display-m's mobile override. */
.preloader__title {
  font-family: 'New Black', system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(28px, 5vw, 50px); /* matches --type-display-m */
  line-height: 1.05;
  letter-spacing: 0.005em;           /* matches --type-display-m */
  color: #fff;
  margin: 0;
  text-align: center;                /* headline centred inside the card */
}

/* Effect 01 · Rise reveal on the preloader title. Same mask
   recipe as .intro__mark__line on about.html - each per-line
   span is an overflow-hidden slot so chars primed to yPercent
   110 sit below the visible baseline until preloader-rise.js
   tweens them up. Container stays visibility: hidden until JS
   has run Splitting + gsap.set so there's no flash of
   un-transformed text on load. */
.preloader__title[data-fx="rise"] {
  visibility: hidden;
}
.preloader__title__line {
  display: block;
  overflow: hidden;
  line-height: 1.05;
  padding-bottom: 0.06em;   /* tiny breathing room so caps aren't clipped */
}
.preloader__title__line .char {
  display: inline-block;
  will-change: transform;
}

/* .preloader__cta wrapper was removed 2026-08-27 - caption and
   button are now direct children of .preloader__card so all
   three rows share the same 20px card gap (equal spacing above
   and below "Coming along?"). This rule stays as a no-op guard
   in case any stray markup still uses the class. */

/* Aligned to design system's .type--body-m-b: Inter 700 at
   18px desktop / 16px mobile, line-height 1.5. Colour +
   margin stay preloader-specific.

   Fades in on a delay so the caption arrives AFTER the title's
   Effect 01 rise finishes. Title rise timing: 0.15s delay +
   0.35s per-line offset + 1s duration ≈ 1.5s total. Caption
   starts fading in at 1.6s (100ms breathing room). */
.preloader__caption {
  font-family: 'Inter', system-ui, sans-serif;
  font-weight: 700;
  font-size: 18px;                  /* matches --type-body-m */
  line-height: 1.5;                 /* matches body-m-b */
  color: #fff;
  margin: 0;
  opacity: 0;
  animation: preloader-fade-in 500ms ease-out 1.6s forwards;
}
@keyframes preloader-fade-in {
  to { opacity: 1; }
}

/* Aligned to design system's .type--body-m-b size (18/16).
   Kept line-height at 1 (not 1.5) because the button already
   has vertical padding for spacing — a 1.5 line-height would
   inflate the pill unnecessarily.

   Fades in ~400ms after the caption so the three rows
   (headline → sub-headline → CTA) arrive in a clear sequence
   rather than the caption and button popping together. */
.preloader__enter {
  position: relative;               /* anchor for the fill overlay */
  overflow: hidden;                 /* clip the sweep to the button */
  font-family: 'Inter', system-ui, sans-serif;
  font-weight: 700;
  font-size: 18px;                  /* matches --type-body-m */
  line-height: 1;
  color: #000;
  background: #fff;                 /* final state — visible when fully loaded */
  border: none;
  border-radius: 999px;             /* pill — matches "Coming along?" side of the CTA */
  padding: 8px 18px;                /* extra horizontal room so the label sits nicely inside the pill */
  /* Smooth swap into the hover state below. */
  transition: background-color 200ms ease, color 200ms ease, box-shadow 200ms ease;
  /* +6px on top of the 20px card gap = 26px space between the
     caption and the button. Per Arun 2026-08-27. */
  margin-top: 6px;
  opacity: 0;
  animation: preloader-fade-in 500ms ease-out 2s forwards;
  cursor: pointer;
  transition: background 200ms ease, color 200ms ease;
  display: flex;
  align-items: center;
  gap: 10px;
}

/* Loading state — button starts disabled, gray, with a white fill that grows
   from the left as the media downloads. `--load-progress` is set by JS. */
.preloader__enter[disabled] {
  background: #3a3a3a;              /* gray base */
  color: rgba(255, 255, 255, 0.85); /* soft white text over gray */
  cursor: wait;
}
/* The white sweep — its width tracks download progress. Rounded so the
   growing leading edge reads as a pill sliding in from the left, not a
   hard vertical cut. At 100% width its rounded right edge lines up with
   the parent pill's right edge. */
.preloader__enter[disabled]::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: var(--load-progress, 0%);
  background: #fff;
  border-radius: 999px;
  z-index: 0;
  transition: width 250ms ease-out;
}
/* Sit label + percent above the fill, and use difference-blend so as
   the white sweep passes underneath, they invert from soft-white-on-gray to
   black-on-white. */
.preloader__enter[disabled] .preloader__enter__label,
.preloader__enter[disabled] .preloader__enter__percent {
  position: relative;
  z-index: 1;
  mix-blend-mode: difference;
}

/* Percent readout occupies the arrow's slot while loading. */
.preloader__enter__percent {
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;   /* stops width jitter as digits change */
  min-width: 3.5em;                     /* stable footprint from 0% → 100% */
  text-align: right;
}

/* Show the correct label + percent depending on loading state. */
.preloader__enter[disabled] .preloader__enter__label--ready { display: none; }
.preloader__enter:not([disabled]) .preloader__enter__label--loading { display: none; }
.preloader__enter:not([disabled]) .preloader__enter__percent        { display: none; }

/* Hover state — inverts the pill: black fill, white text, and
   a 2px white stroke rendered as an inset box-shadow so there's
   no layout shift (a real border would grow the button by 4px
   the moment the visitor moved the cursor onto it). Transition
   is on the base rule below so the swap is smooth. */
.preloader__enter:not([disabled]):hover {
  background: #000;
  color: #fff;
  box-shadow: inset 0 0 0 2px #fff;
}

.preloader__enter:focus { outline: none; }
.preloader__enter:focus-visible { outline: 1px solid rgba(255, 255, 255, 0.7); outline-offset: 4px; }

/* ==========================================================
   ABOUT PANEL  (music.html)
   Slides in from the right; underlying hero is dimmed + blurred.
   ========================================================== */

/* Black slide-in panel sitting on the right side of the hero. Video keeps
   playing behind it; when the panel opens, the hero blurs. */

/* Blur the VIDEOS only (not the whole hero) so the chrome stays sharp AND
   can z-index above the panel — .hero no longer gets a filter, so it
   creates no stacking context and .hero__chrome (z:1010) sits over
   .about-panel (z:1000) as expected. */
body.is-panel-open .hero__video { filter: blur(10px) brightness(0.7); }

.about-panel {
  position: fixed;
  /* Top: 100 px clears the top-right sound icon. Bottom: 120 px keeps the
     panel's close × comfortably above the chrome nav (ABOUT/CONTACT sit
     at bottom: 40 px + ~50 px column height, so their top edge is ~90 px
     above the viewport bottom — this leaves a ~50 px breather). */
  top: 100px;
  right: 60px;
  bottom: 120px;
  width: 40vw;
  /* Transparent panel — the dark-glass treatment lives on the chat bubbles
     themselves; the panel just positions them over the (blurred) video. */
  background: transparent;
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  color: #fff;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  transform: translateX(calc(100% + 60px));  /* off-screen incl. right margin */
  transition: transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
  pointer-events: none;
}

.about-panel.is-open {
  transform: translateX(0);
  /* Panel body is (mostly) transparent — keep the container itself
     click-through so tapping empty space fires the outside-click handler
     in about-panel.js. Interactive content elements below explicitly
     re-enable pointer-events. */
  pointer-events: none;
}

/* Re-enable hit-testing only on the actual visible content of a panel. */
.about-panel .chat__bubble,
.about-panel .chat__typing,
.about-panel .chat-cta,
.about-panel .about-panel__lede,
.about-panel .about-panel__meta,
.about-panel .about-panel__close {
  pointer-events: auto;
}

/* Contact panel — same chat design as About; no theme override needed.
   The previous light-glass background, email meta line, and black close
   icon were removed as part of the chat redesign. */

@media (max-width: 720px) {
  .about-panel {
    top: 20px;
    right: 20px;
    bottom: 20px;
    width: calc(100vw - 40px);
    transform: translateX(calc(100% + 20px));
  }
}

.about-panel__body {
  flex: 1;
  padding: 80px;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}

/* Chat-panel overrides (About + Contact, which share the same design):
   • zero right pad — bubbles sit flush with ABOUT/CONTACT nav below
   • small bottom pad — close × sits close to ABOUT/CONTACT vertically
   • justify-content: flex-end — pack [footer → chat → cta] at the bottom
     of the body, so the WHOLE stack (close × included) rides up together
     when new bubbles are appended. */
.about-panel[data-panel="about"] .about-panel__body,
.about-panel[data-panel="contact"] .about-panel__body {
  padding-right: 0;
  padding-bottom: 10px;              /* trimmed so the last bubble sits ~40 px
                                        above ABOUT/CONTACT, matching the
                                        hero-chat spacing */
  justify-content: flex-end;
}
.about-panel__body::-webkit-scrollbar { width: 0; background: transparent; }

.about-panel__lede {
  font-family: 'Inter', system-ui, sans-serif;
  font-weight: 400;
  font-size: 20px;
  line-height: 1.5;
  color: #fff;
  margin: 0;
}

/* ---------- Chat conversation (About panel) ----------
   Right-aligned bubbles from Arun, revealed one at a time with a typing
   indicator before each one. Sequence lives in js/about-chat.js. */

.chat {
  /* Natural height — the parent .about-panel__body handles bottom-anchoring
     of the whole [footer → chat → cta] stack, so the close × rides up
     together with the bubbles as new ones append. */
  display: flex;
  flex-direction: column;
  align-items: flex-end;             /* right-align every bubble */
  gap: 10px;
}

/* Unified chat bubble — same white pill as .hero-chat__bubble.
   Used in About panel, Contact panel, and (via .hero-chat) hero + game.
   White bg / dark text / 14 px / 12 px corners / no shadow / right-aligned
   text so multi-line bubbles hug the same edge as the bubble stack itself. */
.chat__bubble,
.chat__typing {
  max-width: 100%;
  padding: 12px 18px;
  background: #fff;
  color: #111;
  border-radius: 12px;
  font-family: 'Inter', system-ui, sans-serif;
  font-weight: 400;
  font-size: 14px;
  line-height: 1.4;
  text-align: right;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 500ms ease, transform 500ms ease;
  word-wrap: break-word;
}
.chat__bubble.is-in,
.chat__typing.is-in {
  opacity: 1;
  transform: none;
}

/* Typing indicator: three dots pulsing inside a bubble shell. */
.chat__typing {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 14px 16px;
}
.chat__typing span {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.45);   /* dark dots on white pill */
  animation: chat-typing-dot 1.15s ease-in-out infinite;
}
.chat__typing span:nth-child(2) { animation-delay: 0.15s; }
.chat__typing span:nth-child(3) { animation-delay: 0.30s; }
@keyframes chat-typing-dot {
  0%, 60%, 100% { transform: translateY(0);    opacity: 0.55; }
  30%          { transform: translateY(-3px);  opacity: 1;    }
}

/* CTA styled as another chat bubble — same white pill treatment.
   Underline + arrow signal it's a link. */
.chat-cta {
  align-self: flex-end;
  margin-top: 10px;
  max-width: 100%;
  padding: 12px 18px;
  background: #fff;
  color: #111;
  border: none;
  border-radius: 12px;
  font-family: 'Inter', system-ui, sans-serif;
  font-weight: 400;
  font-size: 14px;
  line-height: 1.4;
  text-align: right;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 260ms ease, transform 260ms ease, background 200ms ease;
}
.chat-cta.is-in {
  opacity: 1;
  transform: none;
}
/* Reads as another chat bubble — no underline, no arrow, no hover swap.
   Cursor still shows pointer so it's discoverable as clickable. */
.chat-cta__label { text-decoration: none; }
.chat-cta:focus         { outline: none; }
.chat-cta:focus-visible { outline: 1px solid rgba(255, 255, 255, 0.7); outline-offset: 4px; }

/* Close button removed from the chat panels — Escape key and click-outside
   handle closing. No footer overrides needed for About/Contact. */

/* Anchors inside chat bubbles (phone / email links in Contact) — dark
   underlined on the white pill, dim on hover. */
.chat__bubble a {
  color: #111;
  text-decoration: underline;
  text-underline-offset: 3px;
  transition: opacity 200ms ease;
}
.chat__bubble a:hover { opacity: 0.75; }

/* Bubbles support explicit line breaks so multi-line messages (like the
   contact info) render as the author typed them. */
.chat__bubble {
  white-space: pre-line;
}

/* Bottom row: meta on the left, close button on the right. margin-top auto
   pushes the whole row to the bottom of the flex-column body. */
.about-panel__footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  margin-top: auto;
  padding-top: 24px;
}

.about-panel__meta {
  font-family: 'Inter', system-ui, sans-serif;
  font-weight: 400;
  font-size: 20px;
  line-height: 1.5;
  color: #fff;
  margin: 0;
}

.about-panel__close {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  transition: opacity 200ms ease;
}
.about-panel__close:hover  { opacity: 0.7; }
.about-panel__close:focus  { outline: none; }
.about-panel__close:focus-visible { outline: 1px solid rgba(255,255,255,0.6); outline-offset: 4px; }

/* Mask the SVG so the icon is always white regardless of the file's colour. */
.about-panel__close__icon {
  display: block;
  width: 100%;
  height: 100%;
  background: #fff;
  -webkit-mask: url('images/close.svg') center / contain no-repeat;
          mask: url('images/close.svg') center / contain no-repeat;
}

/* ---------- Scene nav (mobile-only arrows) ----------
   Hidden on desktop; the mobile media query below reveals the frosted-
   glass pair. Wired to window.rideAdvanceScene by js/scene-nav.js. */
.scene-nav { display: none; }

/* ==========================================================
   MOBILE (≤ 767px)
   ========================================================== */

@media (max-width: 767px) {
  /* Mobile banner: 80vh, pinned to the TOP of the viewport (bottom 20vh
     is the footer chrome). Explicit black background + no border prevents
     any subpixel seam between the video's bottom row and the footer black. */
  .hero {
    position: absolute;
    left: 0;
    right: 0;
    top: env(safe-area-inset-top);   /* start below notch / status bar */
    bottom: auto;
    height: calc(100vh - 164px - env(safe-area-inset-top));   /* fallback */
    background: #000;
    border: 0;
    /* Kill the browser's built-in text-selection loupe / long-press
       callout so our press-and-hold gesture (night mode) doesn't fight
       Chrome's / Safari's own long-press zoom bubble on the video. */
    -webkit-user-select: none;
            user-select: none;
    -webkit-touch-callout: none;
    -webkit-tap-highlight-color: transparent;
  }
  /* iOS-Safari-safe: dynamic viewport height accounts for the URL bar so
     .hero fits exactly in the visible area between top chrome and footer. */
  @supports (height: 100dvh) {
    .hero { height: calc(100dvh - 164px - env(safe-area-inset-top)); }
  }

  /* Mobile: show the video's FULL vertical height. Width is auto (natural
     aspect ratio) and centered horizontally — if the video is wider than
     the viewport it overflows off both sides (parent .hero has
     overflow:hidden), if narrower the .hero black background fills the
     gap. No scale(1.2) — that would crop the top of the frame. */
  .hero__video {
    left: 50%;
    right: auto;
    width: auto;
    height: 100%;
    object-fit: fill;                /* aspect kept via width:auto — no cropping */
    transform: translate(-50%, 0) translate3d(var(--tx), var(--ty), 0);
    transform-origin: 50% 50%;
    /* Block the browser's drag-image / long-press "Save image / Copy"
       callout on the video itself. Belt-and-braces alongside the
       .hero user-select / touch-callout rules above. */
    -webkit-user-drag: none;
    pointer-events: none;            /* let long-press reach .hero for the night gesture, not the <video> element */
  }

  /* Mobile-only: eye-blink transition is re-enabled (comes from the base
     styles); the desktop's is-eye-closed video blur stays killed on
     mobile to keep the scene swap light on the GPU. The pupil's own
     SVG feGaussianBlur is also killed — a sharp-edged pupil is cheaper
     than the CPU-bound gaussian filter on mobile. */
  .hero.is-eye-closed .hero__video { filter: none; }
  .eye-blink__pupil { filter: none; }

  /* Bottom-fade wash disabled on mobile — the effect is now baked into
     the videos/mobile/ re-encodes. Kills the desktop ::after vignette too. */
  .hero::after { content: none; }

  /* Scene-nav arrows — flat, no glass. Split to the outer edges of the
     footer bottom row via absolute positioning inside a full-width
     container. Figma centre positions: 8.93% (prev) and 91.07% (next). */
  .scene-nav {
    position: fixed;
    bottom: 32px;                /* was 40 — reduced 20% */
    left: 0;
    right: 0;
    transform: none;
    display: block;               /* children position themselves absolutely */
    height: 26px;
    z-index: 1020;
    pointer-events: none;         /* let taps pass through the container */
  }
  .scene-nav__btn {
    position: absolute;
    bottom: 0;
    width: 44px;                  /* tap target */
    height: 26px;
    padding: 0;
    background: transparent;
    border: none;
    border-radius: 0;
    -webkit-backdrop-filter: none;
            backdrop-filter: none;
    display: grid;
    place-items: center;
    cursor: pointer;
    pointer-events: auto;         /* re-enable on the buttons themselves */
    -webkit-tap-highlight-color: transparent;
    transition: opacity 180ms ease;
  }
  .scene-nav__btn[data-scene-dir="-1"] { left:  8.93%; transform: translateX(-50%); }
  .scene-nav__btn[data-scene-dir="1"]  { left: 91.07%; transform: translateX(-50%); }
  .scene-nav__icon {
    display: block;
    width: auto;
    height: 22px;                 /* Figma arrows are ~25 tall */
    pointer-events: none;
  }
  .scene-nav__btn[data-scene-dir="-1"] .scene-nav__icon {
    transform: scaleX(-1);        /* flip the right-pointing chevron for "previous" */
  }
  .scene-nav__btn:active { opacity: 0.6; }
  .scene-nav__btn:focus  { outline: none; }
  .scene-nav__btn:focus-visible {
    outline: 1px solid rgba(255, 255, 255, 0.7);
    outline-offset: 3px;
  }

  /* Hero is now at the top and the footer chrome absorbs the black area,
     so the desktop top-wash pseudo-element is unnecessary. */
  body::before  { content: none; }

  /* Top helmet-shade wash disabled on mobile — the effect is now baked
     into the videos/mobile/ re-encodes. */
  .hero::before { content: none; }

  /* Footer chrome (Figma reference — 402×178.5 at iPhone 15 Pro width):
     20 vh strip pinned to the viewport bottom, 40 px top / 20 px side
     padding, grid layout with logo top-left + ABOUT top-right, and the
     audio waveform as a full-width separator line 20 px below them. */
  .hero__chrome {
    position: fixed;
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    height: 164px;               /* sized to make above-line = below-line = 20 px */
    padding: 32px 20px 0;        /* was 40px top — reduced 20% */
    background: #000;
    gap: 0;
    z-index: 1010;
    display: grid;
    grid-template-columns: 45px 1fr auto;
    grid-template-rows: 33px 1px;   /* pin separator to 1px so it can't drift */
    column-gap: 16px;
    row-gap: 20px;
    align-items: center;
    align-content: start;           /* rows sit at top of the grid area */
  }
  .brand {
    grid-column: 1;
    grid-row: 1;
    width: 45px;
    height: 33px;
    pointer-events: none;         /* logo click (game modal) disabled on mobile */
    cursor: default;
  }
  .hero__rule--audio {
    grid-column: 1 / -1;
    grid-row: 2;
    height: 1px;
    width: 100%;
    /* Audio-reactive waveform + tap-to-mute re-enabled on mobile —
       the SVG paints the animated line, audio-rule.js handles the
       pointer-up-to-mute wiring, same as desktop. */
  }
  .hero__rule__path { stroke-width: 1.5; }        /* thinner stroke on mobile */
  .site-nav {
    grid-column: 3;
    grid-row: 1;
    align-self: end;               /* bottom-align ABOUT with logo bottom */
    gap: 4px;
  }

  /* Logo scales down proportionally */
  .brand { width: 56px; }

  /* Nav sits inline within chrome — restore default flow (revert the
     earlier fixed-position override that only moved the nav). */
  .site-nav { gap: 6px; }
  .site-nav a {
    font-size: 12px;
    letter-spacing: 0.02em;
    /* Force "WHO'S RIDING" onto two right-aligned lines. `width: min-content`
       shrinks the anchor to the width of its longest word, so the space
       between the two words becomes a wrap point. */
    display: inline-block;
    width: min-content;
    text-align: right;
    line-height: 1.2;
  }

  /* Sound icon removed on mobile — mute lives on the waveform tap. */
  .sound-toggle { display: none; }

  /* Night toggle centred on the viewport (was at 65 %). */
  .night-toggle {
    top: auto;
    bottom: 32px;
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    width: 26px;
    height: 26px;
  }
  .night-toggle__icon { width: 20px; height: 20px; }

  /* Suppress iOS long-press behaviour on all footer chrome so a
     press-and-hold anywhere only triggers the day↔night gesture on the
     video, not text selection / callouts / tap highlights on the footer. */
  .hero__chrome,
  .hero__chrome *,
  .sound-toggle,
  .sound-toggle *,
  .night-toggle,
  .night-toggle *,
  .scene-nav,
  .scene-nav * {
    -webkit-user-select: none;
            user-select: none;
    -webkit-touch-callout: none;
    -webkit-tap-highlight-color: transparent;
  }

  /* Hero-chat on mobile: centered horizontally over the footer, bottom-
     aligned with the logo/ABOUT baseline, max 2 bubbles visible at a
     time. Overrides the (pointer: coarse) `display: none` further up. */
  .hero-chat {
    display: flex;
    position: fixed;
    top: auto;
    right: auto;
    left: 50%;
    bottom: 99px;                  /* logo/ABOUT bottom = footer height (164) - top-pad (32) - row1 (33) */
    transform: translateX(-50%);
    align-items: center;           /* centre-anchor bubbles horizontally */
    max-width: min(80vw, 300px);
    gap: 8px;
    z-index: 1015;                 /* above footer chrome (1010) */
  }
  .hero-chat > :nth-last-child(n+3) { display: none; }   /* keep only latest 2 */

  /* Scene-nav arrows blink 3× when the mobile hint bubble appears. */
  @keyframes scene-nav-blink {
    0%, 33%, 66%, 100% { opacity: 1; }
    16%, 50%, 83%      { opacity: 0.2; }
  }
  .scene-nav.is-blinking-hint .scene-nav__btn {
    animation: scene-nav-blink 1.2s ease-in-out;
  }

  .hero-chat__bubble,
  .hero-chat__typing {
    padding: 6px 12px;            /* was 12px 18px desktop */
    font-size: 11px;              /* design system label size */
    text-align: center;
  }

  /* Mobile: typing indicator loses the white pill — just three white dots
     bouncing on the transparent background. */
  .hero-chat__typing {
    background: transparent;
    padding: 6px 0;
  }
  .hero-chat__typing span {
    background: #fff;
  }

  /* Hint pinned top-right, below the top chrome row. Same mask-reveal
     behaviour as desktop, just smaller offsets/type. */
  .hint {
    top: 80px;
    right: 20px;
    left: auto;
    bottom: auto;
    max-width: calc(100vw - 40px);
    font-size: 12px;
    padding: 8px 12px;
  }

  /* Preloader on mobile — sizes now match design system:
     title 28px (--type-display-m mobile), caption + button 16px
     (--type-body-m mobile). Card padding + gaps + button padding
     stay preloader-specific. */
  .preloader__card    { padding: 32px; gap: 16px; }   /* uniform 16px between all three rows on mobile */
  .preloader__title   { font-size: 28px; line-height: 1.05; }
  .preloader__caption { font-size: 16px; }
  .preloader__enter   { font-size: 16px; padding: 7px 16px; }

  /* About / Contact panel: shrink padding + text on phones */
  .about-panel__body   { padding: 40px 28px; }
  .about-panel__lede   { font-size: 15px; line-height: 1.5; }
  .about-panel__footer { padding-top: 20px; gap: 12px; }
  .about-panel__meta   { font-size: 14px; line-height: 1.4; }
  .about-panel__close        { width: 20px; height: 20px; }
  .about-panel__close__icon  { width: 100%; height: 100%; }

  /* Hint pinned to a mobile-friendly top-right */
  /* Corner blur pools are desktop-only — off on mobile. */
  .corner-blur { display: none; }

  /* Kill iOS double-tap wait on the audio waveline so mute toggles instantly. */
  .hero__rule__svg { touch-action: manipulation; }
}

/* ==========================================================
   GAME MODAL OVERLAY
   ========================================================== */

.game-modal {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  z-index: 10000;
  background: #000;
  display: flex;
  flex-direction: column;
  opacity: 0;
  pointer-events: none;
  transform: scale(0.96);
  transition: opacity 400ms cubic-bezier(0.16, 1, 0.3, 1), transform 400ms cubic-bezier(0.16, 1, 0.3, 1);
}

.game-modal.is-active {
  opacity: 1;
  pointer-events: auto;
  transform: scale(1);
}

/* Game modal header + close button removed — exit via Escape key
   (handled in js/game-modal.js). */

.game-modal__frame {
  flex: 1;
  width: 100%;
  height: 100%;
  border: none;
  background: #000;
}

