/* ============================================================================
   PeakIRL.com
   ----------------------------------------------------------------------------
   The eight axis colours, the OVR bands and the tier colours are lifted
   verbatim from the app's own source (app/src/engine/axes.ts, ui/bands.ts,
   engine/tiers.ts). Those are the product's data language and the reason the
   octagon is recognisable, so they are never adjusted for taste.

   ── Separation from LiveLive (Scott's call, 2026-08-09) ─────────────────────
   The CHROME deliberately diverges from app/src/ui/theme.ts. The app's base is
   #06080F with a cyan accent, which collided with LiveLive: that site runs
   #07080A, cyan #2BE8D4 and magenta #FF3D8B, and its stated colour doctrine is
   "the outcome pair is cyan / magenta" on a neutral near-black. This page was
   reproducing exactly that: cyan pool top-right, magenta pool bottom-left, on
   neutral near-black. The two read as siblings.

   Two changes, chosen to keep what Scott liked and drop what was shared:
     1. The base moved from neutral near-black to a VIOLET-shifted black
        (#070512), so it is no longer the same neutral ground.
     2. The second light pool and the IRL panel tint moved from magenta to warm
        AMBER, which breaks the cyan/magenta pairing entirely and gives the page
        a cool-top, warm-bottom split LiveLive does not have.
   The cyan accent itself is unchanged. It was not the problem; the pairing was.

   The app's theme.ts is NOT updated to match. Scott has parallel sessions
   working in that repo, so the app keeps its own base for now and the two will
   be reconciled before launch. Until then this is a known, deliberate split.

   The app is dark because its two real usage moments are literally in the dark.
   The site inherits that rather than switching to a bright marketing skin,
   because a bright site into a dark app is a broken promise on first launch.

   Anti-references, from PRODUCT.md: Apple Fitness rings, Whoop's clinical
   charcoal, SaaS dashboard cards, wellness pastel. The lineage is the sports
   card and the MMO progression screen.
   ========================================================================== */

:root {
  /* Violet-shifted black, not the app's near-neutral #06080F.
     See the "separation from LiveLive" note in the header comment. */
  --bg:        #070512;
  --bg-raise:  #100C24;
  /* The glass and hairlines carry the same violet cast, so panels sit in the
     base rather than on top of it as neutral grey rectangles. */
  --glass:     rgba(205, 195, 255, 0.05);
  --line:      rgba(210, 200, 255, 0.11);
  --text:      #F2F0FA;
  /* 6.5:1 on --bg. Every piece of body copy uses this or --text; the faint grey
     from the app palette is ~3.3:1 and is reserved for hairlines and the
     chart's tick dots, never for a sentence. */
  --dim:       #948FAD;
  /* Kept. Scott likes this accent and it is the app's own. It is not what made
     the two sites look alike; the pairing around it was. */
  --accent:    #7DF9FF;
  /* Text sitting ON the accent. 15.2:1. */
  --on-accent: #04121a;
  /* The octagon's centre well. Reads as depth only if it is darker than --bg. */
  --sink:      #050310;
  --placeholder: #7D7897;      /* 4.8:1 */
  /* Cool at the top, warm at the bottom. The warm pool used to be magenta,
     which together with the cyan reproduced LiveLive's signature outcome pair. */
  --wash-1:    rgba(125, 249, 255, 0.05);
  --wash-2:    rgba(255, 178, 92, 0.042);
  --wash-feature: rgba(255, 178, 92, 0.10);
  --radius:    20px;

  --ax-movement:   #00E5FF;
  --ax-recovery:   #A855F7;
  --ax-strength:   #FF6B35;
  --ax-fuel:       #7CFF6B;
  --ax-mind:       #4D9FFF;
  --ax-work:       #FFCC33;
  --ax-social:     #FF4D8D;
  --ax-discipline: #E8ECF4;

  --shell: min(1180px, 100% - 3rem);
  --font: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Display",
          "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

/* The octagon carries two colours that must follow the base, or the vertex dots
   ring themselves in the wrong black and the centre well stops reading as depth.
   Both are attributes in the markup; these override them per theme. */
.octagon [data-dots] circle:first-child { fill: var(--bg); }
.octagon #sink stop { stop-color: var(--sink); }

/* Per-item accent tokens.
   ---------------------------------------------------------------------------
   These live here rather than in a `style="--c: ..."` attribute because the
   site ships a strict Content-Security-Policy with `style-src 'self'` and no
   'unsafe-inline'. That policy blocks inline style ATTRIBUTES, so the inline
   version of this failed silently: every --c resolved to empty, and the axis
   dots, band bars, IRL rules and tier chips all rendered colourless with no
   console error to explain it. Adding 'unsafe-inline' would have fixed the
   symptom by weakening the policy. This keeps the policy.

   (Custom properties set from JS via style.setProperty are CSSOM, not an
   attribute, so the band tinting on .card-shell is unaffected by the policy.) */
.ax-movement   { --c: var(--ax-movement); }
.ax-recovery   { --c: var(--ax-recovery); }
.ax-strength   { --c: var(--ax-strength); }
.ax-fuel       { --c: var(--ax-fuel); }
.ax-mind       { --c: var(--ax-mind); }
.ax-work       { --c: var(--ax-work); }
.ax-social     { --c: var(--ax-social); }
.ax-discipline { --c: var(--ax-discipline); }

/* The seven OVR bands, in ladder order (bands.ts). */
.bands li:nth-child(1) { --c: #8A94A8; }
.bands li:nth-child(2) { --c: #7CC6FF; }
.bands li:nth-child(3) { --c: #5FE3C0; }
.bands li:nth-child(4) { --c: #7CFF6B; }
.bands li:nth-child(5) { --c: #FFCC33; }
.bands li:nth-child(6) { --c: #FF9A3D; }
.bands li:nth-child(7) { --c: #FF5FA2; }

/* The six rank tiers, in ladder order. These are the `core` stop of each entry in
   TIER_PALETTE (engine/tiers.ts). Platinum, diamond and legendary were wrong in the
   first build: they had been eyeballed as #7DF9FF / #8FE3FF / #FF9A3D instead of read
   from source, which is exactly the drift done-criterion 3 exists to catch. */
.tiers span:nth-child(1) { --c: #C97B4A; }  /* bronze    */
.tiers span:nth-child(2) { --c: #B9C3D4; }  /* silver    */
.tiers span:nth-child(3) { --c: #FFC94A; }  /* gold      */
.tiers span:nth-child(4) { --c: #8FE3D8; }  /* platinum  */
.tiers span:nth-child(5) { --c: #6FD3FF; }  /* diamond   */
.tiers span:nth-child(6) { --c: #FF7A3D; }  /* legendary */

*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* Depth. Two very soft pools of colour so the page is not flat black, sized
   large enough that they read as light rather than as shapes. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  background:
    radial-gradient(1100px 700px at 78% -10%, var(--wash-1), transparent 62%),
    radial-gradient(900px 620px at 8% 12%, var(--wash-2), transparent 60%);
}

main, .nav, .foot { position: relative; z-index: 1; }

h1, h2, h3 { margin: 0; font-weight: 900; letter-spacing: -0.03em; line-height: 0.97; }
p { margin: 0 0 1.1rem; }
a { color: var(--accent); }

img, svg { max-width: 100%; }

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

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 50;
  background: var(--accent); color: var(--on-accent); padding: 0.7rem 1.1rem;
  font-weight: 800; border-radius: 0 0 12px 0;
}
.skip:focus { left: 0; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 6px;
}

/* ── Nav ─────────────────────────────────────────────────────────────────── */

.nav {
  width: var(--shell);
  margin: 0 auto;
  padding: 1.4rem 0;
  display: flex;
  align-items: center;
  gap: 2rem;
}

.wordmark {
  font-weight: 900;
  font-size: 1.15rem;
  letter-spacing: 0.02em;
  text-decoration: none;
  color: var(--text);
  white-space: nowrap;
}
.wordmark-peak { color: var(--text); }
.wordmark-irl {
  color: var(--accent);
  /* The name is the thesis, so the IRL half is the part that glows. */
  text-shadow: 0 0 22px color-mix(in srgb, var(--accent) 45%, transparent);
}
.wordmark-sm { font-size: 1rem; }

.nav-links { margin-left: auto; display: flex; gap: 1.75rem; }
.nav-links a {
  color: var(--dim);
  text-decoration: none;
  font-size: 0.92rem;
  font-weight: 600;
  transition: color 0.18s ease;
}
.nav-links a:hover { color: var(--text); }

@media (max-width: 860px) { .nav-links { display: none; } .nav { gap: 1rem; } }

/* ── Buttons ─────────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  background: var(--accent);
  color: var(--on-accent);
  border: 0;
  border-radius: 999px;
  padding: 0.95rem 1.7rem;
  font: inherit;
  font-weight: 800;
  letter-spacing: -0.01em;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 50%, transparent);
  transition: transform 0.16s ease, box-shadow 0.24s ease;
}
.btn:hover { transform: translateY(-1px); box-shadow: 0 10px 34px -10px color-mix(in srgb, var(--accent) 65%, transparent); }
.btn:active { transform: translateY(0); }
.btn[disabled] { opacity: 0.55; cursor: progress; transform: none; }

.btn-sm { padding: 0.62rem 1.15rem; font-size: 0.92rem; margin-left: auto; }
@media (min-width: 861px) { .btn-sm { margin-left: 0; } }

.ghost {
  background: transparent;
  border: 1px solid var(--line);
  color: var(--dim);
  border-radius: 999px;
  padding: 0.5rem 1.05rem;
  font: inherit;
  font-size: 0.84rem;
  font-weight: 600;
  cursor: pointer;
  transition: color 0.18s ease, border-color 0.18s ease;
}
.ghost:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.28); }

/* ── Type scale ──────────────────────────────────────────────────────────── */

/* Block, not flex. As a flex container every text node became its own item, so
   the gap opened a space before the full stop in "Peak IRL. Peak in real life." */
.eyebrow {
  font-size: 0.76rem;
  font-weight: 800;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--dim);
  margin: 0 0 1.1rem;
}
.eyebrow .hl { color: var(--accent); }
.eyebrow-hot { color: var(--ax-social); }

.pill {
  display: inline-block;
  margin-left: 0.7rem;
  border: 1px solid var(--line);
  background: var(--glass);
  color: var(--dim);
  border-radius: 999px;
  padding: 0.2rem 0.6rem;
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  vertical-align: 0.05em;
}

h1 { font-size: clamp(2.7rem, 6.4vw, 4.6rem); }
h1 em { font-style: normal; color: var(--accent); text-shadow: 0 0 40px color-mix(in srgb, var(--accent) 30%, transparent); }

h2 { font-size: clamp(1.9rem, 3.8vw, 3rem); margin-bottom: 1.2rem; }
h2 em { font-style: normal; color: var(--accent); }

h3 { font-size: 1.1rem; letter-spacing: -0.015em; }

.lede { font-size: clamp(1.05rem, 1.5vw, 1.22rem); color: var(--dim); max-width: 34rem; }
.sec-lede { color: var(--dim); max-width: 46rem; }
.micro { font-size: 0.86rem; color: var(--dim); max-width: 42rem; }

/* ── Hero ────────────────────────────────────────────────────────────────── */

/* The card is sized so the whole hero, headline through the state toggle, clears
   a 1280x800 viewport without scrolling. That is the first screen and it has to
   carry both the eight axes and the two-number model on its own. */
.hero {
  width: var(--shell);
  margin: clamp(0.75rem, 2.5vw, 1.4rem) auto clamp(4rem, 9vw, 7rem);
  display: grid;
  /* 440, not 470. The axis read-out panel added ~55px of card height and pushed
     the card 13px past an 800px fold; the octagon is square, so taking 30px off
     the width takes 30px off the height with it. Re-measure this if anything is
     ever added to the card again. */
  grid-template-columns: 1fr minmax(0, 430px);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
}
@media (max-width: 980px) {
  .hero { grid-template-columns: 1fr; }
  .hero-card { max-width: 440px; margin-inline: auto; width: 100%; }
}

.hero h1 { margin-bottom: 1.4rem; }

.card-shell {
  position: relative;
  border: 1px solid var(--line);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.012)),
    var(--bg-raise);
  border-radius: 28px;
  padding: 1.4rem 1.4rem 1.1rem;
  /* The glow is tinted by the current OVR band, set from JS, so the whole card
     changes temperature when form changes. Same idea as the app's frame. */
  box-shadow:
    /* Derived from the band hex, not a second copy of it.
       This used to read a `--band-glow` that JS set from a `glow` field
       duplicating bands.ts's rgba values at a higher alpha, which the Codex
       round 3 audit correctly called source drift: the same colour existed in
       two places and only one of them tracked the app. Now the hex is the
       single source and the site's alpha is stated once, here, where it is
       plainly chrome. A CSS box-shadow is not the app's React Native glow and
       needs more alpha to read at all, which is why it differs. */
    0 40px 90px -50px color-mix(in srgb, var(--band, #FFCC33) 42%, transparent),
    inset 0 1px 0 rgba(255, 255, 255, 0.06);
  transition: box-shadow 0.7s ease;
}

.card-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding: 0 0.4rem 0.2rem;
}

.ovr-block { display: flex; flex-direction: column; }
.ovr {
  font-size: 3.4rem;
  font-weight: 900;
  line-height: 0.85;
  letter-spacing: -0.045em;
  font-variant-numeric: tabular-nums;
  color: var(--band, #FFCC33);
  transition: color 0.7s ease;
}
.ovr-label {
  font-size: 0.66rem;
  font-weight: 800;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--dim);
  margin-top: 0.4rem;
}

.band-block { display: flex; flex-direction: column; align-items: flex-end; gap: 0.5rem; }
.band-pill {
  border: 1px solid var(--band, #FFCC33);
  color: var(--band, #FFCC33);
  border-radius: 999px;
  padding: 0.25rem 0.8rem;
  font-size: 0.74rem;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  transition: color 0.7s ease, border-color 0.7s ease;
}
.lvl {
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--dim);
  text-transform: uppercase;
}
.lvl b { color: var(--text); font-variant-numeric: tabular-nums; }
.lvl-title { color: var(--dim); }

.octagon { display: block; width: 100%; height: auto; }
.octagon .al {
  font-size: 11px; font-weight: 800; letter-spacing: 0.06em; fill: var(--dim);
}
.octagon .av { font-size: 18px; font-weight: 900; font-variant-numeric: tabular-nums; }

/* Dim every facet except the one being inspected. */
.octagon[data-focus] [data-facets] polygon,
.octagon[data-focus] [data-dots] g,
.octagon[data-focus] [data-labels] text { opacity: 0.22; transition: opacity 0.25s ease; }
.octagon[data-focus] .is-focus { opacity: 1 !important; }

/* ── The octagon's own hit targets ───────────────────────────────────────── */

.hit {
  fill: transparent;      /* transparent still receives pointer events; none does not */
  outline: none;          /* the ring is drawn below, on the shape, not by the UA */
}
/* Only once JS has wired them. A pointer cursor over a dead area with scripting
   off is a promise the page cannot keep. */
.hits[data-ready] .hit { cursor: pointer; }
/* Keyboard focus has to be visible on a transparent shape, so it draws itself.
   :focus-visible only, so a mouse click does not leave a ring behind. */
.hit:focus-visible {
  fill: color-mix(in srgb, var(--accent) 7%, transparent);
  stroke: var(--accent);
  stroke-width: 1.5;
  stroke-dasharray: 5 4;
}

/* ── Axis read-out ───────────────────────────────────────────────────────── */

/* Always present, never conditionally rendered. It carries a default hint so the
   card does not resize when an axis is picked, and so the affordance is legible
   before anyone has hovered anything. */
.axis-panel {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  /* Fixed, not min-height. The blurbs are one or two lines depending on the axis
     (Social is the long one), so a min-height would let the card grow and shrink
     as you move across the chart, which reads as the page twitching under the
     cursor. Sized for the two-line case. */
  height: 4.4rem;
  padding: 0.55rem 0.7rem;
  margin-top: 0.2rem;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--glass);
  transition: border-color 0.25s ease, background 0.25s ease;
}
.axis-panel[data-active] {
  border-color: color-mix(in srgb, var(--ac) 45%, transparent);
  background: color-mix(in srgb, var(--ac) 8%, var(--glass));
}

.ap-dot {
  flex: 0 0 auto;
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--dim);
  box-shadow: none;
  transition: background 0.25s ease, box-shadow 0.25s ease;
}
.axis-panel[data-active] .ap-dot { background: var(--ac); box-shadow: 0 0 12px var(--ac); }

.ap-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.ap-title {
  font-size: 0.7rem; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text);
}
.axis-panel[data-active] .ap-title { color: var(--ac); }
.ap-body { font-size: 0.76rem; line-height: 1.35; color: var(--dim); }

.ap-score {
  margin-left: auto;
  flex: 0 0 auto;
  font-size: 1.5rem; font-weight: 900; letter-spacing: -0.04em;
  font-variant-numeric: tabular-nums;
  color: var(--ac, var(--dim));
  min-width: 1.6em; text-align: right;
}

.card-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; padding: 0.5rem 0.4rem 0;
  border-top: 1px solid var(--line); margin-top: 0.5rem;
}
.card-note { margin: 0; font-size: 0.8rem; color: var(--dim); flex: 1 1 12rem; }

/* ── Signup ──────────────────────────────────────────────────────────────── */

.signup { display: flex; flex-wrap: wrap; gap: 0.6rem; margin: 0 0 0.9rem; max-width: 34rem; }
.signup input[type="email"] {
  flex: 1 1 15rem;
  min-width: 0;
  background: var(--glass);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.95rem 1.3rem;
  color: var(--text);
  font: inherit;
  transition: border-color 0.18s ease, background 0.18s ease;
}
.signup input[type="email"]::placeholder { color: var(--placeholder); }
.signup input[type="email"]:focus { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 6%, transparent); }

/* Honeypot. Off-screen rather than display:none, because some bots skip hidden
   inputs but happily fill anything still in the layout tree. */
.hp {
  position: absolute !important;
  left: -9999px !important;
  width: 1px; height: 1px; opacity: 0;
}

.signup-msg { flex: 1 0 100%; margin: 0.35rem 0 0; font-size: 0.9rem; min-height: 1.3rem; }
.signup-msg.ok { color: var(--ax-fuel); }
/* Errors are grey, not red. The app has no alarm red and neither does this. */
.signup-msg.bad { color: var(--text); }

.signup-big { max-width: 32rem; margin-inline: auto; }
.signup-big input[type="email"] { flex: 1 1 18rem; }

/* Once the field and button have wrapped onto separate lines, a half-width
   button reads as unfinished and is a smaller target than it needs to be. */
@media (max-width: 560px) {
  .signup input[type="email"], .signup .btn { flex: 1 1 100%; }
  .hero { gap: 1.5rem; }
}

/* ── Sections ────────────────────────────────────────────────────────────── */

.sec {
  width: var(--shell);
  margin: 0 auto clamp(4.5rem, 10vw, 8rem);
}
/* No max-width here. Constraining the section box centred it inside the page
   shell, so these sections sat indented while every other one started at the
   left edge, which read as a mistake rather than a rhythm. Line length is
   already held by .sec-lede; the ladders below want the full width. */

/* ── Axis grid ───────────────────────────────────────────────────────────── */

.axes {
  list-style: none;
  margin: 2.2rem 0 1.2rem;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(255px, 1fr));
  gap: 0.7rem;
}
.axes button {
  width: 100%;
  height: 100%;
  text-align: left;
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto;
  column-gap: 0.75rem;
  row-gap: 0.2rem;
  background: var(--glass);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 1rem 1.15rem;
  color: var(--text);
  font: inherit;
  cursor: pointer;
  transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease;
}
.axes button:hover,
.axes button[aria-pressed="true"] {
  border-color: color-mix(in srgb, var(--c) 55%, transparent);
  background: color-mix(in srgb, var(--c) 9%, var(--glass));
  transform: translateY(-2px);
}
.ax-dot {
  grid-row: 1 / span 2;
  align-self: center;
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--c);
  box-shadow: 0 0 14px var(--c);
}
.axes b { font-size: 0.98rem; font-weight: 800; letter-spacing: -0.01em; }
.axes span:not(.ax-dot) { color: var(--dim); font-size: 0.85rem; line-height: 1.45; }
.axes-note { margin-top: 1.4rem; }

/* ── Bands ───────────────────────────────────────────────────────────────── */

.bands {
  list-style: none; margin: 2rem 0 1.2rem; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); gap: 0.55rem;
}
.bands li {
  border: 1px solid var(--line);
  border-top: 3px solid var(--c);
  border-radius: 12px;
  padding: 0.85rem 0.9rem;
  background: linear-gradient(180deg, color-mix(in srgb, var(--c) 11%, transparent), transparent);
  display: flex; flex-direction: column; gap: 0.15rem;
}
.bands b { font-size: 0.9rem; font-weight: 800; }
.bands span { font-size: 0.74rem; color: var(--dim); font-variant-numeric: tabular-nums; }

/* ── IRL feature ─────────────────────────────────────────────────────────── */

.sec-feature {
  border: 1px solid var(--line);
  border-radius: 32px;
  padding: clamp(2rem, 5vw, 4rem);
  background:
    radial-gradient(900px 420px at 85% 0%, var(--wash-feature), transparent 65%),
    var(--glass);
}
.irl-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 0.9rem; margin: 2.4rem 0 1.8rem;
}
.irl-card {
  border: 1px solid var(--line);
  border-left: 3px solid var(--c);
  border-radius: 16px;
  padding: 1.25rem 1.35rem;
  background: rgba(6, 8, 15, 0.5);
}
.irl-k {
  font-size: 0.68rem; font-weight: 800; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--c);
}
.irl-q { font-size: 1.05rem; font-weight: 800; letter-spacing: -0.015em; margin: 0.55rem 0 0.5rem; }
.irl-a { color: var(--dim); font-size: 0.9rem; margin: 0; }

.disclosure {
  border-top: 1px solid var(--line);
  padding-top: 1.4rem;
  color: var(--dim);
  font-size: 0.92rem;
  max-width: 48rem;
  margin: 0;
}

/* ── Ship / roadmap split ────────────────────────────────────────────────── */

.ship {
  display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 2.4rem;
}
@media (max-width: 820px) { .ship { grid-template-columns: 1fr; } }

.ship-col { border: 1px solid var(--line); border-radius: var(--radius); padding: 1.5rem 1.6rem; }
.ship-now  { background: var(--glass); }
/* Visually distinct on purpose. What is not built yet must not look like what
   is, or the split is decoration rather than a disclosure. */
.ship-next {
  background: repeating-linear-gradient(
    -45deg, rgba(255, 255, 255, 0.022) 0 10px, transparent 10px 20px);
  border-style: dashed;
}
.ship-col h3 { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 1rem; }
.ship-col ul { margin: 0; padding-left: 1.1rem; color: var(--dim); }
.ship-col li { margin-bottom: 0.45rem; font-size: 0.94rem; }
.ship-col .micro { margin-top: 1.1rem; }
.tick { color: var(--ax-fuel); font-weight: 900; }
.soon { color: var(--dim); font-weight: 900; }

/* ── Rules ───────────────────────────────────────────────────────────────── */

.rules {
  list-style: none; margin: 2.4rem 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem;
}
.rules li {
  border: 1px solid var(--line);
  background: var(--glass);
  border-radius: var(--radius);
  padding: 1.6rem;
  position: relative;
  overflow: hidden;
}
.rule-n {
  display: block;
  font-size: 2.6rem; font-weight: 900; line-height: 1;
  letter-spacing: -0.05em;
  color: transparent;
  -webkit-text-stroke: 1px rgba(255, 255, 255, 0.18);
  margin-bottom: 0.7rem;
}
.rules h3 { margin-bottom: 0.6rem; }
.rules p { color: var(--dim); font-size: 0.93rem; margin: 0; }

/* ── Ranks ───────────────────────────────────────────────────────────────── */

.ranks {
  list-style: none; margin: 2rem 0 1.4rem; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.5rem;
  counter-reset: rank;
}
.ranks li {
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 0.75rem 0.9rem;
  display: flex; justify-content: space-between; align-items: baseline; gap: 0.6rem;
  background: var(--glass);
}
.ranks b { font-size: 0.9rem; font-weight: 800; }
.ranks span { font-size: 0.76rem; color: var(--dim); white-space: nowrap; }

.tiers { display: flex; flex-wrap: wrap; gap: 0.45rem; margin-bottom: 1.4rem; }
.tiers span {
  border: 1px solid color-mix(in srgb, var(--c) 45%, transparent);
  color: var(--c);
  background: color-mix(in srgb, var(--c) 8%, transparent);
  border-radius: 999px;
  padding: 0.3rem 0.85rem;
  font-size: 0.74rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase;
}

/* ── The moments: level-up and promotion takeovers ───────────────────────────
   Ported from the app's LevelUpBurst.tsx and PromotionScreen.tsx, including the
   reason they look nothing alike: gold/radial/fast for the routine event, and
   cold/rising/slow for the rare one, so a different temperature reads as a
   different KIND of event before a word is processed.

   THE RULE THESE OBEY, which is the app's own and is written into
   LevelUpBurst.tsx in as many words: a reveal must enhance an already-visible
   default. Nothing load-bearing starts at opacity 0. Each stage's resting state
   is a finished celebration, and the animations only move things into it. A
   stage that never animates (reduced motion, a tab that was never focused, JS
   blocked) shows the composed still, never an empty box.

   Consequently the decorative layers DO fade, because losing a mote costs
   nothing, while the number and the titles animate transform only. */

.moments {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.4rem;
  margin: 2.6rem 0 1rem;
}
@media (max-width: 900px) { .moments { grid-template-columns: 1fr; } }

.moment { margin: 0; }

.stage {
  position: relative;
  aspect-ratio: 400 / 520;
  border: 1px solid var(--line);
  border-radius: 24px;
  overflow: hidden;
  isolation: isolate;
  display: grid;
  place-items: center;
}
/* The app's own scrims. Near-black rather than the page base, because on the
   phone this is a full takeover over whatever was underneath. */
.stage-level { background: rgba(3, 5, 11, 0.96); }
.stage-promo { background: rgba(2, 4, 10, 0.96); }

/* Positioning only. This rule deliberately does NOT set a box: it used to carry
   `inset: 0; width: 100%; height: 100%`, and at (0,2,1) it out-specified every
   single-class layer rule below, so the beam and the chevron were stretched to
   the full frame instead of sitting where they belong. Each layer states its own
   box, and the full-bleed ones opt in explicitly. */
.stage > svg, .stage > .motes, .stage > .shards {
  position: absolute;
  pointer-events: none;
}

.rays, .promo-glow, .motes, .shards { inset: 0; width: 100%; height: 100%; }

/* ── Level up, gold ──────────────────────────────────────────────────────── */

.rays { opacity: 0.32; }
.rays-spin path { fill: url(#rayfade); transform-origin: 200px 229px; }
.rays-spin { transform-origin: 200px 229px; transform: rotate(38deg); }

.beam { left: 50%; width: 55%; transform: translateX(-50%); }

.ring {
  top: 52%; left: 50%;
  width: 80%; height: auto;
  transform: translate(-50%, -50%) scale(2.6);
  opacity: 0;                       /* rings are transient; at rest there are none */
}

.motes i, .shards i { position: absolute; display: block; opacity: 0; }

.motes i {
  left: 50%; top: 50%;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: #FFC94A;
}
.motes i:nth-child(3n+1) { background: #FFF3C4; }

/* ── Promotion, cold ─────────────────────────────────────────────────────── */

.promo-glow { opacity: 0.5; }

.chevron {
  top: 18%; left: 50%;
  width: 46%; height: auto;
  transform: translateX(-50%) translateY(-10px);
  opacity: 0.5;
}

/* Opacity stays 0 at rest. The shards are transient, like the level stage's rings:
   the resting still is the settled screen, not a screen with debris parked in it. */
.shards i {
  top: 50%;
  width: 5px; height: 15px;
  border-radius: 2px;
  background: #7DF9FF;
}

/* ── The payload, shared ─────────────────────────────────────────────────── */

.takeover {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 1.5rem 1.25rem;
  max-width: 100%;
}
.takeover p { color: var(--dim); font-size: 0.82rem; line-height: 1.5; margin: 0; max-width: 21rem; }
.takeover .serif { font-family: Georgia, "Times New Roman", serif; font-size: 0.86rem; }

.kicker {
  display: inline-block;          /* inline boxes ignore transform */
  font-size: 0.66rem;
  font-weight: 900;
  letter-spacing: 0.45em;
  text-indent: 0.45em;            /* letter-spacing pads the right; this recentres it */
}
.kicker-gold {
  color: #FFF3C4;
  background: rgba(255, 201, 74, 0.16);
  border-top: 1px solid #FFC94A;
  border-bottom: 1px solid #FFC94A;
  padding: 0.4rem 1.3rem;
  border-radius: 4px;
}
.kicker-cold { color: #7DF9FF; margin-bottom: 0.9rem; }

.big-level {
  display: block;
  font-size: clamp(4.5rem, 11vw, 7rem);
  font-weight: 900;
  line-height: 1;
  letter-spacing: -0.06em;
  color: #FFF3C4;
  text-shadow: 0 0 28px #FF8A1F;
  margin-top: 0.3rem;
}

.promo-title {
  font-size: clamp(1.8rem, 4vw, 2.6rem);
  font-weight: 900;
  letter-spacing: -0.02em;
  color: var(--text);
}

.hair { display: block; width: 120px; height: 1px; margin: 0.7rem 0 0.9rem; }
.hair-gold { background: rgba(255, 201, 74, 0.5); }
.hair-cold { width: 70px; background: rgba(125, 249, 255, 0.5); margin: 1.1rem 0; }

/* The card change. The promotion is an edit to the card, shown rather than told. */
.cardchange {
  margin-top: 1.4rem;
  align-self: stretch;
  border: 1px solid rgba(125, 249, 255, 0.22);
  background: rgba(125, 249, 255, 0.045);
  border-radius: 16px;
  padding: 0.9rem 1rem;
}
.cc-cap {
  display: block;
  font-size: 0.55rem; font-weight: 800; letter-spacing: 0.24em;
  color: var(--dim); margin-bottom: 0.75rem;
}
.cc-row { display: flex; align-items: center; justify-content: center; gap: 0.9rem; }
.cc-side { display: flex; flex-direction: column; gap: 0.15rem; min-width: 5.5rem; }
.cc-old { color: var(--dim); font-family: Georgia, serif; font-size: 0.82rem; opacity: 0.75; }
.cc-oldlvl { color: var(--dim); font-size: 0.7rem; font-weight: 800; opacity: 0.75; }
.cc-arrow { color: #7DF9FF; font-size: 1.1rem; font-weight: 900; }
.cc-new { color: var(--text); font-family: Georgia, serif; font-size: 0.94rem; }
.cc-newlvl { color: #7DF9FF; font-size: 0.76rem; font-weight: 900; }

.moment figcaption {
  color: var(--dim);
  font-size: 0.88rem;
  line-height: 1.55;
  margin: 1.1rem 0 0.9rem;
}
.moment figcaption b { color: var(--text); }
/* Hidden until JS wires it, for the same reason the hit wedges get their
   role and tabindex from JS: a control that cannot do the thing it names is
   worse than no control. Round 5 caught these sitting visible and inert with
   scripting disabled. */
.replay { display: none; }
.replay[data-ready] { display: inline-block; }

/* ── Keyframes ───────────────────────────────────────────────────────────── */

@keyframes m-rays   { from { opacity: 0;    transform: rotate(0deg)   scale(0.6); }
                      to   { opacity: 0.32; transform: rotate(38deg)  scale(1); } }
@keyframes m-beam   { from { opacity: 0; transform: translateX(-50%) scaleY(0.05) scaleX(0.4); }
                      to   { opacity: 1; transform: translateX(-50%) scaleY(1)    scaleX(1); } }
@keyframes m-ring   { from { opacity: 0.85; transform: translate(-50%, -50%) scale(0.15); }
                      to   { opacity: 0;    transform: translate(-50%, -50%) scale(2.6); } }
@keyframes m-glow   { from { opacity: 0.15; transform: scale(0.7); }
                      to   { opacity: 0.5;  transform: scale(1); } }
@keyframes m-chev   { from { opacity: 0;   transform: translateX(-50%) translateY(40px) scale(0.8); }
                      to   { opacity: 0.5; transform: translateX(-50%) translateY(-10px) scale(1); } }
/* Transform only. No opacity: this is the payload.
   The app starts this at scale 3.4, which works because its Modal is the whole
   phone screen and the overshoot is clipped by the device bezel. In a 400px-wide
   embedded stage 3.4 throws the copy far outside the frame and the mid-flight
   frames are unreadable, so this opens at 2.2. Same gesture, sized to the canvas. */
@keyframes m-pop    { from { transform: scale(2.2) translateY(14px); }
                      to   { transform: scale(1)   translateY(0); } }
@keyframes m-rise   { from { transform: translateY(22px); }
                      to   { transform: translateY(0); } }
@keyframes m-swap   { from { transform: scale(0.94); }
                      to   { transform: scale(1); } }

.stage.is-playing .rays     { animation: m-rays 2600ms ease-out both; }
.stage.is-playing .rays-spin{ animation: none; }
.stage.is-playing .beam     { animation: m-beam 620ms ease-out both; }
.stage.is-playing .ring     { animation: m-ring 1500ms ease-out both; }
.stage.is-playing .ring-2   { animation-delay: 260ms; }
.stage.is-playing .ring-3   { animation-delay: 520ms; }
.stage.is-playing .promo-glow { animation: m-glow 2400ms ease-out both; }
.stage.is-playing .chevron  { animation: m-chev 1600ms ease-out both; }

/* The overshoot lands on the banner and the number only, not the whole block.
   The app scales its entire centre view, which is fine on a phone where that
   view is narrow relative to the screen. In a 580px stage, scaling the
   supporting sentence too pushes it past both edges for most of the flight and
   the clipped mid-frames read as a layout bug. So the payload pops and the
   sentence rises, which is the same gesture without the casualty. */
.stage-level.is-playing .kicker-gold,
.stage-level.is-playing .big-level { animation: m-pop 900ms cubic-bezier(.2,1.5,.4,1) 180ms both; }
.stage-level.is-playing .hair-gold,
.stage-level.is-playing .takeover p { animation: m-rise 800ms cubic-bezier(.16,1,.3,1) 420ms both; }

.stage-promo.is-playing .takeover  { animation: m-rise 900ms cubic-bezier(.16,1,.3,1) 260ms both; }
.stage-promo.is-playing .cc-new-side { animation: m-swap 700ms cubic-bezier(.34,1.56,.64,1) 1100ms both; }

/* Motes and shards. Deterministic scatter via nth-child rather than a per-element
   style attribute, because the site's CSP blocks inline styles. */
@keyframes m-mote {
  from { opacity: 0.95; transform: translate(var(--dx0), 60px) scale(1); }
  to   { opacity: 0;    transform: translate(var(--dx),  var(--dy)) scale(0.3); }
}
.stage-level.is-playing .motes i { animation: m-mote var(--dur) ease-out var(--del) both; }

.motes i:nth-child(1)  { --dx:-64px; --dx0:-16px; --dy:-232px; --dur:1700ms; --del:120ms; }
.motes i:nth-child(2)  { --dx: 48px; --dx0: 12px; --dy:-186px; --dur:1960ms; --del:210ms; }
.motes i:nth-child(3)  { --dx:-30px; --dx0: -8px; --dy:-268px; --dur:2220ms; --del:300ms; }
.motes i:nth-child(4)  { --dx: 66px; --dx0: 16px; --dy:-204px; --dur:2480ms; --del:390ms; }
.motes i:nth-child(5)  { --dx:-52px; --dx0:-13px; --dy:-320px; --dur:2740ms; --del:480ms; }
.motes i:nth-child(6)  { --dx: 22px; --dx0:  5px; --dy:-240px; --dur:1700ms; --del:570ms; }
.motes i:nth-child(7)  { --dx:-70px; --dx0:-18px; --dy:-292px; --dur:1960ms; --del:660ms; }
.motes i:nth-child(8)  { --dx: 40px; --dx0: 10px; --dy:-220px; --dur:2220ms; --del:120ms; }
.motes i:nth-child(9)  { --dx:-18px; --dx0: -4px; --dy:-350px; --dur:2480ms; --del:240ms; }
.motes i:nth-child(10) { --dx: 58px; --dx0: 14px; --dy:-262px; --dur:2740ms; --del:360ms; }
.motes i:nth-child(11) { --dx:-42px; --dx0:-10px; --dy:-198px; --dur:1820ms; --del:450ms; }
.motes i:nth-child(12) { --dx: 34px; --dx0:  8px; --dy:-304px; --dur:2080ms; --del:540ms; }

/* One pass, not `infinite`.
   ---------------------------------------------------------------------------
   These looped originally, on the reasoning that the promotion screen is slow
   and a single pass would leave it static while you are still reading. Two
   audits later that reasoning is dead: round 1 caught the loop running for the
   life of the page, and the fix (dropping `is-playing` after a settle timer)
   then had round 2 catch it cutting shards off mid-flight, because the slowest
   shard runs 4280ms behind an 880ms delay and the timer fired at 2600ms.
   A single staggered pass still covers 5.2 seconds of continuous rise, which is
   longer than the rest of the stage takes, so nothing is actually lost. The
   settle timer in main.js is set from the 5160ms total below; if these
   durations change, that constant changes with them. */
@keyframes m-shard {
  from { opacity: 0.9; transform: translateY(115px); }
  to   { opacity: 0;   transform: translateY(-176px); }
}
.stage-promo.is-playing .shards i { animation: m-shard var(--dur) linear var(--del) both; }

.shards i:nth-child(1) { left:12%; --dur:2600ms; --del:0ms;   height:12px; }
.shards i:nth-child(2) { left:22%; --dur:3020ms; --del:110ms; height:18px; }
.shards i:nth-child(3) { left:33%; --dur:3440ms; --del:220ms; height:24px; }
.shards i:nth-child(4) { left:44%; --dur:3860ms; --del:330ms; height:14px; }
.shards i:nth-child(5) { left:55%; --dur:4280ms; --del:440ms; height:20px; }
.shards i:nth-child(6) { left:66%; --dur:2600ms; --del:550ms; height:26px; }
.shards i:nth-child(7) { left:77%; --dur:3020ms; --del:660ms; height:16px; }
.shards i:nth-child(8) { left:86%; --dur:3440ms; --del:770ms; height:22px; }
.shards i:nth-child(9) { left:94%; --dur:3860ms; --del:880ms; height:13px; }

/* ── The emblem ladder ───────────────────────────────────────────────────── */

.ranks-head { font-size: clamp(1.2rem, 2.2vw, 1.6rem); margin: 3.5rem 0 1rem; }

.ranks-ladder {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 0.6rem;
  margin: 2rem 0 1.2rem;
}
@media (max-width: 860px) { .ranks-ladder { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 420px) { .ranks-ladder { grid-template-columns: repeat(2, 1fr); } }

.rank {
  margin: 0;
  border: 1px solid var(--line);
  background: var(--glass);
  border-radius: 16px;
  padding: 0.9rem 0.5rem 0.85rem;
  text-align: center;
}
.emblem { width: 100%; height: auto; max-width: 108px; }
.rank figcaption { display: flex; flex-direction: column; gap: 0.1rem; margin-top: 0.4rem; }
.rank b { font-size: 0.82rem; font-weight: 800; }
.rank span { font-size: 0.66rem; color: var(--dim); line-height: 1.35; }

/* Sparkles start at gold. If every tier sparkled, sparkle would stop meaning
   anything and the day you finally hit gold would look like day one. */
@keyframes m-spark {
  0%, 100% { opacity: 0.15; transform: scale(0.6); }
  50%      { opacity: 1;    transform: scale(1); }
}
/* The twinkle only runs while the ladder is actually on screen.
   ---------------------------------------------------------------------------
   Found while verifying the Codex fix for the promotion shards, not by the
   audit itself: these are 30 `infinite` animations across the six emblems, and
   they were running for the life of the page whether or not anyone could see
   them. Same defect as the shards, different costume.

   Unlike a stage, a twinkle has no settled state to land on, so it is gated on
   visibility rather than bounded by a timer. `.in-view` is added by an
   observer; if that observer never fires the emblems simply do not sparkle,
   which loses decoration and no content. */
.spark { transform-box: fill-box; transform-origin: center; opacity: 0.5; }
.ranks-ladder.in-view .sp-0  { animation: m-spark 2600ms ease-in-out    0ms infinite; }
.ranks-ladder.in-view .sp-1  { animation: m-spark 2600ms ease-in-out  220ms infinite; }
.ranks-ladder.in-view .sp-2  { animation: m-spark 2600ms ease-in-out  440ms infinite; }
.ranks-ladder.in-view .sp-3  { animation: m-spark 2600ms ease-in-out  660ms infinite; }
.ranks-ladder.in-view .sp-4  { animation: m-spark 2600ms ease-in-out  880ms infinite; }
.ranks-ladder.in-view .sp-5  { animation: m-spark 2600ms ease-in-out 1100ms infinite; }
.ranks-ladder.in-view .sp-6  { animation: m-spark 2600ms ease-in-out 1320ms infinite; }
.ranks-ladder.in-view .sp-7  { animation: m-spark 2600ms ease-in-out 1540ms infinite; }
.ranks-ladder.in-view .sp-8  { animation: m-spark 2600ms ease-in-out 1760ms infinite; }
.ranks-ladder.in-view .sp-9  { animation: m-spark 2600ms ease-in-out 1980ms infinite; }
.ranks-ladder.in-view .sp-10 { animation: m-spark 2600ms ease-in-out 2200ms infinite; }
.ranks-ladder.in-view .sp-11 { animation: m-spark 2600ms ease-in-out 2420ms infinite; }

/* ── CTA ─────────────────────────────────────────────────────────────────── */

.sec-cta {
  border: 1px solid var(--line);
  border-radius: 32px;
  padding: clamp(2.5rem, 6vw, 5rem) clamp(1.5rem, 4vw, 3rem);
  text-align: center;
  background:
    radial-gradient(700px 340px at 50% 0%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 70%),
    var(--glass);
}
.cta-inner { max-width: 42rem; margin: 0 auto; }
.sec-cta .eyebrow { justify-content: center; }
.sec-cta .sec-lede { margin-inline: auto; margin-bottom: 2rem; }
.sec-cta .micro { margin-inline: auto; }

/* ── Long-form document pages (privacy) ──────────────────────────────────── */

.doc { margin-top: 2rem; }
.doc h1 { font-size: clamp(2rem, 4.5vw, 3rem); margin-bottom: 0.7rem; }
.doc h2 { font-size: 1.25rem; margin: 2.6rem 0 0.9rem; }
.doc p { color: var(--dim); max-width: 44rem; }
.doc .micro { margin-bottom: 2rem; }

/* ── Footer ──────────────────────────────────────────────────────────────── */

.foot {
  width: var(--shell);
  margin: 0 auto;
  padding: 2.2rem 0 3rem;
  border-top: 1px solid var(--line);
  display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap;
}
.foot nav { display: flex; gap: 1.25rem; }
.foot a { color: var(--dim); text-decoration: none; font-size: 0.88rem; }
.foot a:hover { color: var(--text); }
.foot-note { margin-left: auto; color: var(--dim); font-size: 0.82rem; }

/* ── Entrance ────────────────────────────────────────────────────────────── */

/* Pure CSS, hero only, and it always runs to completion. There is deliberately
   no scroll-triggered reveal anywhere on this page: every implementation of one
   hides content first and un-hides it on an event that is not guaranteed to
   arrive, which strands a section at opacity 0 with nothing coming to fix it.
   TRANSFORM ONLY. No opacity, anywhere in this keyframe.
   ---------------------------------------------------------------------------
   This used to fade from `opacity: 0`, and the comment here used to claim the
   worst case was "a skipped animation, never lost content". That was flatly
   wrong: `animation-fill-mode: both` applies the FROM state before the
   animation starts, so an animation that never runs leaves the headline and
   the card sitting at opacity 0. It is the same defect as the scroll reveal
   that was torn out of this build earlier, reintroduced by the person who tore
   it out. It was visible in screenshots the whole time and got rationalised
   away as "it self-heals when the tab is focused"; the Codex round 2 audit
   called it, correctly, as a violation of done criterion 18.
   The rise reads fine without the fade, and now nothing on this page hides
   content behind an animation. Do not add opacity back. */
@keyframes rise {
  from { transform: translateY(16px); }
  to   { transform: none; }
}

.hero-copy { animation: rise 0.7s cubic-bezier(0.22, 1, 0.36, 1) both; }
.hero-card { animation: rise 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.12s both; }

/* ── Reduced motion ──────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  /* Every stage still shows its finished still, because nothing on them was
     hidden behind an animation in the first place. The replay buttons go,
     though: a control that promises motion which will not happen is worse
     than no control. The looping sparkles and shards are stopped by the
     iteration-count override above.
     Both selectors, so this still wins over `.replay[data-ready]` on
     specificity rather than relying only on source order. */
  .replay, .replay[data-ready] { display: none; }
}
