/* ═══════════════════════════════════════════════════════════════════
   BETA INVITE ROOM — "The Recall" + the acrostic plaque
   ═══════════════════════════════════════════════════════════════════

   Surface: marketing-site/beta/ — the texted invite link. Dark by
   design: nothing on the public site links here.

   Its own little world — this surface does not use the app's themes —
   but the house rules still bind: brand fonts, no emoji, no badges or
   counts, 16px minimum on inputs, 12px minimum anywhere, rem for type,
   motion elegant and snappy but never bouncy.

   Strictly 2D. No perspective, no transforms pretending at depth. The
   spotlight is a radial gradient; the séance stage is flat void; the
   squiggles — rendered by the app's real helpers — are the design.

   Layout notes that matter:
   - The page scrolls normally on every beat. An earlier version locked
     scrolling on the puzzle beats because they were "composed to fit" —
     they are not, in landscape or at large text sizes, and locking it put
     the answer field and the door out of reach with no way to get to them.
   - Squiggle SVGs arrive with intrinsic width/height from the renderer;
     `.seance-piece svg` overrides both so they scale to their piece.
   ═══════════════════════════════════════════════════════════════════ */

:root {
  --void: #07080a;
  --edge-strong: rgba(233, 227, 214, 0.18);
  --ink: #e9e3d6;
  --ink-muted: #8a8578;
  --warm: #ffd8a0;
  --signal: #3aa6fe;
  --mustang: #2563eb;

  --bronze-dark: #4a3418;
  --bronze: #8a6a35;
  --bronze-light: #c9a45e;
  --tarnish: #2b2a24;

  --font-display: 'Roboto Slab', Georgia, serif;
  --font-label: 'Roboto Mono', ui-monospace, monospace;
  --font-body: 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
  --font-logo-cortex: 'Bitcount Single Ink', 'Roboto Mono', monospace;
  --font-logo-buddy: 'Beth Ellen', cursive;

  /* Elegant, snappy. Never bouncy — no overshoot past 1 anywhere. */
  --ease: cubic-bezier(0.2, 0.7, 0.3, 1);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--void);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1rem;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior: none;
}

/* The room itself: one warm light from the viewer's right at ~45°,
   falling off into the void. Two layers — a broad wash and a tighter
   core — because a single gradient reads flat and cheap. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(120% 90% at 88% 6%, rgba(255, 216, 160, 0.13) 0%,
      rgba(255, 184, 119, 0.05) 38%, rgba(0, 0, 0, 0) 72%),
    radial-gradient(45% 40% at 92% 0%, rgba(255, 226, 183, 0.10) 0%,
      rgba(0, 0, 0, 0) 70%);
  z-index: 0;
}

.room {
  position: relative;
  z-index: 1;
  min-height: 100%;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: max(1.25rem, env(safe-area-inset-top)) 1.25rem
           max(1.25rem, env(safe-area-inset-bottom));
}

/* ── Beats ─────────────────────────────────────────────────────────
   One page-feel: beats swap in place, they never navigate. */
.beat { display: none; }
/* Focus moves to the beat when it appears so assistive tech has somewhere to
   land, but a container is not a control — no ring. */
.beat:focus { outline: none; }
.beat.is-active { display: block; animation: beat-in 0.5s var(--ease) both; }

@keyframes beat-in {
  from { opacity: 0; transform: translateY(0.5rem); }
  to   { opacity: 1; transform: translateY(0); }
}

.beat-inner {
  width: 100%;
  max-width: 46rem;
  margin: 0 auto;
}

/* ── Stage 1: the séance ───────────────────────────────────────────
   A black stage. Six squiggles appear one at a time, draw themselves in,
   drift, fade. Then all six come back at once in a rearranged layout,
   still drifting, and the solve is tap-in-order-of-first-appearance. No
   title, no instructions, no counter — the page never explains itself. */
.seance-stage {
  position: relative;
  width: 100%;
  height: min(70vh, 34rem);
  min-height: 19rem;
}

/* Absolutely positioned via inline left/top (JS-computed, re-randomized on
   every reveal — position carries no information the solve depends on).
   Centered on its left/top point via negative margins rather than a static
   `transform: translate(-50%,-50%)`, because `transform` is the ONE property
   the drift keyframes below own outright; a static base transform on the
   same property would fight the animated one instead of composing with it. */
.seance-piece {
  --draw-ms: 4000ms;
  --fade-ms: 1400ms;
  --stagger-ms: 90ms;
  position: absolute;
  left: 50%;
  top: 50%;
  width: 10rem;
  height: 6.3rem;
  margin-left: -5rem;
  margin-top: -3.15rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.4rem;
  background: none;
  border: none;
  border-radius: 0.75rem;
  appearance: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: opacity var(--fade-ms) var(--ease);
}
.seance-piece[hidden] { display: none; }
.seance-piece.is-fading { opacity: 0; }
.seance-piece:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; }

.seance-piece svg {
  display: block;
  width: 100%;
  height: auto;
  pointer-events: none;
  transition: filter 0.6s var(--ease);
}

/* The draw-in. Every stroke/dot the renderer emits already carries
   pathLength="1" and a `--i` stagger index (its own real animation
   plumbing) — this keyframe is the one thing scoped to `.seance-piece` so
   it can never collide with the reward's own infinite sq-draw loop. Starts
   fully undrawn; `.is-appearing` is what makes it play. */
.seance-piece .sq-anim-path,
.seance-piece .sq-anim-dot {
  opacity: 0;
}
.seance-piece.is-appearing .sq-anim-path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: seance-draw var(--draw-ms) cubic-bezier(.5, .1, .25, 1) forwards;
  /* --stagger-ms is set alongside --draw-ms (JS) and scales the same way a
     replay speeds up — without this, a fast replay's shrunk draw+hold
     window could end before a many-stroke piece's LAST stroke had even
     started its own unscaled 90ms-per-primitive delay. */
  animation-delay: calc(var(--i, 0) * var(--stagger-ms, 90ms));
}
.seance-piece.is-appearing .sq-anim-dot {
  animation: seance-dot-in var(--draw-ms) ease-out forwards;
  animation-delay: calc(var(--i, 0) * var(--stagger-ms, 90ms));
}
@keyframes seance-draw {
  0%   { stroke-dashoffset: 1; opacity: 0; }
  8%   { opacity: 1; }
  100% { stroke-dashoffset: 0; opacity: 1; }
}
@keyframes seance-dot-in {
  0%   { opacity: 0; transform: scale(0.5); }
  60%  { opacity: 1; transform: scale(1); }
  100% { opacity: 1; transform: scale(1); }
}

/* Correct tap: glow haze in the squiggle's OWN palette (--glow is set
   inline per-piece from the generator's brightest ink color), stops
   drifting, pinned. Freezing uses animationPlayState (set in JS), not a
   class here — pausing holds the CURRENT mid-arc position; swapping the
   animation off entirely would snap it back to a resting transform. */
.seance-piece.is-locked svg {
  /* Tightened per Kyle's second device pass: the wide 16/34px layers
     merged six strokes' halos into one box-ish cloud. Three TIGHT layers
     keep the energy on the line itself — each stroke carries its own
     halo — with the doubled 1.5px layer supplying the intensity. */
  filter:
    drop-shadow(0 0 1.5px var(--glow, rgba(255, 216, 160, 0.6)))
    drop-shadow(0 0 1.5px var(--glow, rgba(255, 216, 160, 0.6)))
    drop-shadow(0 0 4px var(--glow, rgba(255, 216, 160, 0.5)))
    drop-shadow(0 0 9px var(--glow, rgba(255, 216, 160, 0.35)));
}

/* Slow lazy arcs — CSS transforms only, no physics, no JS animation loop.
   Percent-based drift would be relative to the PIECE's own tiny box, not the
   stage, so these use rem deltas instead: enough travel to read as floating,
   never enough to leave the general vicinity. Four variants + a randomized
   negative delay (set in JS) keep six pieces from ever drifting in lockstep. */
@keyframes seance-drift-a {
  0%   { transform: translate(0, 0) rotate(0deg); }
  25%  { transform: translate(2.4rem, -2rem) rotate(2deg); }
  50%  { transform: translate(-1.6rem, -3.2rem) rotate(-1.5deg); }
  75%  { transform: translate(-2.8rem, 1.2rem) rotate(1deg); }
  100% { transform: translate(0, 0) rotate(0deg); }
}
@keyframes seance-drift-b {
  0%   { transform: translate(0, 0) rotate(0deg); }
  30%  { transform: translate(-2.6rem, 1.8rem) rotate(-2deg); }
  60%  { transform: translate(1.8rem, 3rem) rotate(1.5deg); }
  100% { transform: translate(0, 0) rotate(0deg); }
}
@keyframes seance-drift-c {
  0%   { transform: translate(0, 0) rotate(0deg); }
  33%  { transform: translate(2rem, 2.4rem) rotate(1deg); }
  66%  { transform: translate(-2.4rem, -1.4rem) rotate(-2deg); }
  100% { transform: translate(0, 0) rotate(0deg); }
}
@keyframes seance-drift-d {
  0%   { transform: translate(0, 0) rotate(0deg); }
  40%  { transform: translate(-1.8rem, -2.6rem) rotate(-1deg); }
  70%  { transform: translate(2.6rem, -0.8rem) rotate(2deg); }
  100% { transform: translate(0, 0) rotate(0deg); }
}

/* Wrong tap: the whole screen, not just the stage — fixed, above
   everything, independent of `.room`'s own stacking context. */
.blackout {
  position: fixed;
  inset: 0;
  background: var(--void);
  opacity: 0;
  pointer-events: none;
  transition: opacity 850ms ease;
  z-index: 500;
}
.blackout.is-shown { opacity: 1; pointer-events: auto; }

.door {
  display: block;
  margin: 2.5rem auto 0;
  padding: 0.9rem 2.5rem;
  background: transparent;
  /* The door is the first PRODUCT-colored thing in the room (Kyle,
     2026-07-30, rejecting the bronze pill): app-standard rectangle,
     Signal Blue — the same blue as "buddy" in the logo above it. The
     candlelit world ends at this button. */
  border: 1px solid var(--signal);
  border-radius: 0.5rem;
  color: var(--signal);
  font-family: var(--font-label);
  font-size: 0.8125rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  cursor: pointer;
  opacity: 0;
  animation: door-in 1.4s var(--ease) 0.5s forwards;
  transition: background 0.24s var(--ease), color 0.24s var(--ease);
}

/* `display:block` above would otherwise beat the [hidden] attribute and
   the door would sit there before it's been earned. */
.door[hidden] { display: none; }

.door:hover { background: rgba(58, 166, 254, 0.12); }
.door:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; }

@keyframes door-in { to { opacity: 1; } }

/* ── Stage 2: the plaque ───────────────────────────────────────────*/
.mark {
  text-align: center;
  margin-bottom: 1.75rem;
  font-size: 1.75rem;
  line-height: 1;
}
.mark .c { font-family: var(--font-logo-cortex); color: var(--mustang); }
.mark .b { font-family: var(--font-logo-buddy); color: var(--signal); }

.plaque-wrap {
  position: relative;
  max-width: 30rem;
  margin: 0 auto;
  border-radius: 0.375rem;
  overflow: hidden;
  touch-action: none;
  box-shadow: 0 1.25rem 3rem rgba(0, 0, 0, 0.6);
}

/* Aged bronze with a real patina: warm base, uneven wear, a cool
   verdigris drift in the low corners. Layered gradients, no images. */
.plaque {
  position: relative;
  padding: 2rem 1.75rem;
  background:
    radial-gradient(80% 60% at 78% 12%, rgba(255, 232, 190, 0.20) 0%,
      rgba(255, 232, 190, 0) 60%),
    radial-gradient(50% 50% at 12% 88%, rgba(96, 138, 112, 0.22) 0%,
      rgba(96, 138, 112, 0) 70%),
    linear-gradient(145deg, var(--bronze-light) 0%, var(--bronze) 42%,
      var(--bronze-dark) 100%);
  border: 1px solid rgba(255, 232, 190, 0.22);
  border-radius: 0.375rem;
}

/* Flowing paragraph, not a line-broken verse (queue item 131's poem fix):
   sentence-initials still spell the answer word for someone who re-splits
   it back into sentences, but nothing here visually aligns them — no bold,
   no enlarged initial, no hanging indent, no per-line block. It just reads
   as prose. */
.verse {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(0.9375rem, 3.6vw, 1.125rem);
  line-height: 1.85;
  color: #2a1e0c;
  /* Incised, lit from the upper right: the far wall of each stroke
     catches light, the near wall falls into shadow. */
  text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.42),
                1px  1px 0 rgba(255, 238, 205, 0.28);
}

/* The tarnish layer the finger rubs away. */
.tarnish {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  cursor: pointer;
  transition: opacity 0.9s var(--ease);
}
.tarnish.is-gone { opacity: 0; pointer-events: none; }

/* ── The acrostic answer ───────────────────────────────────────────
   Sits ABOVE the fold on purpose: the iOS keyboard must never cover
   the field the keyboard is for. */
.answer {
  max-width: 30rem;
  margin: 1.75rem auto 0;
  display: flex;
  gap: 0.5rem;
  opacity: 0;
  /* visibility, not just opacity: an invisible field must not be
     focusable or tappable before the plaque has been polished. */
  visibility: hidden;
  transition: opacity 0.8s var(--ease), visibility 0.8s var(--ease);
}
.answer.is-ready { opacity: 1; visibility: visible; }

.answer input {
  flex: 1;
  min-width: 0;
  padding: 0.8rem 1rem;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--edge-strong);
  border-radius: 0.5rem;
  color: var(--ink);
  font-family: var(--font-label);
  font-size: 1rem;                 /* 16px — never less, or iOS zooms */
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-align: center;
}
.answer input::placeholder { color: rgba(138, 133, 120, 0.5); letter-spacing: 0.1em; }
.answer input:focus { outline: none; border-color: var(--warm); }

.answer button {
  padding: 0.8rem 1.35rem;
  background: transparent;
  border: 1px solid var(--edge-strong);
  border-radius: 0.5rem;
  color: var(--ink-muted);
  font-family: var(--font-label);
  font-size: 0.8125rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 0.2s var(--ease), border-color 0.2s var(--ease);
}
.answer button:hover { color: var(--ink); border-color: var(--warm); }

/* ── The reward ────────────────────────────────────────────────────*/
.reward {
  max-width: 34rem;
  margin: 0 auto;
  opacity: 0;
  transform: scale(0.97);
  animation: reward-in 1.6s var(--ease) forwards;
}
@keyframes reward-in { to { opacity: 1; transform: scale(1); } }
.reward svg { width: 100%; height: auto; display: block; }

/* ── Driftwood dark (Kyle ruling, 2026-07-30): the puzzle beats keep the
   candlelit key aesthetic, but once you're through the door the form
   beats read as the PRODUCT — the app's dark mode: near-black, powder
   ink, Signal Blue as the one interactive accent. Everything in these
   beats rides the four vars below, so this override is the whole skin. */
.beat[data-beat='redeem'],
.beat[data-beat='done'] {
  /* MONOTONE + SIGNAL BLUE (Kyle round 3, final: the app's dark tokens are
     warm-tinted and read cream on this page — he wants grayscale: whites,
     grays, blacks, with Signal Blue as the only color, for interaction). */
  --ink: #ffffff;                           /* PURE white — zero warmth (Kyle round 4: the heading read yellow against the address bar) */
  --ink-muted: #a3a3a3;                     /* pure neutral gray */
  --warm: var(--signal);                    /* the accent IS Signal Blue */
  --edge-strong: rgba(255, 255, 255, 0.14); /* neutral border */
  /* THE LEAK the first three rounds missed: body sets `color: var(--ink)`
     at the ROOT, so anything that just INHERITS (the .lede heading) was
     still the room's cream — the scoped --ink only reaches elements that
     read the var themselves. Re-declaring color here makes inheritance
     resolve inside the beat. */
  color: var(--ink);
}
/* The one VISIBLE blue at rest (Kyle: the page read as all-gray because
   blue only lived in focus states and below the fold) — the mono-caps
   field labels carry it. */
.beat[data-beat='redeem'] .field-label,
.beat[data-beat='done'] .field-label {
  color: var(--signal);
}
.beat[data-beat='redeem'] .field input,
.beat[data-beat='done'] .field input {
  background: rgba(0, 0, 0, 0.4);           /* app --input-bg */
}
/* The room's warm candle-light wash reads as the puzzle's world; while a
   form beat is active the light cools to the app's own. :has() is fine on
   every iOS this beta targets; browsers without it just keep the warm wash. */
body:has(.beat[data-beat='redeem'].is-active)::before,
body:has(.beat[data-beat='done'].is-active)::before {
  /* The app's dark mode has NO colored corner light — a barely-there
     neutral gloss keeps the page from going flat, nothing more. */
  background:
    radial-gradient(120% 90% at 88% 6%, rgba(255, 255, 255, 0.05) 0%,
      rgba(255, 255, 255, 0.02) 38%, rgba(0, 0, 0, 0) 72%);
}

/* ── Beat 3: the invite ────────────────────────────────────────────*/
.beat[data-beat='redeem'] .beat-inner { max-width: 34rem; }

.lede {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(1.375rem, 5.5vw, 1.875rem);
  letter-spacing: 0.025em;
  margin: 0 0 0.75rem;
}

/* Primary instructional prose — 16px floor, body face. */
.sub { color: var(--ink-muted); font-size: 1rem; line-height: 1.6; margin: 0 0 1.5rem; }

.field { display: block; margin-bottom: 1.25rem; }
.field-label {
  display: block;
  font-family: var(--font-label);
  font-size: 0.75rem;              /* 12px floor */
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-bottom: 0.5rem;
}
.field input {
  width: 100%;
  padding: 0.85rem 1rem;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--edge-strong);
  border-radius: 0.5rem;
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1rem;                 /* 16px — iOS zoom guard */
}
.field input:focus { outline: none; border-color: var(--warm); }

/* First and last name sit side by side where there is room and stack where
   there is not — a 10rem basis wraps them on the narrowest phones rather
   than squeezing two 16px inputs into half a 320px screen. */
.name-row { display: flex; flex-wrap: wrap; gap: 0 0.75rem; }
.name-row .field { flex: 1 1 10rem; min-width: 0; }

/* Why the name is asked for at all, said once and quietly. */
.field-note {
  margin: -0.75rem 0 1.25rem;
  font-family: var(--font-body);
  font-size: 0.875rem;          /* 14px — above the 12px floor */
  line-height: 1.5;
  color: var(--ink-muted);
}

/* The agreement and its button, held back until there is a name and an
   address to attach the consent to. Author `display` would beat [hidden],
   the same cascade trap as .door and .done-link. */
.consent { animation: consent-in 0.28s var(--ease) both; }
.consent[hidden] { display: none; }

@keyframes consent-in {
  from { opacity: 0; transform: translateY(0.5rem); }
  to   { opacity: 1; transform: none; }
}

/* The clause, rendered verbatim from the confidentiality agreement's short
   form. Any drift between that source and this rendering is a bug. */
.clause {
  border-left: 2px solid rgba(255, 216, 160, 0.35);
  padding: 0.25rem 0 0.25rem 1rem;
  margin: 0 0 1.5rem;
  /* 16px floor: this is the agreement someone is about to be bound by,
     which makes it the most primary read on the page, not fine print. */
  font-size: 1rem;
  line-height: 1.65;
  color: #cdc7ba;
}
.clause strong { color: var(--ink); }
.clause ul { margin: 0.6rem 0; padding-left: 1.1rem; }
.clause li { margin: 0.4rem 0; }
.clause a { color: var(--warm); text-underline-offset: 3px; }

.agree {
  width: 100%;
  padding: 1rem 1.25rem;
  background: transparent;
  border: 1px solid var(--warm);
  border-radius: 0.5rem;
  color: var(--warm);
  font-family: var(--font-label);
  font-size: 0.875rem;
  letter-spacing: 0.1em;
  cursor: pointer;
  transition: background 0.24s var(--ease);
}
.agree:hover { background: rgba(255, 216, 160, 0.1); }
.agree:disabled { opacity: 0.45; cursor: default; }
.agree:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; }

/* Never pretend an acceptance was recorded. When the write fails this
   is what the tester sees — plain, honest, retryable. */
.retry {
  margin-top: 1rem;
  /* The one message on the page that must not be missed: it says the
     agreement did NOT save. Full prose size. */
  font-size: 1rem;
  color: #f0b48a;
  line-height: 1.5;
  display: none;
}
.retry.is-shown { display: block; }

.fineprint {
  margin-top: 1.5rem;
  /* Two sentences of prose: body face, not the label mono. */
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--ink-muted);
  font-family: var(--font-body);
}

/* ── Beat 4: done ──────────────────────────────────────────────────*/
.beat[data-beat='done'] .beat-inner { max-width: 30rem; text-align: center; }

.done-link {
  display: inline-block;
  margin-top: 1.75rem;
  padding: 0.9rem 2rem;
  border: 1px solid var(--warm);
  border-radius: 2rem;
  color: var(--warm);
  text-decoration: none;
  font-family: var(--font-label);
  font-size: 0.8125rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
/* Same cascade trap as .door: an author `display` beats [hidden]. Without
   this, every finished tester sees an "install" button that was meant to be
   invisible, pointing at a placeholder href. */
.done-link[hidden] { display: none; }

.done-link:hover { background: rgba(255, 216, 160, 0.1); }

/* Screen-reader-only. Pieces carry the same description a sighted
   player reads off the plate — form and palette. That's parity, not a
   hint: it says what the thing looks like, never where it goes. */
.sr {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ── Wider screens ─────────────────────────────────────────────────
   Mobile-first; the stage simply gets more room to work with. */
@media (min-width: 40rem) {
  .seance-piece {
    width: 8.5rem;
    height: 5.5rem;
    margin-left: -4.25rem;
    margin-top: -2.75rem;
    padding: 0.6rem;
  }
  .mark { font-size: 2.25rem; }
  .plaque { padding: 2.75rem 2.5rem; }
}

/* ── Mythic reward motion (G4 ruling: THE MYTHIC SHIPS) ────────────
   Copied from the app's squiggle animation CSS at the deploy sitting,
   with one substitution: the app hooks these off data-cx-* attributes,
   which the pieces sanitiser strips before anything reaches this page —
   so here they hook off .reward, the one element meant to move. The
   SVG carries its three neon colors as inline --c0..--c2 custom
   properties; the keyframes' fallback chain (--c3 falls back to --c1,
   etc.) is what makes a 3-color subset cycle cleanly through 5 stops.
   Cosmetic drift between the two copies is an accepted cost of the
   ruling. Opacity is locked with !important because important
   declarations outrank animations — without the lock, sq-draw's 0.4
   dips composite the neons against the page and invent colors that
   were never in the palette. */
.reward svg .sq-anim-path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  opacity: 1 !important;
  animation:
    sq-draw 10000ms cubic-bezier(.5, .1, .25, 1) infinite,
    cx-glitch-stroke 400ms step-end infinite,
    cx-glitch-jitter 300ms steps(4) infinite;
  animation-delay:
    calc(var(--i, 0) * 650ms),
    calc(var(--i, 0) * -73ms),
    calc(var(--i, 0) * -97ms);
  transform-box: fill-box;
  transform-origin: center;
}
.reward svg .sq-anim-path.sq-fill {
  stroke-width: 6;
  animation:
    sq-draw 10000ms cubic-bezier(.5, .1, .25, 1) infinite,
    cx-glitch-stroke 400ms step-end infinite,
    cx-glitch-fill 400ms step-end infinite,
    cx-glitch-jitter 300ms steps(4) infinite;
  animation-delay:
    calc(var(--i, 0) * 650ms),
    calc(var(--i, 0) * -73ms),
    calc(var(--i, 0) * -113ms),
    calc(var(--i, 0) * -97ms);
}
.reward svg .sq-anim-dot {
  opacity: 1 !important;
  animation:
    sq-dot-pulse 10000ms ease-in-out infinite,
    cx-glitch-fill 400ms step-end infinite,
    cx-glitch-jitter 300ms steps(4) infinite;
  animation-delay:
    calc(var(--i, 0) * 650ms),
    calc(var(--i, 0) * -73ms),
    calc(var(--i, 0) * -97ms);
  transform-box: fill-box;
  transform-origin: center;
}
@keyframes sq-draw {
  0%   { stroke-dashoffset: 1;  opacity: 0.4; }
  40%  { stroke-dashoffset: 0;  opacity: 1;   }
  60%  { stroke-dashoffset: 0;  opacity: 1;   }
  100% { stroke-dashoffset: -1; opacity: 0.4; }
}
@keyframes sq-dot-pulse {
  0%   { opacity: 0; transform: scale(0.4); }
  25%  { opacity: 1; transform: scale(1);   }
  60%  { opacity: 1; transform: scale(1);   }
  100% { opacity: 0; transform: scale(0.4); }
}
@keyframes cx-glitch-stroke {
  0%   { stroke: var(--c0); }
  20%  { stroke: var(--c1); }
  40%  { stroke: var(--c2, var(--c0)); }
  60%  { stroke: var(--c3, var(--c1)); }
  80%  { stroke: var(--c4, var(--c2, var(--c0))); }
  100% { stroke: var(--c0); }
}
@keyframes cx-glitch-fill {
  0%   { fill: var(--c0); }
  20%  { fill: var(--c1); }
  40%  { fill: var(--c2, var(--c0)); }
  60%  { fill: var(--c3, var(--c1)); }
  80%  { fill: var(--c4, var(--c2, var(--c0))); }
  100% { fill: var(--c0); }
}
@keyframes cx-glitch-jitter {
  0%   { translate:  0 0;   }
  25%  { translate:  1px -1px; }
  50%  { translate: -1px 1px;  }
  75%  { translate:  1px 1px;  }
  100% { translate:  0 0;   }
}

/* ── Reduced motion ────────────────────────────────────────────────
   Operability is untouched; only the motion goes. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
    transition-duration: 0.01ms !important;
  }
  /* The door and the reward are gated by markup, so forcing them
     visible is safe. `.answer` is NOT listed: it's gated by opacity
     alone, and revealing the field before the plaque is polished would
     hand reduced-motion users a different puzzle. */
  .door, .reward { opacity: 1; }
  /* G16, kept per the G4 ruling: the mythic rests as a fully-drawn
     static squiggle. First color of its palette; no flicker, no jitter. */
  .reward svg .sq-anim-path { animation: none; stroke-dashoffset: 0; stroke: var(--c0); }
  .reward svg .sq-anim-path.sq-fill { fill: var(--c0); }
  .reward svg .sq-anim-dot { animation: none; transform: none; fill: var(--c0); }
}
