/* =====================================================================
   Heroes — "The Call" early-access landing page
   Design system: Neo Tokyo navy, gold = progression, one accent per region.
   No gradients on chrome. Fitness-first: utility numbers large/clean/white.
   V7: visual design ported from the "The Call V3" prototype — square corners,
   hard offset shadows, mono labels, top-anchored hero, three-column Connection.
   ===================================================================== */

/* ---- Self-hosted type: Plus Jakarta Sans (SIL OFL) ---- */
@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/plus-jakarta-sans-latin-400-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("../fonts/plus-jakarta-sans-latin-600-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../fonts/plus-jakarta-sans-latin-700-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url("../fonts/plus-jakarta-sans-latin-800-normal.woff2") format("woff2");
}

/* ---- Tokens (canonical brand values) ---- */
:root {
  /* Surfaces */
  --surface-base: #0A1020;
  --surface-raised: #0E1526;
  --surface-overlay: #131B2F;
  --line: #1C2740;
  --scrim: #05080F;
  --surface-panel: #081528; /* the Loop panels (the base-Echo capture's own background, so it blends) */

  /* Text */
  --text-primary: #E4E9F4;
  --text-secondary: #9FACC8;
  --text-muted: #6B7A9C;
  --on-accent: #0A1020;

  /* Accents */
  --gold: #E3B95E;   /* PRIMARY — progression / glow / confirmation */
  --gold-pale: #F3DCA3; /* tint of --gold (white-mixed) for the hero subline's second sentence; same hue */
  --blue: #6FA9DC;
  --pink: #E08BAB;
  --green: #5CC5B0;
  --coral: #D8776B;

  /* Single filled control — gold with navy text (V7, prototype: dark on gold, 10.3:1; V6 was white) */
  --button-fill: #E3B95E;
  --button-text: #0A1020;

  /* Radius — square everywhere (V7, prototype look) */
  --r-sm: 0;
  --r-md: 0;
  --r-lg: 0;
  --r-pill: 0;

  /* Hard offset shadow for floating panels (prototype signature; no blur) */
  --shadow-offset: 4px 4px 0 rgba(5, 8, 15, 0.47);

  /* Type */
  --font-sans: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont,
    "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Roboto Mono",
    Menlo, Consolas, monospace;

  /* Motion (V14): one easing set and one duration scale shared by the V14 effects */
  --ease-out: cubic-bezier(0.2, 0.7, 0.2, 1);
  --ease-in: cubic-bezier(0.5, 0, 0.9, 0.4);
  --dur-step: 180ms;  /* one beat of a sequence (Challenge selection, signal bars) */
  --dur-quick: 300ms; /* small reveals and crossfades */
  --dur-pulse: 600ms; /* a glow or bubble entrance */

  /* Spacing rhythm: 1248px content column, 56px side margins at 1440, 20px on phones */
  --maxw: 1248px;
  --gutter: clamp(20px, 3.9vw, 56px);
}

/* ---- Reset ---- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}
body {
  margin: 0;
  background: var(--surface-base);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-weight: 400;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
img { display: block; max-width: 100%; }
a { color: var(--blue); text-decoration: none; }
a:hover { text-decoration: underline; }
button { font-family: inherit; }

/* Pixel art must stay crisp */
.pixelated { image-rendering: pixelated; image-rendering: crisp-edges; }

/* ---- Type scale ---- */
.kicker {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
/* Small mono caption (the prototype's 8–9px labels, at our .68rem floor). */
.label {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
h1, h2, h3 { margin: 0; }
/* Display: prototype's scale and tracking (h1 46–96px, −.05em; h2 30–43px, −.047em). */
h1 {
  font-weight: 800;
  font-size: clamp(2.875rem, 6.8vw, 6rem);
  line-height: 1;
  letter-spacing: -0.05em;
  text-transform: uppercase;
}
h2 {
  font-weight: 800;
  font-size: clamp(1.875rem, 1rem + 2.1vw, 2.6875rem);
  line-height: 1.12;
  letter-spacing: -0.047em;
  text-transform: uppercase;
}
h3 { font-weight: 700; font-size: 0.9375rem; line-height: 1.3; letter-spacing: -0.02em; }
p { margin: 0; }
.num { font-variant-numeric: tabular-nums; color: #fff; }
@media (max-width: 850px) {
  h1 { font-size: clamp(2.875rem, 12vw, 4.625rem); }
}
@media (max-width: 600px) {
  h1 { font-size: clamp(2.875rem, 12vw, 4.375rem); line-height: 1.02; }
  h2 { line-height: 1.15; }
}

/* ---- Layout ---- */
.wrap {
  width: 100%;
  max-width: calc(var(--maxw) + 2 * var(--gutter));
  margin: 0 auto;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}
section { position: relative; }
/* Breathing but tight section rhythm (V2: less blank space). */
.section-pad { padding-block: clamp(64px, 10vw, 120px); }

/* Config-driven product / city names (owner-editable placeholders). */
.cfg-name { color: inherit; }

/* =====================================================================
   Buttons — the gold fill is the SINGLE filled control
   (prototype: mono caps, square, arrow at the far edge, navy notch top-right)
   ===================================================================== */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: 25px;
  min-height: 54px;
  padding: 15px 20px;
  border-radius: var(--r-sm);
  border: 1px solid transparent;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 0.75rem;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: transform 0.18s ease, filter 0.18s ease, background 0.18s ease, color 0.18s ease;
  text-decoration: none;
}
.btn:hover { text-decoration: none; }
.btn__arrow { font-family: var(--font-sans); font-size: 1.25rem; font-weight: 400; line-height: 1; }
.btn-primary {
  background: var(--button-fill);
  color: var(--button-text);
  border-color: var(--button-fill);
}
/* Corner notch: a navy triangle cutting the top-right corner. Its colour must match the surface behind the
   button (--btn-notch), so it is only used where that surface is (near-)solid navy. */
.btn-primary::after {
  content: "";
  position: absolute;
  right: -1px; top: -1px;
  border-top: 8px solid var(--btn-notch, var(--surface-base));
  border-left: 8px solid transparent;
}
.btn-primary:hover { transform: translateY(-2px); filter: brightness(1.08); }
.btn-primary:active { transform: translateY(0); }
.btn-ghost {
  background: transparent;
  color: var(--text-primary);
  border-color: var(--line);
}
.btn-ghost:hover { border-color: var(--gold); color: var(--gold); }
/* Focus ring sits outside the control, on the navy surface; off-white so it reads against both navy and the gold fill. */
.btn:focus-visible {
  outline: 2px solid var(--text-primary);
  outline-offset: 5px;
}
.btn[disabled] { opacity: 0.55; cursor: wait; transform: none; }

/* =====================================================================
   Status chip
   ===================================================================== */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  padding: 0.4em 0.85em;
  border-radius: var(--r-pill);
  background: var(--surface-overlay);
  box-shadow: inset 0 0 0 1px var(--line);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.chip .dot {
  width: 5px; height: 5px;
  background: var(--gold);
  animation: pulse 2.4s ease-in-out infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

/* =====================================================================
   HERO — V9 (owner mockups, HER-15). One viewport: headline → bold subline
   → CTA → beta label, vertically centred in the left column; the player
   centre-right standing on the street line; the incoming-message panel
   top-right (desktop/tablet only). Phones stack headline → subline → player
   → full-width CTA → centred label on the first screen.
   ===================================================================== */
.hero {
  position: relative;
  z-index: 1;            /* paints above <main>, so the Ascension burst may overflow the bottom edge unclipped */
  display: flex;
  flex-direction: column;
  min-height: clamp(640px, 92svh, 820px);
  overflow-x: clip;      /* the sprite canvas is wider than the viewport; vertical overflow stays visible (no clip) */
  background: var(--scrim);
  border-bottom: 1px solid var(--line);
  isolation: isolate;
}
/* STATIC hero background (right-anchored, as in the prototype). A richer cinematic treatment can be dropped into
   .hero__bg later (e.g. video/canvas) without changing the surrounding markup. */
.hero__fallback {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; max-width: none;
  object-fit: cover; object-position: 90% 40%;
}
.hero__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: url("../img/first_walk.png");
  background-size: cover;
  background-position: 90% 40%;
  image-rendering: pixelated;
  transform: scale(1.04); /* slight static crop; no animation */
}
/* Legibility scrim over the imagery (not chrome). V9: lighter than V8 so the street reads under the copy
   (the mockups show the city across the whole width); the bottom fades to navy. */
.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    linear-gradient(90deg, rgba(10,16,32,0.90) 0%, rgba(10,16,32,0.78) 30%, rgba(10,16,32,0.45) 52%, rgba(10,16,32,0.06) 78%),
    linear-gradient(0deg, var(--surface-base) 0%, transparent 22%);
  pointer-events: none;
}
/* Tasteful STATIC scanline veil (no animation) for a transmission feel. */
.hero__scan {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0.4;
  background-image: repeating-linear-gradient(
    180deg, rgba(111,169,220,0.04) 0px, rgba(111,169,220,0.04) 1px,
    transparent 2px, transparent 4px);
}

/* The content column fills the hero (so the sprite stage and the panel can be placed against the hero's box)
   and centres the copy vertically. */
.hero__inner {
  position: relative;
  z-index: 3;
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  padding-top: 56px;
  padding-bottom: 56px;
}
/* The copy paints above the sprite (z 2 > stage z 1) so the effect passes behind it; the panel is above both. */
.hero__title, .hero__sub, .hero__cta, .hero__beta { position: relative; z-index: 2; }

/* Headline: the mockup's scale — "IS CALLING" ends at ~46% of a 1440 viewport (114px; V8 was 96px). */
.hero__title {
  margin: 0;
  font-size: clamp(2.875rem, 7.9vw, 7.125rem);
  line-height: 1.05;
}
.hero__accent { color: var(--gold); }
/* Subline: bold caps, two lines; the second sentence in pale gold. */
.hero__sub {
  margin-top: 26px;
  max-width: 640px;
  font-weight: 800;
  font-size: clamp(1.5rem, 2.95vw, 2.625rem);
  line-height: 1.2;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  color: var(--text-primary);
}
.hero__sub-b { color: var(--gold-pale); }
.hero__cta { margin-top: 36px; display: flex; align-items: center; }
.hero__beta {
  margin-top: 18px;
  font-weight: 800;
  font-size: 1rem;
  line-height: 1.3;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-primary);
}

/* The player. Art (V8): 352x320 native canvas shipped at 3x (1056x960), rendered at whole multiples of the native
   grid so it stays crisp. Figure 38x115 native px at x 158-196 (centre 177), feet on row 218; the Ascension
   effect spans x 32-319, y 26-285 (a 5-frame burst) plus a persistent ground disc on rows 219-225, and must stay
   unclipped: .hero__stage gives it room (no overflow clipping) and the hero clips horizontally only.
   Desktop x3: the stage covers the hero; the figure's centre sits at 68.75% of the width (the mockup's 990px at
   1440) and its feet on the street line at 85% of the hero's height, so left = 68.75% - 177*3 and
   top = 85% - 218*3. x4 (canvas 1408x1280, effect 1036px tall) cannot fit any hero <= 820px without clipping. */
.hero__stage {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}
.hero__stage > picture { display: contents; }
.hero__player {
  position: absolute;
  width: 1056px;
  height: 960px;
  max-width: none;
  left: calc(68.75% - 531px);
  top: calc(85% - 654px);
  /* Whole-pixel placement where round() is supported (a fractional offset can blur pixel art in some engines). */
  left: round(calc(68.75% - 531px), 1px);
  top: round(calc(85% - 654px), 1px);
}

/* =====================================================================
   SIGNAL PANEL — the shared incoming-message component (V10): top-right of
   the hero and of sections 2–5, each with its own quote. The DOM is the final
   "locked" state; with motion the bars blink and the progress bar fills once.
   --alert is the red (coral) variant for the Stakes. V11 makes it a sliding
   dialogue (see below) and gives phones a compact version.
   ===================================================================== */
.sigpanel {
  position: absolute;
  z-index: 3;
  width: 272px;
  margin: 0;
  padding: 16px 18px;
  background: rgba(14, 21, 38, 0.82); /* surface-raised, glass */
  border: 1px solid var(--line);
  box-shadow: var(--shadow-offset);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  --sig-accent: var(--gold);
}
.sigpanel::before {
  content: ""; position: absolute; top: -1px; left: -1px;
  width: 20px; height: 2px; background: var(--sig-accent);
}
.sigpanel__top { display: flex; align-items: center; gap: 10px; margin: 0; }
.sigpanel .signal { display: flex; align-items: flex-end; gap: 2px; height: 13px; }
.sigpanel .signal i { display: block; width: 2px; height: 4px; background: var(--text-secondary); }
.sigpanel .signal i:nth-child(2) { height: 7px; }
.sigpanel .signal i:nth-child(3) { height: 9px; }
.sigpanel .signal i:nth-child(4) { height: 11px; }
.sigpanel .signal i:nth-child(5) { height: 13px; }
.sigpanel__quote {
  margin: 14px 0 17px;
  font-size: 1rem; font-weight: 600; line-height: 1.6; letter-spacing: -0.01em;
  color: var(--text-primary);
}
.sigpanel__foot { display: flex; justify-content: space-between; align-items: center; margin: 0; color: var(--text-muted); }
.sigpanel .bar { display: block; width: 62px; height: 3px; background: var(--line); }
.sigpanel .bar i { display: block; width: 100%; height: 100%; background: var(--sig-accent); }
/* The acquire animation replays on every slide-in (keyed to .is-in, which only JS sets). */
.motion-ok .sigpanel.is-in .bar i { animation: acquire 3.8s steps(12, end) 1 both; }
.motion-ok .sigpanel.is-in .signal i { animation: signal-bars 0.6s ease-in-out 5 alternate; }
.motion-ok .sigpanel.is-in .signal i:nth-child(even) { animation-delay: 0.2s; }
@keyframes acquire { from { width: 0; } to { width: 100%; } }
@keyframes signal-bars { from { opacity: 0.3; } to { opacity: 1; } }

/* Sliding dialogue (V11). The DOM/no-JS state is the static one above (visible on desktop, hidden on phones).
   With JS (html.js, set inline in <head>) every panel starts off-stage to the right; main.js adds .is-in when its
   section scrolls into view (slide in, 320ms ease-out) and .is-out once the visitor scrolls 50px either way
   (slide out, 260ms ease-in); leaving the section clears both so it re-arms. Each section clips horizontally,
   so the off-stage panel never widens the page. Reduced motion: no translation, fade only (same triggers). */
html.js .sigpanel { opacity: 0; transform: translateX(120%); transition: none; pointer-events: none; }
html.js .sigpanel.is-in { opacity: 1; transform: none; transition: transform 320ms ease-out, opacity 320ms ease-out; }
html.js .sigpanel.is-in.is-out { opacity: 0; transform: translateX(120%); transition: transform 260ms ease-in, opacity 260ms ease-in; }
@media (prefers-reduced-motion: reduce) {
  html.js .sigpanel { transform: none !important; transition-property: opacity !important; }
}
/* Phones: hidden without JS (as V10); with JS a compact 260px version per section, placed where it covers no
   headline or CTA: in the flow above the hero's and the Loop's headline and above the Decision copy (so it can
   push nothing over), top-right over the World's backdrop, bottom-right over the Stakes backdrop. */
@media (max-width: 720px) {
  .sigpanel { display: none; }
  html.js .sigpanel { display: block; width: 260px; padding: 10px 14px; }
  html.js .sigpanel__quote { margin: 8px 0 10px; font-size: 0.8125rem; line-height: 1.5; }
  html.js .sigpanel--alert .sigpanel__quote { margin: 10px 0 12px; font-size: 1.125rem; }
  html.js .hero__panel { position: static; order: -1; align-self: flex-end; margin: 0 0 8px; }
  html.js .world__panel { top: 14px; right: 20px; }
  html.js .loop__inner { display: flex; flex-direction: column; }
  html.js .loop__panel { position: static; order: -1; align-self: flex-end; margin: 0 0 20px; }
  html.js .stakes__panel { top: auto; bottom: 20px; right: 20px; }
  html.js .decide__side { display: contents; }
  html.js .decide__panel { position: static; order: -1; justify-self: end; margin: 0; box-shadow: var(--shadow-offset); }
}
/* Red variant (Stakes): tick, bar and the quote in coral; the quote set larger and centred, as drawn. */
.sigpanel--alert { --sig-accent: var(--coral); }
.sigpanel--alert .sigpanel__quote {
  margin: 18px 0 22px;
  text-align: center;
  font-size: 1.5rem; line-height: 1.2; letter-spacing: -0.02em;
  color: var(--coral);
}
/* Quote alignment as drawn: flush right (sections 2, 5) or centred (section 3). */
.sigpanel--right .sigpanel__quote { text-align: right; }
.sigpanel--center .sigpanel__quote { text-align: center; }
/* Hero placement (V9 values, unchanged). */
.hero__panel { top: 43px; right: max(var(--gutter), calc((100% - var(--maxw)) / 2)); }

/* Tablets (721-1099px): same composition at x2 (704x640; left = 68.75% - 177*2, top = 85% - 218*2), narrower
   type so the column stays clear of the figure. */
@media (max-width: 1099px) {
  .hero__player { width: 704px; height: 640px; left: calc(68.75% - 354px); top: calc(85% - 436px); left: round(calc(68.75% - 354px), 1px); top: round(calc(85% - 436px), 1px); }
  .hero__panel { width: 240px; top: 44px; }
  .hero__title { font-size: clamp(2.875rem, 9vw, 6.5rem); }
  .hero__sub { font-size: clamp(1.375rem, 4vw, 2.5rem); max-width: 56%; }
}
/* Phones: stacked on the first screen (390x844). The stage is a block in the flow sized for the x2 figure
   and its ground disc only (head 24px below the stage top, disc bottom 4px above the stage bottom: 24 + (226-104)*2
   + 4 = 272px); the burst's upper part passes behind the subline/headline and its lower part behind the CTA
   (V11: nothing is reserved below the disc, so the figure stands 20px above the button).
   left = 50% - 177*2 centres the figure; top = 24px - 104*2 puts the head 24px below the stage top. */
@media (max-width: 720px) {
  .hero__inner { justify-content: flex-start; align-items: stretch; padding-top: 28px; padding-bottom: 28px; }
  .hero__title { font-size: clamp(2.875rem, 17.2vw, 4.25rem); }
  .hero__sub { margin-top: 18px; max-width: 100%; font-size: clamp(1.375rem, 7.2vw, 1.75rem); line-height: 1.15; }
  .hero__stage { position: relative; inset: auto; height: 272px; margin-top: 16px; }
  .hero__player { left: calc(50% - 354px); top: calc(24px - 208px); left: round(calc(50% - 354px), 1px); }
  /* CTA (V11): centred, not edge-to-edge (82% of the column or its content, whichever is wider), bigger label. */
  .hero__cta { margin-top: 16px; justify-content: center; }
  .hero__cta .btn { width: auto; min-width: 82%; max-width: 100%; font-size: 1rem; padding: 15px 20px; }
  .hero__beta { margin-top: 14px; text-align: center; font-size: 0.9375rem; }
  .hero__scrim {
    background:
      linear-gradient(90deg, rgba(10,16,32,0.84) 0%, rgba(10,16,32,0.72) 30%, rgba(10,16,32,0.35) 90%),
      linear-gradient(0deg, var(--surface-base) 8%, transparent 45%);
  }
}
/* Short phones (375x667 class): type scales down first; the stage holds the figure and its ground disc with the
   head 8px below the stage top (8 + 244 + 4 = 256px), the same 20px disc → button gap. */
@media (max-width: 720px) and (max-height: 700px) {
  .hero__inner { padding-top: 12px; padding-bottom: 16px; }
  .hero__title { font-size: clamp(2.75rem, 16vw, 4rem); }
  .hero__sub { margin-top: 14px; font-size: clamp(1.25rem, 6.8vw, 1.625rem); }
  .hero__stage { height: 256px; margin-top: 12px; }
  .hero__player { top: calc(8px - 208px); }
  .hero__cta { margin-top: 16px; }
  .hero__beta { margin-top: 6px; }
  /* The in-flow panel (JS) must leave the CTA on the first screen here: tighter, and without its footer row. */
  html.js .hero__panel { padding: 8px 12px; margin-bottom: 6px; }
  html.js .hero__panel .sigpanel__quote { margin: 6px 0 2px; line-height: 1.4; }
  html.js .hero__panel .sigpanel__foot { display: none; }
}
/* Landscape phones: no vertical room for the figure, so it is hidden rather than crowd the copy. */
@media (max-width: 720px) and (max-height: 560px) {
  .hero__stage { display: none; }
}
/* Short landscape tablets/laptops: smaller display type so the CTA stays on the first screen. */
@media (min-width: 721px) and (max-height: 560px) {
  .hero__title { font-size: clamp(2.6rem, 6vw, 4rem); }
  .hero__sub { font-size: clamp(1.125rem, 2.4vw, 1.5rem); margin-top: 16px; }
  .hero__cta { margin-top: 22px; }
}

/* =====================================================================
   SPRITES — owner-supplied pixel art at whole multiples of its native grid.
   Echo v2 GIFs: 112x168 grid (shipped at x2). Colossus GIF: 216x256 grid.
   --k / --kc set the scale per breakpoint.
   ===================================================================== */
.spr { display: block; max-width: none; flex: 0 0 auto; }
.spr--v2 {
  --k: 2;
  width: calc(112px * var(--k)); height: calc(168px * var(--k));
}
.spr--colossus {
  --kc: 2;
  width: calc(216px * var(--kc)); height: calc(256px * var(--kc));
}
/* Pictures around sprites must not become extra boxes in flex rows. */
.lpanel__stages > picture, .stakes__foe > picture { display: contents; }

/* Visually hidden (screen-reader) text. */
.sr-only {
  position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* =====================================================================
   2 · THE WORLD — full-bleed layered cityscape (owner layers, composed in
   CSS), display headline + city line centred, signal panel top-right.
   The art is a 1440x700 stage (aspect 72:35) anchored to the bottom and
   scaled by its width: 100% of the section on desktop, zoomed on phones.
   Layer offsets are the mockup's, in % of the stage (left/width of its
   width, top of its height).
   ===================================================================== */
.world {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  background: var(--surface-base); /* the sky layer's own top tone, for any gap above the art */
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  aspect-ratio: 1440 / 700;
  min-height: 520px;
  max-height: 760px;
  --art-w: 100%;
}
.world__stage {
  position: absolute; z-index: 0;
  left: 50%; bottom: 0;
  width: var(--art-w);
  aspect-ratio: 1440 / 700;
  transform: translateX(-50%);
  pointer-events: none;
}
.world__layer { position: absolute; max-width: none; height: auto; }
/* Back to front: sky+skyline (covers the stage) → left building → garage → right shop → fog (front). */
.world__sky, .world__fog { left: 0; top: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 100%; }
.world__sky { z-index: 0; }
.world__bldg-left { z-index: 1; left: -0.5%; top: -1.7%; width: 32.8%; }
.world__garage { z-index: 2; left: 14.5%; top: 28.4%; width: 25%; }
.world__shop { z-index: 3; left: 72%; top: 6.1%; width: 30.6%; }
.world__fog { z-index: 4; }

.world__inner {
  position: relative; z-index: 2;
  height: 100%;
  display: flex; flex-direction: column; justify-content: center; align-items: center;
  text-align: center;
  padding-top: 40px; padding-bottom: 40px;
}
/* Headline: the mockup's scale — "EXPLORE A FRACTURED" is ~749px wide at 1440 (≈67px). */
.world__title {
  font-size: clamp(1.9rem, 4.65vw, 4.1875rem);
  line-height: 1.03;
  letter-spacing: -0.03em;
  text-shadow: 0 2px 24px rgba(5, 8, 15, 0.55);
}
.world__sub {
  margin-top: clamp(22px, 3vw, 44px);
  max-width: 540px;
  font-weight: 700;
  font-size: clamp(0.9375rem, 1.32vw, 1.1875rem);
  line-height: 1.5;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--text-primary);
  text-shadow: 0 1px 12px rgba(5, 8, 15, 0.6);
}
.world__city { color: var(--gold); }
.world__panel { top: 52px; right: max(var(--gutter), calc((100% - var(--maxw)) / 2)); }

/* Tablets: the stage is wider than the section (zoomed), bottom-anchored. */
@media (max-width: 1099px) {
  .world { --art-w: 150%; aspect-ratio: auto; height: 560px; }
  .world__panel { width: 240px; top: 36px; }
}
/* Phones: same composition, zoomed and cropped; the panel is hidden as in the hero. */
@media (max-width: 720px) {
  .world { --art-w: 250%; height: 520px; min-height: 0; }
  .world__inner { padding-left: 20px; padding-right: 20px; }
  .world__title { font-size: clamp(1.75rem, 8vw, 2.5rem); line-height: 1.05; }
  .world__sub { margin-top: 18px; font-size: 0.9375rem; line-height: 1.45; }
}
@media (max-width: 720px) and (max-height: 700px) {
  .world { height: 460px; }
}

/* =====================================================================
   3 · THE LOOP — gold heading, signal panel, three square panels whose
   overlaps are placed absolutely in % of the panel (376px at 1440).
   ===================================================================== */
.loop {
  background: var(--surface-base);
  padding-block: 128px 120px; /* V12 (owner: "feels tight"): was 58px 96px */
  overflow-x: clip; /* the Activity card overlaps the panel's left edge; never a horizontal scroll */
}
.loop__inner { position: relative; }
.loop__title { max-width: 60%; letter-spacing: -0.02em; }
.loop__accent { color: var(--gold); }
.loop__panel { top: -4px; right: var(--gutter); }

/* V12: the row keeps 72px below the panels (the hanging cards overhang the panel bottom by ~24px at 1440, so
   ~48px of clear air) before the section's bottom padding. Column gap stays 53px: the panel width (and every
   overlap measured in % of it, and the 380px Ascend row that fills it) depends on it. */
.loop__panels {
  list-style: none; margin: 0; padding: 0 0 89px; /* V14: 48px clear below the 98px Auto-log card (was 72) */
  margin-top: 140px; /* heading bottom → panel-head top as mocked (kept; already ≥ the V12 96px target) */
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 53px;
}
.lpanel { position: relative; min-width: 0; }
/* Diamond marker + name + line, above each panel. */
.lpanel__head {
  position: relative;
  min-height: 46px;
  padding-left: 49px;
  margin-bottom: 36px; /* V12: was 22px */
}
.lpanel__diamond {
  position: absolute; left: 5px; top: 9px;
  width: 28px; height: 28px;
  border: 1.5px solid var(--gold);
  transform: rotate(45deg);
}
.lpanel__name {
  font-weight: 700; font-size: 1.375rem; line-height: 1.2; letter-spacing: -0.01em;
  text-transform: uppercase; color: var(--text-primary);
}
.lpanel__desc { margin-top: 2px; font-size: 0.8125rem; line-height: 1.5; color: var(--text-primary); }

/* The square panel. Overflow stays visible (cards hang over the bottom edge); only the ghost is clipped. */
.lpanel__box {
  position: relative;
  aspect-ratio: 1 / 1;
  background: var(--surface-panel);
}
.lpanel__num {
  position: absolute; z-index: 0;
  left: 2%; top: -4%;
  font-weight: 800; font-size: 175px; line-height: 1; letter-spacing: -0.05em;
  color: var(--line);
  user-select: none;
}
.lpanel__clip { position: absolute; inset: 0; z-index: 1; overflow: hidden; }

/* Floating cards: dark navy, 1px line, rounded as drawn (the only rounded chrome on the page). */
.xcard {
  position: absolute; z-index: 3;
  background: var(--surface-base);
  border: 1px solid var(--line);
  border-radius: 10px;
}
.xcard__title {
  font-weight: 500; font-size: 1.25rem; line-height: 1.25; letter-spacing: 0;
  text-transform: uppercase; color: var(--text-secondary);
}

/* 1 · TRAIN: base Echo top-right (the capture's figure is 259px tall at 1440), Activity card over the left
   edge, Auto-log card over the bottom edge. */
.lpanel__base {
  position: absolute; z-index: 2;
  right: 11.5%; top: 3.5%;
  height: 83%; width: auto; max-width: none;
  image-rendering: auto;
}
.xcard--activity {
  left: -10.1%; top: 31%; width: 76.3%;
  padding: 14px 13px 12px;
}
.xcard__kicker { font-size: 0.875rem; line-height: 1.3; color: var(--text-secondary); }
.xcard__activity {
  display: flex; align-items: center; gap: 6px;
  margin-top: 4px;
  font-size: 0.875rem; font-weight: 500; line-height: 1.3; color: var(--coral);
}
.xcard__activity img { width: 18px; height: 18px; flex: none; }
.xcard__metrics { display: flex; justify-content: space-between; align-items: flex-end; gap: 12px; margin-top: 10px; }
.xcard__metrics p { display: grid; gap: 4px; }
.xcard__metrics .label { font-size: 0.625rem; letter-spacing: 0.12em; }
.xcard__num {
  font-size: 1.625rem; font-weight: 600; line-height: 1.1; letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums; color: #fff;
}
.xcard__num span { color: var(--text-secondary); }
.xcard__xp { text-align: right; justify-items: end; }
.xcard__xp strong {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 1.125rem; font-weight: 500; line-height: 1.1; color: var(--gold); font-variant-numeric: tabular-nums;
}
.xcard__xp img { width: 15px; height: 12px; }
.xcard__bar { display: block; height: 3px; margin-top: 12px; background: var(--line); overflow: hidden; }
/* V14: full (the level-up is the finished state); with motion it fills from 62% (transform only, see Phase 3). */
.xcard__bar span { position: relative; display: block; height: 100%; width: 100%; background: var(--gold); transform-origin: 0 50%; }
.xcard__lbl { display: inline-grid; }
.xcard__lbl > span { grid-area: 1 / 1; justify-self: start; }
.xcard__lbl-xp { opacity: 0; }

/* Auto-log card (V14 owner fix): matched to the owner's reference crop. Measured there (tile bodies, glow
   excluded): Apple Health = 0.726 of the card's inner height; Oura and WHOOP 0.439 of Health each with a 0.138
   gap, the stack spanning Health top to bottom; the column 0.215 of Health to its right; padding 0.252 (left) /
   0.244 (right); label cap height 0.211 of Health, two lines, left. Shipped (desktop, label kept at 20px):
   Health 70, tiles 30 + 10 + 30, column gap 15, padding 18 / 17, card 288 x 98 (inner ratio 0.729); phones scale
   the group to Health 60, tiles 26 + 8 + 26, label 18px, card 252 x 85. The card is centred where V13 had it
   (48.5% of the panel). Icons are Lanczos exports of the 400px originals at the desktop size x 2. */
.xcard--autolog {
  --hs: 70px; --ts: 30px; --tg: 10px; --cg: 15px; --cw: 288px;
  left: calc(48.5% - var(--cw) / 2); top: 85%; width: var(--cw); height: 98px;
  padding: 0 17px 0 18px;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.xcard--autolog .xcard__title { font-weight: 400; line-height: 1.2; }
.xcard__apps { position: relative; flex: none; width: calc(var(--hs) + var(--cg) + var(--ts)); height: var(--hs); }
/* App icons (not pixel art): smooth scaling, their own baked-in rounded corners; a soft white glow that follows
   the rounded tile (drop-shadow, not box-shadow), measured on the reference: it fades out over ~0.09 of Health's
   size, brighter beside the white Health tile than beside the dark ones. */
.xcard__app { position: absolute; image-rendering: auto; filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.3)); }
.xcard__app--health { left: 0; top: 0; width: var(--hs); height: var(--hs); filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.45)); }
.xcard__app--oura, .xcard__app--whoop { left: calc(var(--hs) + var(--cg)); width: var(--ts); height: var(--ts); }
.xcard__app--oura { top: 0; }
.xcard__app--whoop { top: calc(var(--ts) + var(--tg)); }

/* 2 · ASCEND (V11): Base → arrow → II → arrow → III at x2, feet on one line (the three GIFs share one 112x168
   canvas, bottom-aligned), centred; the row is 380px wide, so it fills the 381px panel at 1440 and bleeds a
   little past the panel's edges on phones (integer scale kept). Collect-gear card over the bottom edge. */
/* V13: the group is centred with four EQUAL gaps (left edge → Base = Base → II (arrow inside) = II → III = III →
   right edge). Each sprite is trimmed to its exact visible bounds (union over every GIF frame, native grid: Base
   x 38-76, II x 35-80, III x 27-88 incl. its shadow = 76 + 90 + 122 = 288px at x2), so the gap is
   (panel - 288) / 4: 23px at 1440. It never goes below 16px: on phones the between-gaps are 16px and the 320px
   row is centred, leaving 15px edges at 390 and 7.5px at 375 (no bleed), the x2 integer scale kept. The arrows sit
   out of flow, centred in their gap, small enough to leave air on both sides. The gap is applied as the second
   and third sprite's left margin, not flex `gap`: Chromium also inserts `gap` around the absolutely positioned
   arrow spans between the sprites, which doubled it. */
.lpanel__stages {
  --k: 2; /* the sprites' scale (.spr--v2 sets the same); the maths below follow it */
  --sprites: calc(76px * var(--k) / 2 + 90px * var(--k) / 2 + 122px * var(--k) / 2); /* 288px at --k: 2 */
  --g: max(16px, calc((100% - var(--sprites)) / 4));
  position: absolute; z-index: 2;
  left: 0; right: 0; top: 0; height: 83%;
  display: flex; align-items: flex-end; justify-content: center; gap: 0;
}
.lpanel__arrow {
  position: absolute; bottom: 53%; /* the V11 height (44% of the panel width above the stage bottom) */
  transform: translateX(-50%);
  color: var(--gold); font-size: 1rem; line-height: 1;
}
/* Gap centres: start = 50% - (288px + 2g) / 2; arrow 1 = start + 76 + g/2; arrow 2 = start + 76 + g + 90 + g/2. */
.lpanel__arrow:nth-of-type(1) { left: calc(50% - var(--sprites) / 2 + 76px * var(--k) / 2 - var(--g) / 2); }
.lpanel__arrow:nth-of-type(2) { left: calc(50% - var(--sprites) / 2 + 166px * var(--k) / 2 + var(--g) / 2); }
.lpanel__i { margin: 0 calc(-36px * var(--k)) 0 calc(-38px * var(--k)); }
.lpanel__ii { margin: 0 calc(-32px * var(--k)) 0 calc(-35px * var(--k) + var(--g)); }
.lpanel__iii { margin: 0 calc(-24px * var(--k)) 0 calc(-27px * var(--k) + var(--g)); }
.xcard--gear {
  left: 18.6%; top: 85%; width: 62.8%; height: 81px;
  padding: 10px 14px;
}
.gear { position: absolute; left: 0; top: 0; width: 100%; height: 100%; }
/* Item icons (owner-supplied) scaled smoothly, laid out as a collage (V11) across the whole width right of
   the "COLLECT GEAR" label (its text ends at 45% of the card): varied sizes and offsets, slight overlaps, every
   quadrant of that region filled, each rarity tag on its item. Lefts in % of the card so the phone card (72% of
   the panel) keeps the spread; tops/widths in px against the fixed 81px card height. */
.gear__item { position: absolute; image-rendering: auto; height: auto; }
.gear__hoodie { left: 46%; top: 26px; width: 46px; }
.gear__vest { left: 58%; top: -10px; width: 50px; }
.gear__cap { left: 64%; top: 44px; width: 40px; }
.gear__shades { left: 79%; top: 2px; width: 40px; }
.gear__pants { left: 85%; top: 28px; width: 46px; }
.gear__tag {
  position: absolute;
  padding: 0 4px;
  border-radius: 999px;
  border: 1px solid currentColor;
  background: var(--surface-base);
  font-style: normal; font-weight: 700; font-size: 0.375rem; line-height: 1.5; letter-spacing: 0.04em;
  text-transform: uppercase;
}
.gear__tag--legend { left: 54%; top: 0; color: var(--gold); }
.gear__tag--common { color: var(--text-secondary); }
.gear__tag--shades { left: 84%; top: -2px; }
.gear__tag--hoodie { left: 48%; top: 66px; }
.gear__tag--uncommon { left: 66%; top: 74px; color: var(--green); }
.gear__tag--rare { left: 88%; top: 62px; color: var(--blue); }

/* 3 · CHALLENGE: the dissolving silhouette (V11: moved left) and the back-view fighter in front of it, bleeding
   off the panel's left edge; both clipped by the panel (.lpanel__clip). The Fight-back card is outside the clip
   so it still hangs over the bottom edge, above the fighter. */
.lpanel__ghost {
  position: absolute;
  left: 6%; top: 2%;
  width: 110%; height: auto; max-width: none;
  image-rendering: auto;
}
/* V13: the fighter is 20% larger (38.5% → 46.2% of the panel; the shipped PNG is the owner zip's full 516x498
   source, rendered at 176px at 1440, so no upscale) and covers the panel's bottom-left corner: the art's opaque
   bottom row sits 1% below the panel edge and its opaque left edge (56px into the 516px canvas) 3% past the left
   edge, both clipped by .lpanel__clip, so no panel background shows below or left of him. */
.lpanel__fighter {
  position: absolute; z-index: 2;
  left: -8%; bottom: -1%;
  width: 46.2%; height: auto;
  image-rendering: auto;
}
/* V13: wide enough for its content (label + 173px chips + padding = 266px at 1440) so the chips no longer
   overflow the card's right border (V12: 5px past it at 1440, 1.5px at 375, which read as the card being clipped
   by the panel edge). Nothing clips the card: it is outside .lpanel__clip and above it (z 3 over z 1). */
.xcard--fight {
  left: 14%; top: 85%; width: 72%; height: 81px;
  padding: 10px 12px 10px 16px;
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
.xcard__acts { position: relative; flex: none; width: 173px; }
.xcard__actions { width: 100%; height: auto; image-rendering: auto; }
/* V14: chip outlines in the app's selected-action style (BossView: gold stroke at 0.9, 1.5pt, gold glow at 0.35),
   scaled to the 173px chips. Boxes measured on the 508x209 source: chips x 7-247 / 260-499, y 5-98 / 109-202,
   corner radius 13px (4.5px here). CENTER is the selected one. */
.xcard__sel {
  position: absolute; width: 47.44%; height: 44.98%;
  border: 1.5px solid color-mix(in srgb, var(--gold) 90%, transparent); border-radius: 4.5px;
  box-shadow: 0 0 8px color-mix(in srgb, var(--gold) 35%, transparent);
  opacity: 0;
}
.xcard__sel--attack { left: 1.38%; top: 2.39%; }
.xcard__sel--guard { left: 51.18%; top: 2.39%; }
.xcard__sel--analyze { left: 1.38%; top: 52.15%; }
.xcard__sel--center { left: 51.18%; top: 52.15%; opacity: 1; }

/* Below 1100px the three panels stack (max 440px wide, keeping every overlap at the panel's own scale). */
@media (max-width: 1099px) {
  .loop { padding-block: 112px 104px; } /* V12: was 48px 72px */
  .loop__title { max-width: 100%; padding-right: 280px; }
  .loop__panel { top: -8px; }
  /* V12: 128px between the stacked cards (was 56px) so each hanging card clears the next title; 80px below the
     heading (was 56px); the hanging card's ~28px overhang + 44px clear air below the stack. */
  .loop__panels { margin-top: 80px; padding-bottom: 77px; grid-template-columns: minmax(0, 440px); gap: 128px; }
  .lpanel__num { font-size: 160px; }
}
@media (max-width: 720px) {
  .loop { padding-block: 96px 88px; } /* V12: was 40px 64px */
  /* V11: heading, panel titles and their lines centred; smaller diamonds; more air between the stacked cards.
     V12: 64px heading → panels (was 36px), 128px between the stacked cards (was 72px), 32px title row → card. */
  .loop__title { padding-right: 0; text-align: center; }
  .loop__panels { margin-top: 64px; gap: 128px; }
  .lpanel__head {
    min-height: 0; padding-left: 0; margin-bottom: 32px;
    display: flex; flex-wrap: wrap; justify-content: center; align-items: center; column-gap: 14px;
    text-align: center;
  }
  .lpanel__diamond { position: static; flex: none; width: 22px; height: 22px; margin: 0 5px; }
  .lpanel__name { flex: none; }
  .lpanel__desc { flex: 1 0 100%; margin-top: 4px; }
  .lpanel__arrow { font-size: 0.875rem; } /* the 16px minimum gap: a smaller arrow keeps air on both sides */
  /* Overlaps stay, but nothing may cross the viewport edge (20px gutters). */
  .xcard--activity { left: -14px; width: 84%; }
  .xcard__metrics .label { letter-spacing: 0.08em; }
  .xcard--gear, .xcard--fight { left: 12%; width: 72%; }
  /* V13: the Fight-back card holds its content at 375 (275px for 266px). V14: the Auto-log group scaled to
     phones (Health 60, tiles 26 + 8 + 26, label 18px, card 252 x 85). */
  .xcard--autolog { --hs: 60px; --ts: 26px; --tg: 8px; --cg: 13px; --cw: 252px; height: 85px; padding: 0 15px; }
  .xcard--autolog .xcard__title { font-size: 1.125rem; }
  .xcard--autolog .xcard__app { filter: drop-shadow(0 0 4px rgba(255, 255, 255, 0.3)); }
  .xcard--autolog .xcard__app--health { filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.45)); }
  .xcard--fight { left: 9%; width: 82%; }
  .lpanel__num { font-size: 140px; }
}

/* =====================================================================
   4 · STAKES — see the section comment in index.html. (Styles below are the
   V7–V9 rules; V10 adds the red signal panel top-right and the mockup's
   height.)
   ===================================================================== */
/* =====================================================================
   BEAT 3 · STAKES — "Transmission interrupted": Colossus as a near-black
   silhouette over his darkened gym, two of his lines beside him.
   Default styles are the FINAL state (no-JS, reduced motion); .stakes-armed /
   .is-playing (JS, motion only) hold the text back until the beat scrolls in,
   then flicker the eyebrow and bring the lines in once.
   ===================================================================== */
.stakes {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  background: var(--scrim);
  border-top: 1px solid var(--line);
}
.stakes__bg { position: absolute; inset: 0; z-index: -1; }
.stakes__bg img { position: absolute; inset: 0; width: 100%; height: 100%; max-width: none; object-fit: cover; }
/* Darkened gym, a touch lighter than V5 so the silhouette's outline reads against it. */
.stakes__gym { object-position: 18% 70%; filter: grayscale(1) brightness(0.6); }
/* Legibility scrim over the imagery (not chrome): darker toward the dialogue; the bottom band starts
   low enough that the silhouette's legs stay distinct from it. */
.stakes__scrim {
  position: absolute; inset: 0;
  background:
    linear-gradient(90deg, rgba(5,8,15,0.15) 0%, rgba(5,8,15,0.4) 42%, rgba(5,8,15,0.88) 70%),
    linear-gradient(180deg, rgba(5,8,15,0) 82%, rgba(10,16,32,0.85) 100%);
}
.stakes__inner {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  padding-top: 24px;
}
/* The beat's heading, set as the transmission eyebrow (overrides the display h2 scale). */
.stakes__eyebrow {
  font-family: var(--font-mono); font-weight: 400; font-size: 0.72rem; line-height: 1.5;
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--coral);
  font-variant-numeric: tabular-nums;
}
.stakes__foe { position: relative; display: flex; justify-content: flex-start; align-items: flex-start; }
/* Flipped to face his lines; nearly black so the outline and a hint of detail read, not the character.
   The faint blue rim (drop-shadow) is what keeps the outline legible over the dark gym. */
.stakes__colossus { transform: scaleX(-1); filter: brightness(0.13) contrast(1.2) drop-shadow(0 0 2px rgba(111, 169, 220, 0.45)); }
/* Figure trimmed to his bounds (the flipped art has 63 grid px of transparent margin on the left). */
.stakes .spr--colossus { margin: calc(-30px * var(--kc)) 0 calc(-4px * var(--kc)) calc(-62px * var(--kc)); }

/* Dialogue card modelled on the app's StoryView: name plate, glass bubbles (stacked), framed portrait. */
.foe__card { display: flex; align-items: flex-end; gap: 10px; }
.foe__talk { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; min-width: 0; }
.foe__name {
  font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--gold); padding-inline: 8px;
}
.foe__line {
  margin: 0;
  padding: 12px 16px;
  border-radius: var(--r-lg);
  border: 1px solid var(--line);
  background: rgba(19, 27, 47, 0.82); /* surface-overlay, glass */
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  color: var(--text-primary);
  font-weight: 600;
  font-size: 1rem;
  line-height: 1.45;
}
.foe__portrait {
  flex: 0 0 auto;
  width: 72px; height: 72px; padding: 4px;
  border-radius: var(--r-lg);
  background: rgba(5, 8, 15, 0.55); /* scrim frame */
  box-shadow: inset 0 0 0 1px var(--line);
  overflow: hidden;
}
/* Native 1x, cropped to the head (no scaling, no mirroring). */
.foe__portrait img { width: 160px; height: 160px; max-width: none; margin: -26px 0 0 -42px; }

/* Phones / tablets: Colossus x2 on the left, his lines to the right of his head. */
.stakes .foe__card { position: absolute; right: 0; top: 64px; max-width: 250px; }
.stakes .foe__talk { flex: 1 1 auto; }
.stakes .foe__line { padding: 10px 12px; font-size: 0.95rem; }
.stakes .foe__portrait { width: 56px; height: 56px; padding: 3px; }
.stakes .foe__portrait img { margin: -30px 0 0 -48px; }
/* Desktop: eyebrow across the top, the silhouette in the left column, the lines in the right column beside him. */
@media (min-width: 900px) {
  .stakes__inner { grid-template-columns: auto minmax(0, 1fr); column-gap: 40px; align-items: start; padding-top: 32px; }
  .stakes__eyebrow { grid-column: 1 / -1; }
  .stakes__foe { grid-column: 1; grid-row: 2; }
  .stakes .foe__card { position: static; grid-column: 2; grid-row: 2; justify-self: start; margin-top: 64px; max-width: 460px; }
  .stakes .foe__line { padding: 12px 16px; font-size: 1.1rem; }
  .stakes .foe__portrait { width: 72px; height: 72px; padding: 4px; }
  .stakes .foe__portrait img { margin: -26px 0 0 -42px; }
}
/* Desktop (V7): a noticeably bigger dialogue card and bubbles beside the x3 silhouette. */
@media (min-width: 1100px) {
  .stakes .spr--colossus { --kc: 3; margin: calc(-34px * var(--kc)) 0 calc(-5px * var(--kc)) calc(-62px * var(--kc)); }
  .stakes .foe__card { margin-top: 96px; max-width: 560px; gap: 14px; }
  .stakes .foe__talk { gap: 8px; }
  .stakes .foe__name { font-size: 0.72rem; }
  .stakes .foe__line { padding: 16px 22px; font-size: 1.3rem; line-height: 1.4; }
}
/* V10: the red signal panel top-right (desktop/tablet), the section at the mockup's height (662px at 1440). */
.stakes__panel { top: 40px; right: max(var(--gutter), calc((100% - var(--maxw)) / 2)); }
@media (min-width: 1100px) { .stakes { min-height: 662px; } }
@media (max-width: 1099px) { .stakes__panel { width: 240px; top: 36px; } }

/* Opening state (armed by JS, motion only) and the one-shot reveal. */
.stakes-armed .stakes:not(.is-playing) .stakes__eyebrow,
.stakes-armed .stakes:not(.is-playing) .foe__card { opacity: 0; }
.stakes.is-playing .stakes__foe { animation: glitch 0.42s steps(1) 0.2s 1; }
.stakes.is-playing .stakes__bg { animation: flicker 0.42s steps(1) 0.2s 1; }
.stakes.is-playing .stakes__eyebrow { animation: tx-flicker 0.5s steps(1) 0.2s both; }
.stakes.is-playing .foe__card { animation: tx-in 0.6s ease 0.9s both; }
.stakes.is-playing .foe__line:nth-of-type(2) { animation: tx-in 0.6s ease 1.9s both; }
@keyframes tx-in { from { opacity: 0; transform: translateY(8px); } }
@keyframes tx-flicker { 0% { opacity: 0; } 20% { opacity: 1; } 40% { opacity: 0.2; } 60% { opacity: 1; } 80% { opacity: 0.4; } 100% { opacity: 1; } }
@keyframes glitch {
  0% { transform: translate(-6px, 0); clip-path: inset(0 0 55% 0); }
  25% { transform: translate(5px, 2px); clip-path: inset(30% 0 20% 0); }
  50% { transform: translate(-3px, -1px); clip-path: inset(60% 0 0 0); }
  75% { transform: translate(2px, 0); clip-path: none; }
  100% { transform: none; clip-path: none; }
}
@keyframes flicker { 0% { opacity: 0.55; } 30% { opacity: 1; } 55% { opacity: 0.7; } 100% { opacity: 1; } }

/* =====================================================================
   5 · DECISION — copy left ("ANSWER THE CALL.." + the city line), the form
   card right with the signal panel overlapping its top-right corner.
   ===================================================================== */
.decide {
  background: var(--scrim);
  border-top: 1px solid var(--line);
  padding-block: 61px 58px;
  overflow-x: clip; /* the off-stage signal panel (V11) must not widen the page */
}
.decide__grid {
  display: grid;
  gap: 32px;
  grid-template-columns: 1fr;
  align-items: center;
}
.decide__title {
  font-size: clamp(2.5rem, 5vw, 4.5rem);
  line-height: 1.05;
  letter-spacing: -0.05em;
}
.decide__sub {
  margin-top: 24px;
  max-width: 24em;
  font-weight: 700;
  font-size: clamp(0.9375rem, 1.3vw, 1.125rem);
  line-height: 1.5;
  text-transform: uppercase;
  color: var(--text-primary);
}
.decide__sub-a { color: var(--gold); }
.decide__note {
  margin-top: 14px;
  color: var(--text-muted);
  font-size: 0.8125rem;
  line-height: 1.5;
}
/* The card + panel block: the panel sits 70px over the card's right edge and 19px above its top; the block is
   625px wide at the mockup's scale, so the panel's right edge lands on the content column's edge. */
.decide__side { position: relative; }
.decide__panel { top: -19px; right: 0; }
.decide__side .card { position: relative; z-index: 1; width: 419px; max-width: 100%; }
@media (min-width: 1100px) {
  .decide__grid { grid-template-columns: minmax(0, 1fr) 625px; gap: 24px; }
  .decide__side { min-height: 364px; }
}
@media (min-width: 860px) and (max-width: 1099px) {
  .decide__grid { grid-template-columns: minmax(0, 1fr) 560px; }
  .decide__panel { width: 240px; }
}
@media (max-width: 859px) {
  .decide { padding-block: 48px 48px; }
  .decide__grid { gap: 32px; }
  .decide__side .card { width: 100%; }
  .decide__panel { position: static; width: 100%; margin-top: 16px; box-shadow: none; }
}
@media (max-width: 720px) {
  /* V11: the copy is centred on phones (desktop keeps the mockup's left column). */
  .decide__copy { text-align: center; }
  .decide__title { font-size: clamp(2.5rem, 12vw, 3.25rem); }
  .decide__sub { margin: 18px auto 0; font-size: 0.9375rem; }
}

.card {
  --btn-notch: var(--surface-raised);
  background: var(--surface-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: clamp(22px, 4vw, 30px);
}
.field { margin-bottom: 18px; }
.field label {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: 8px;
}
.field input[type="email"] {
  width: 100%;
  min-height: 54px;
  padding: 0 16px;
  border-radius: var(--r-md);
  background: var(--surface-overlay);
  border: 1px solid var(--line);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: 1rem;
}
.field input[type="email"]::placeholder { color: var(--text-muted); }
.field input[type="email"]:focus-visible {
  outline: none;
  border-color: var(--gold);
  box-shadow: 0 0 0 3px rgba(227,185,94,0.18);
}
.consent {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  margin-bottom: 20px;
  color: var(--text-secondary);
  font-size: 0.9rem;
  line-height: 1.5;
}
.consent input[type="checkbox"] {
  margin-top: 3px;
  width: 18px; height: 18px;
  accent-color: var(--gold);
  flex: 0 0 auto;
}
/* honeypot — visually and programmatically hidden from humans */
.hp {
  position: absolute !important;
  left: -9999px !important;
  width: 1px; height: 1px;
  overflow: hidden;
  opacity: 0;
}
.form-status {
  margin-top: 14px;
  min-height: 1.2em;
  font-size: 0.9rem;
  color: var(--text-secondary);
}
.form-status[data-state="error"] { color: var(--coral); }
.form-status[data-state="fail"] {
  color: var(--coral);
  font-family: var(--font-mono); font-size: 0.8rem; letter-spacing: 0.18em; text-transform: uppercase;
}
.form-status[data-state="ok"] { color: var(--green); }
.card .btn-primary { width: 100%; }
.card__chip { margin-bottom: 20px; }

/* =====================================================================
   CONFIRMATION
   ===================================================================== */
.confirm {
  background: var(--surface-base);
  border-top: 1px solid var(--line);
  text-align: center;
}
.confirm__inner { max-width: 48ch; margin: 0 auto; }
.confirm__glyph {
  width: 72px; height: 72px; margin: 0 auto 24px;
  border-radius: var(--r-pill);
  display: grid; place-items: center;
  background: var(--surface-overlay);
  box-shadow: inset 0 0 0 1px var(--line);
  color: var(--gold);
}
.confirm h2 { color: var(--gold); }
.confirm .sig {
  font-family: var(--font-mono);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-top: 0.9rem;
  font-size: 0.8rem;
}
.confirm p.whisper {
  margin-top: 1.6rem; color: var(--text-muted); font-style: italic;
}
.social-row {
  margin-top: 2rem;
  display: flex; gap: 12px; justify-content: center; flex-wrap: wrap;
}
.social-row a {
  padding: 10px 16px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  color: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.social-row a:hover { border-color: var(--gold); color: var(--gold); text-decoration: none; }

.card [data-view="confirm"] { text-align: center; padding-block: 8px; }
.card [data-view="confirm"]:focus { outline: none; }
.card .sig {
  font-family: var(--font-mono); letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--gold); font-size: 0.72rem;
}
.card .confirm__title { font-size: clamp(1.6rem, 5vw, 2.2rem); margin-top: 0.6rem; }
.card .confirm__body { color: var(--text-secondary); margin-top: 0.9rem; }

/* State toggling between registry form and confirmation */
[data-view="confirm"] { display: none; }
body.is-confirmed [data-view="registry"] { display: none; }
body.is-confirmed [data-view="confirm"] { display: block; }

/* =====================================================================
   Footer
   ===================================================================== */
.footer {
  background: var(--scrim);
  border-top: 1px solid var(--line);
  padding-block: 24px;
  color: var(--text-muted);
  font-size: 0.8rem;
  line-height: 1.6;
}
.footer__inner { display: flex; flex-wrap: wrap; gap: 12px 28px; justify-content: space-between; align-items: center; min-height: 35px; }
.footer a { color: var(--text-secondary); }
.footer .brandline {
  font-family: var(--font-mono);
  letter-spacing: 0.12em;
  text-transform: none;
  font-size: 0.68rem;
  color: var(--text-secondary);
}

/* =====================================================================
   Standalone confirm / unsubscribe landing states
   ===================================================================== */
.standalone {
  min-height: 100svh;
  min-height: 100vh;
  display: grid;
  place-items: center;
  text-align: center;
  padding: var(--gutter);
}
.standalone .confirm__inner { max-width: 44ch; }
/* Discord invite under "Uplink confirmed": a ghost (secondary) button with the white Discord mark; the gold fill stays
   the only filled control. Logo inline SVG (currentColor), so it follows the label colour on hover. */
.discord-cta { margin-top: 2rem; }
.discord-cta__lead { color: var(--text-secondary); font-size: 0.95rem; line-height: 1.6; }
.discord-cta .btn-discord { margin-top: 0.9rem; }
.btn-discord__label { display: inline-flex; align-items: center; gap: 12px; }
.btn__icon { display: block; flex: none; }

/* =====================================================================
   Privacy page
   ===================================================================== */
.doc { max-width: 72ch; margin: 0 auto; }
.doc h1 { font-size: clamp(2rem, 7vw, 3rem); margin-bottom: 0.4rem; letter-spacing: -0.03em; }
.doc h2 { font-size: 1.3rem; margin-top: 2.4rem; text-transform: none; letter-spacing: -0.01em; }
.doc p, .doc li { color: var(--text-secondary); margin-top: 0.9rem; }
.doc ul { padding-left: 1.2rem; }
.doc .updated { color: var(--text-muted); font-family: var(--font-mono); font-size: 0.78rem; letter-spacing: 0.1em; }
.doc a.back { display: inline-block; margin-top: 2.4rem; }

/* =====================================================================
   V14 · MOTION — Phase 1: load overlay + hero intro.
   Every effect plays once and settles in its final state, which is the DOM/CSS default above: reduced motion and
   no-JS never get these classes (the <head> script only sets them when motion is allowed), so they render the
   final state with no animation. Only transform / opacity are animated, plus clip-path (typing) and a stepped
   mask (the character's pixel dissolve). Easing and timing tokens are shared (:root, --ease-*).
   ===================================================================== */

/* ---- Load overlay (#uplink-intro). Timeline from first paint, all CSS: static + figure at once, the line types
   120–820ms, holds, then the CRT power-on exit: the screen collapses to a bright 2px line (1050–1200ms) and the
   shades open from that line onto the hero (1200–1450ms). motion.js dismisses it early on any input and hands over
   to the hero intro; if the script never runs, the overlay still ends (visibility) and lets the page through, and
   the <head> script's 4s failsafe clears it. */
.uplink-intro { display: none; }
html.motion-ok.intro-on .uplink-intro {
  display: block;
  position: fixed; inset: 0; z-index: 100;
  overflow: hidden;
  animation: uplink-gone 1ms linear 1450ms forwards;
}
.uplink-intro__shade { position: absolute; left: 0; right: 0; background: var(--surface-base); }
.uplink-intro__shade--top { top: 0; height: calc(50% + 1px); transform-origin: 50% 0; }   /* +1px: no seam on odd heights */
.uplink-intro__shade--bottom { bottom: 0; height: 50%; transform-origin: 50% 100%; }
.uplink-intro__screen { position: absolute; inset: 0; transform-origin: 50% 50%; }
/* TV static: one 64px noise tile (generated, seeded), faint, jumping by whole pixels in steps. */
.uplink-intro__screen::before {
  content: ""; position: absolute; inset: -64px;
  image-rendering: pixelated;
  opacity: 0.08;
}
/* .intro-ready arrives two frames after first paint (head script): the static and the figure, the only rasters of
   any size in the overlay, are never part of the first frame. */
html.intro-ready .uplink-intro__screen::before {
  background: url("../img/fx/noise-64.png") repeat; background-size: 64px 64px;
  animation: uplink-static 480ms steps(1, end) infinite;
}
/* The guide's silhouette behind the line: the game sprite (64x128 native) at an integer scale, near-black with a
   faint 1px cold rim (the Stakes treatment), low contrast on the navy. */
.uplink-intro__figure {
  --s: 4;
  position: absolute;
  width: calc(64px * var(--s)); height: calc(128px * var(--s));
  left: calc(50% - 32px * var(--s)); top: calc(50% - 67px * var(--s));
  left: round(calc(50% - 32px * var(--s)), 1px); top: round(calc(50% - 67px * var(--s)), 1px);
  image-rendering: pixelated;
  filter: brightness(0) drop-shadow(0 0 1px rgba(111, 169, 220, 0.5));
}
html.intro-ready .uplink-intro__figure { background: url("../img/intro/npc-guide-elder.png") no-repeat 0 0 / 100% 100%; }
@media (max-width: 720px) { .uplink-intro__figure { --s: 3; } }
@media (max-height: 560px) { .uplink-intro__figure { --s: 2; } }
.uplink-intro__text {
  position: absolute; left: 0; right: 0; top: 50%;
  margin-top: -0.75em;
  font-size: 0.8125rem; letter-spacing: 0.12em;
  text-align: center; white-space: nowrap;
  color: var(--text-secondary);
}
.uplink-intro__text span { display: inline-block; }
/* The bright line of the CRT exit (the only light element; off-white, as the screen's last light). */
.uplink-intro__line {
  position: absolute; left: 0; right: 0; top: calc(50% - 1px); height: 2px;
  background: var(--text-primary);
  box-shadow: 0 0 12px rgba(228, 233, 244, 0.55);
  opacity: 0;
}
html.intro-ready .uplink-intro__figure { animation: uplink-fade 300ms var(--ease-out) both; }
html.intro-on .uplink-intro__text span { animation: uplink-type 700ms steps(25, end) 120ms both; }
html.intro-on .uplink-intro__screen { animation: uplink-collapse 150ms var(--ease-in) 1050ms both; }
html.intro-on .uplink-intro__line { animation: uplink-line 400ms linear 1050ms both; }
html.intro-on .uplink-intro__shade { animation: uplink-open 250ms var(--ease-out) 1200ms both; }
@keyframes uplink-static {
  0% { transform: translate(0, 0); }
  25% { transform: translate(-23px, -41px); }
  50% { transform: translate(-47px, -9px); }
  75% { transform: translate(-11px, -53px); }
}
@keyframes uplink-fade { from { opacity: 0; } }
@keyframes uplink-type { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
@keyframes uplink-collapse { to { transform: scaleY(0.004); opacity: 0.6; } }
@keyframes uplink-line {
  0% { opacity: 0; transform: scaleX(0.4); }
  35% { opacity: 1; transform: scaleX(1); }
  75% { opacity: 1; transform: scaleY(1); }
  100% { opacity: 0; transform: scaleY(3); }
}
@keyframes uplink-open { to { transform: scaleY(0); } }
@keyframes uplink-gone { to { visibility: hidden; } }

/* ---- Hero intro. html.hero-intro (set before first paint) holds the copy, the character and the CTA at their
   start state until motion.js adds .hero-go (after the overlay, or at once on a return visit / early dismiss with
   .hero-quick). Full: ≤ 1.2s including the signal panel; quick: ≤ 700ms. If motion.js never starts it, the <head>
   script drops .hero-intro at 4s (the start state is plain opacity, no animation, so nothing is promoted to its own
   layer before the first paint). The panel's slide-in is released by motion.js (main.js holds it meanwhile). */
html.hero-intro:not(.hero-go) :is(.hero__line, .hero__sub, .hero__cta, .hero__beta, .hero__player) { opacity: 0; }
html.hero-go {
  --hi-rise: 380ms; --hi-l2: 80ms;
  --hi-flick: 460ms; --hi-flickd: 240ms;
  --hi-sub: 260ms; --hi-subd: 380ms;
  --hi-char: 200ms; --hi-chard: 480ms;
  --hi-cta: 520ms; --hi-ctad: 360ms;
}
html.hero-go.hero-quick {
  --hi-rise: 260ms; --hi-l2: 50ms;
  --hi-flick: 310ms; --hi-flickd: 160ms;
  --hi-sub: 150ms; --hi-subd: 260ms;
  --hi-char: 100ms; --hi-chard: 320ms;
  --hi-cta: 320ms; --hi-ctad: 240ms;
}
.hero__line { display: block; }
html.hero-go .hero__line { animation: hero-rise var(--hi-rise) var(--ease-out) both; }
html.hero-go .hero__line + .hero__line { animation-delay: var(--hi-l2); }
html.hero-go .hero__accent { animation: hero-flicker var(--hi-flickd) steps(1, end) var(--hi-flick) both; }
html.hero-go .hero__sub { animation: uplink-fade var(--hi-subd) var(--ease-out) var(--hi-sub) both; }
html.hero-go :is(.hero__cta, .hero__beta) { animation: hero-rise-sm var(--hi-ctad) var(--ease-out) var(--hi-cta) both; }
/* The character materialises: a stepped ordered-dither (4x4 Bayer) mask whose cells are 2x2 native pixels
   (--px is the sprite's scale), so the reveal stays on the art's own grid; the GIF keeps playing underneath. */
.hero__player { --px: 3px; }
@media (max-width: 1099px) { .hero__player { --px: 2px; } }
html.hero-go .hero__player {
  -webkit-mask-size: calc(var(--px) * 8) calc(var(--px) * 8); mask-size: calc(var(--px) * 8) calc(var(--px) * 8);
  animation: hero-dither var(--hi-chard) steps(1, end) var(--hi-char) both;
}
@keyframes hero-rise { from { opacity: 0; transform: translateY(12px); } }
@keyframes hero-rise-sm { from { opacity: 0; transform: translateY(8px); } }
@keyframes hero-flicker {
  0% { opacity: 1; }
  25% { opacity: 0.3; }
  50% { opacity: 1; }
  75% { opacity: 0.55; }
  100% { opacity: 1; }
}
@keyframes hero-dither {
  0% { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3C/svg%3E"); }
  12.5% { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3Cpath d='M0 0h1v1h-1zM2 2h1v1h-1z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3Cpath d='M0 0h1v1h-1zM2 2h1v1h-1z'/%3E%3C/svg%3E"); }
  25% { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3Cpath d='M0 0h1v1h-1zM2 0h1v1h-1zM0 2h1v1h-1zM2 2h1v1h-1z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3Cpath d='M0 0h1v1h-1zM2 0h1v1h-1zM0 2h1v1h-1zM2 2h1v1h-1z'/%3E%3C/svg%3E"); }
  37.5% { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3Cpath d='M0 0h1v1h-1zM2 0h1v1h-1zM1 1h1v1h-1zM0 2h1v1h-1zM2 2h1v1h-1zM3 3h1v1h-1z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3Cpath d='M0 0h1v1h-1zM2 0h1v1h-1zM1 1h1v1h-1zM0 2h1v1h-1zM2 2h1v1h-1zM3 3h1v1h-1z'/%3E%3C/svg%3E"); }
  50% { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3Cpath d='M0 0h1v1h-1zM2 0h1v1h-1zM1 1h1v1h-1zM3 1h1v1h-1zM0 2h1v1h-1zM2 2h1v1h-1zM1 3h1v1h-1zM3 3h1v1h-1z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3Cpath d='M0 0h1v1h-1zM2 0h1v1h-1zM1 1h1v1h-1zM3 1h1v1h-1zM0 2h1v1h-1zM2 2h1v1h-1zM1 3h1v1h-1zM3 3h1v1h-1z'/%3E%3C/svg%3E"); }
  62.5% { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3Cpath d='M0 0h1v1h-1zM1 0h1v1h-1zM2 0h1v1h-1zM1 1h1v1h-1zM3 1h1v1h-1zM0 2h1v1h-1zM2 2h1v1h-1zM3 2h1v1h-1zM1 3h1v1h-1zM3 3h1v1h-1z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3Cpath d='M0 0h1v1h-1zM1 0h1v1h-1zM2 0h1v1h-1zM1 1h1v1h-1zM3 1h1v1h-1zM0 2h1v1h-1zM2 2h1v1h-1zM3 2h1v1h-1zM1 3h1v1h-1zM3 3h1v1h-1z'/%3E%3C/svg%3E"); }
  75% { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3Cpath d='M0 0h1v1h-1zM1 0h1v1h-1zM2 0h1v1h-1zM3 0h1v1h-1zM1 1h1v1h-1zM3 1h1v1h-1zM0 2h1v1h-1zM1 2h1v1h-1zM2 2h1v1h-1zM3 2h1v1h-1zM1 3h1v1h-1zM3 3h1v1h-1z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3Cpath d='M0 0h1v1h-1zM1 0h1v1h-1zM2 0h1v1h-1zM3 0h1v1h-1zM1 1h1v1h-1zM3 1h1v1h-1zM0 2h1v1h-1zM1 2h1v1h-1zM2 2h1v1h-1zM3 2h1v1h-1zM1 3h1v1h-1zM3 3h1v1h-1z'/%3E%3C/svg%3E"); }
  87.5% { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3Cpath d='M0 0h1v1h-1zM1 0h1v1h-1zM2 0h1v1h-1zM3 0h1v1h-1zM0 1h1v1h-1zM1 1h1v1h-1zM3 1h1v1h-1zM0 2h1v1h-1zM1 2h1v1h-1zM2 2h1v1h-1zM3 2h1v1h-1zM1 3h1v1h-1zM2 3h1v1h-1zM3 3h1v1h-1z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' shape-rendering='crispEdges'%3E%3Cpath d='M0 0h1v1h-1zM1 0h1v1h-1zM2 0h1v1h-1zM3 0h1v1h-1zM0 1h1v1h-1zM1 1h1v1h-1zM3 1h1v1h-1zM0 2h1v1h-1zM1 2h1v1h-1zM2 2h1v1h-1zM3 2h1v1h-1zM1 3h1v1h-1zM2 3h1v1h-1zM3 3h1v1h-1z'/%3E%3C/svg%3E"); }
  100% { -webkit-mask-image: none; mask-image: none; }
}

/* ---- Optional pointer drift (site.config.js HERO_POINTER_DRIFT, off by default): motion.js writes whole-device-pixel
   offsets into these properties on desktop pointer devices; the individual translate property keeps the backdrop's
   scale and never touches the pixel art's size. */
.hero.is-drifting .hero__bg { translate: var(--drift-bx, 0) var(--drift-by, 0); }
.hero.is-drifting .hero__player { translate: var(--drift-px, 0) var(--drift-py, 0); }

/* =====================================================================
   V14 · MOTION — Phase 2: section 2 layered perspective scroll.
   As the section approaches, each layer rises into its composed place faster the nearer it is (sky 15, fog 30,
   garage 45, side buildings 60px at full size; halved on phones) while the side buildings part outward 16px (8px on
   phones): the camera pushing into the street. Everything lands exactly when the section is centred and holds
   there (the art has no spare below the stage, so no layer ever rises past its place: no gap can open). Layers only
   translate, by whole CSS pixels (steps); never scaled. Text and panel are static. The fog also drifts slowly
   (a 40s loop, below). CSS scroll-driven animations where supported (.par-css, decided in <head>); otherwise motion.js drives the
   same values while the section is in view. Reduced motion / no JS: the composition as drawn.
   ===================================================================== */
.world__stage > picture { display: contents; }
.world__sky { --ty: 15; }
.world__fog { --ty: 30; --fx: 12; }
.world__garage { --ty: 45; }
.world__bldg-left, .world__shop { --ty: 60; --tx: 16; }
.world__bldg-left { --dir: -1; }
.world__shop { --dir: 1; }
@media (max-width: 720px) {
  .world__sky { --ty: 8; }
  .world__fog { --ty: 15; --fx: 6; }
  .world__garage { --ty: 23; }
  .world__bldg-left, .world__shop { --ty: 30; --tx: 8; }
}
/* Fog drift: 0 → 12px left and back (6px on phones), 20s each way. Only the right edge can come into view, and a
   reflection fills it with the fog's own mirrored continuation, so no edge or seam ever shows; at rest the fog is
   as drawn. Engines without box-reflect keep the parallax but skip the drift. */
@supports (-webkit-box-reflect: right) {
  html.motion-ok .world__fog { -webkit-box-reflect: right; animation: world-fog 20s steps(var(--fx), end) infinite alternate; }
}
html.motion-ok.par-css .world { view-timeline: --world block; }
html.motion-ok.par-css .world__layer {
  animation: world-y linear both;
  animation-timing-function: steps(var(--ty), end);
  animation-timeline: --world;
  animation-range: cover 0% cover 50%;
}
html.motion-ok.par-css :is(.world__bldg-left, .world__shop) {
  animation: world-y linear both, world-x linear both;
  animation-timing-function: steps(var(--ty), end), steps(var(--tx), end);
  animation-timeline: --world, --world;
  animation-range: cover 0% cover 50%, cover 0% cover 50%;
}
@supports (-webkit-box-reflect: right) {
  html.motion-ok.par-css .world__fog {
    animation: world-y linear both, world-fog 20s linear infinite alternate;
    animation-timing-function: steps(var(--ty), end), steps(var(--fx), end);
    animation-timeline: --world, auto;
    animation-range: cover 0% cover 50%, normal;
  }
}
@keyframes world-y { from { transform: translateY(calc(var(--ty) * 1px)); } to { transform: none; } }
@keyframes world-x { from { translate: 0 0; } to { translate: calc(var(--tx) * var(--dir) * 1px) 0; } }
@keyframes world-fog { from { translate: 0 0; } to { translate: calc(var(--fx) * -1px) 0; } }

/* =====================================================================
   V14 · MOTION — Phase 3: section 3 cards. Each card plays when ~50% of it is in view, once per entry (motion.js
   arms a card with .is-armed only while it is fully off screen, and swaps in .is-playing at 50%), and always ends
   on the DOM/CSS default, the finished picture everyone else sees: full bar and LEVEL UP, all Echoes, gear and
   arrows in place, CENTER selected (fill: backwards, so once done every element is back on its own style).
   Opacity, transform (whole pixels via steps) and, where noted, clip-path and filter. Reduced motion / no JS: never
   armed, so the finished state as drawn.
   ===================================================================== */
/* Before state (armed). */
html.motion-ok .lpanel.is-armed .xcard__bar span { transform: scaleX(0.62); }
html.motion-ok .lpanel.is-armed .xcard__lbl-xp { opacity: 1; }
html.motion-ok .lpanel.is-armed :is(.xcard__lbl-up, .lpanel__stages .spr, .gear__item, .gear__tag, .xcard__sel) { opacity: 0; }
html.motion-ok .lpanel.is-armed .lpanel__arrow { clip-path: inset(0 100% 0 0); }

/* TRAIN: the bar fills 62% → 100%, flashes, "+ XP" crossfades to "LEVEL UP" with the ▲ hopping 6px, and the
   base Echo gives one gold pulse (filter: drop-shadow). The capture has a baked background, so the pulse is drawn
   on .lpanel__glow: an overlay at the base's exact box showing the same image through a silhouette mask
   (echo-ponytail-base-mask.png, see assets-src/glow/), visible only while the pulse runs. Its images (and the
   shimmer's mask below) are attached only while the card plays, so they are never requested early, competing
   with the first screen; by then the same files are already loaded by the card's own <img>s (the mask is 1.7 KB). */
.lpanel__glow {
  position: absolute; z-index: 2; right: 11.5%; top: 3.5%; height: 83%; aspect-ratio: 338 / 824;
  opacity: 0; pointer-events: none;
}
.lpanel__glow > span { position: absolute; inset: 0; }
html.motion-ok .lpanel.is-playing .lpanel__glow > span {
  background: url("/assets/img/echo-ponytail-base.png") 0 0 / 100% 100% no-repeat;
  -webkit-mask: url("/assets/img/echo-ponytail-base-mask.png") 0 0 / 100% 100% no-repeat;
  mask: url("/assets/img/echo-ponytail-base-mask.png") 0 0 / 100% 100% no-repeat;
}
html.motion-ok .lpanel.is-playing .xcard__bar span { animation: p3-fill 900ms var(--ease-out) backwards; }
.xcard__bar span::after { content: ""; position: absolute; inset: 0; background: var(--gold-pale); opacity: 0; }
html.motion-ok .lpanel.is-playing .xcard__bar span::after { animation: p3-flash 400ms ease-out 900ms; }
html.motion-ok .lpanel.is-playing .xcard__lbl-xp { animation: p3-out var(--dur-quick) ease-out 900ms backwards; }
html.motion-ok .lpanel.is-playing .xcard__lbl-up { animation: p3-in var(--dur-quick) ease-out 900ms backwards; }
html.motion-ok .lpanel.is-playing .xcard__tri { animation: p3-hop 400ms steps(1, end) 900ms; }
html.motion-ok .lpanel.is-playing .lpanel__glow { animation: p3-glow var(--dur-pulse) ease-in-out 900ms; }

/* ASCEND: the three Echoes rise 8px into place left → right, 150ms apart; each arrow draws in after its Echo. */
html.motion-ok .lpanel.is-playing .lpanel__stages .spr { --rise: 8; animation: p3-rise 400ms steps(8, end) backwards, p3-in 400ms var(--ease-out) backwards; }
html.motion-ok .lpanel.is-playing .lpanel__stages .lpanel__ii { animation-delay: 150ms; }
html.motion-ok .lpanel.is-playing .lpanel__stages .lpanel__iii { animation-delay: 300ms; }
html.motion-ok .lpanel.is-playing .lpanel__arrow { animation: p3-draw 240ms var(--ease-out) 120ms backwards; }
html.motion-ok .lpanel.is-playing .lpanel__arrow:nth-of-type(2) { animation-delay: 270ms; }

/* COLLECT GEAR: items and their tags pop in 60ms apart (6px rise), left to right; then one shimmer across the
   legendary vest (a light band moved by transform, masked to the vest's own shape). */
html.motion-ok .lpanel.is-playing :is(.gear__item, .gear__tag) {
  --rise: 6; animation: p3-rise var(--dur-quick) steps(6, end) backwards, p3-in var(--dur-quick) var(--ease-out) backwards;
}
html.motion-ok .lpanel.is-playing .gear__tag--hoodie { animation-delay: 60ms; }
html.motion-ok .lpanel.is-playing .gear__vest { animation-delay: 120ms; }
html.motion-ok .lpanel.is-playing .gear__tag--legend { animation-delay: 180ms; }
html.motion-ok .lpanel.is-playing .gear__cap { animation-delay: 240ms; }
html.motion-ok .lpanel.is-playing .gear__tag--uncommon { animation-delay: 300ms; }
html.motion-ok .lpanel.is-playing .gear__shades { animation-delay: 360ms; }
html.motion-ok .lpanel.is-playing .gear__tag--shades { animation-delay: 420ms; }
html.motion-ok .lpanel.is-playing .gear__pants { animation-delay: 480ms; }
html.motion-ok .lpanel.is-playing .gear__tag--rare { animation-delay: 540ms; }
.gear__shine { position: absolute; left: 58%; top: -10px; width: 50px; height: 50px; overflow: hidden; pointer-events: none; }
html.motion-ok .lpanel.is-playing .gear__shine {
  -webkit-mask: url("/assets/img/gear/vest_colossus.png") 0 0 / 100% 100% no-repeat;
  mask: url("/assets/img/gear/vest_colossus.png") 0 0 / 100% 100% no-repeat;
}
.gear__shine::before {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, 0.6) 50%, transparent 70%);
  transform: translateX(-100%); opacity: 0;
}
html.motion-ok .lpanel.is-playing .gear__shine::before { animation: p3-shine 450ms ease-in-out 700ms; }

/* CHALLENGE: the selection steps ATTACK → GUARD → ANALYZE (180ms each, lit then back) → CENTER, which stays. */
html.motion-ok .lpanel.is-playing .xcard__sel { animation: p3-blink var(--dur-step) linear backwards; }
html.motion-ok .lpanel.is-playing .xcard__sel--guard { animation-delay: 180ms; }
html.motion-ok .lpanel.is-playing .xcard__sel--analyze { animation-delay: 360ms; }
html.motion-ok .lpanel.is-playing .xcard__sel--center { animation: p3-in 90ms linear 540ms backwards; }

@keyframes p3-fill { from { transform: scaleX(0.62); } to { transform: none; } }
@keyframes p3-flash { 30% { opacity: 1; } }
@keyframes p3-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes p3-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes p3-rise { from { transform: translateY(calc(var(--rise) * 1px)); } to { transform: none; } }
@keyframes p3-hop { 0% { transform: translateY(-2px); } 15% { transform: translateY(-4px); } 30% { transform: translateY(-6px); } 70% { transform: translateY(-3px); } 85% { transform: translateY(-1px); } 100% { transform: none; } }
@keyframes p3-glow { 40% { opacity: 1; filter: drop-shadow(0 0 10px var(--gold)); } }
@keyframes p3-draw { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
@keyframes p3-shine { from { opacity: 1; transform: translateX(-100%); } to { opacity: 1; transform: translateX(100%); } }
@keyframes p3-blink { 0% { opacity: 0; } 20%, 80% { opacity: 1; } 100% { opacity: 0; } }

/* =====================================================================
   V14 · MOTION — Phase 4.
   Section 4: with motion the two lines type out (motion.js, 28 chars/s) as the dialogue card fades in; the untyped
   rest is laid out but hidden, so each bubble has its final size from the start. Bubble 2 now enters when
   line 1 ends (its own line types 300ms later) instead of at a fixed 1.9s. Screen readers get the full lines from the
   .sr-only copies; the typed copies are aria-hidden. Reduced motion / no JS: plain text, as before.
   Section 5: a real signup success (sent, or already confirmed; main.js) sets .is-received on the section's panel:
   its line reads "SIGNAL RECEIVED." for everyone and, with motion, the five signal bars fill gold one after another
   (a gold layer per bar fading in, a beat apart). Error states change nothing.
   ===================================================================== */
.foe__rest { visibility: hidden; }
html.motion-ok.type-armed .stakes.is-playing .foe__line:nth-of-type(2) { animation: none; opacity: 0; }
html.motion-ok.type-armed .stakes.is-playing .foe__line:nth-of-type(2).is-on { animation: tx-in var(--dur-pulse) ease both; opacity: 1; }

.sigpanel .signal i { position: relative; }
.sigpanel .signal i::after { content: ""; position: absolute; inset: 0; background: var(--gold); opacity: 0; }
.sigpanel.is-received .signal i,
.motion-ok .sigpanel.is-received .signal i { animation: none; opacity: 1; }
.sigpanel.is-received .signal i::after { opacity: 1; }
.motion-ok .sigpanel.is-received .signal i::after { animation: p3-in var(--dur-step) var(--ease-out) backwards; }
.motion-ok .sigpanel.is-received .signal i:nth-child(2)::after { animation-delay: calc(var(--dur-step) * 1); }
.motion-ok .sigpanel.is-received .signal i:nth-child(3)::after { animation-delay: calc(var(--dur-step) * 2); }
.motion-ok .sigpanel.is-received .signal i:nth-child(4)::after { animation-delay: calc(var(--dur-step) * 3); }
.motion-ok .sigpanel.is-received .signal i:nth-child(5)::after { animation-delay: calc(var(--dur-step) * 4); }

/* =====================================================================
   Reduced-motion: everything static. (Animations are opt-in via .motion-ok
   which JS only adds when motion is allowed, so this is a belt-and-braces.)
   ===================================================================== */
@media (prefers-reduced-motion: reduce) {
  .hero__bg { animation: none !important; }
  .sigpanel .bar i, .sigpanel .signal i { animation: none !important; }
  .stakes *, .stakes__bg { animation: none !important; }
  .hero__scan { opacity: 0 !important; animation: none !important; }
  .chip .dot { animation: none !important; }
  .btn-primary:hover { transform: none !important; }
}
