/* vote.css — /p/vote (Temptation Chart)
   Spec: WEBSYS_MD/SETLIST_REQUIREMENTS.md §3 (cards), §6 (onboarding), §8 (layout/input).

   PHASE B: entry screen + onboarding only.

   Everything here follows the theme tokens, so light mode works for free. The
   GAME surface in phase C is the fixed-dark one, and it must then pin its own
   color/border/fill to literals — see CLAUDE.md's fixed-background rule and
   scripts/check-fixed-surface.mjs.

   Mobile is the primary target (§8.1). Desktop widens the same layout rather
   than getting a separate one. */

/* Page box. --navbar-total-h, never --navbar-h: .ppw-nav is position:sticky so
   under viewport-fit=cover it occupies the bar height PLUS the top inset, and a
   bare --navbar-h overflows the viewport by exactly that inset (invisible on
   every desktop browser). 100vh first as the fallback, then 100dvh: on iOS
   100vh means the LARGE viewport and never shrinks back, so the tail of the
   page would sit under Safari's floating bar permanently. */
#vote-root {
  /* relative: the entry backdrop and the ambient starfield are absolute children
     of this element, not of the viewport — a fixed one would be painted over by
     the opaque surface it belongs to (found the hard way, see mountStarfield). */
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: calc(100vh  - var(--navbar-total-h));
  min-height: calc(100dvh - var(--navbar-total-h));
}
#vote-root > .lg-footer-link { margin-top: auto; padding-bottom: calc(20px + var(--safe-bottom)); }

.vote-page {
  position: relative;
  z-index: 1;           /* above .vote-entrysky */
  /* A base ink for the surface. Without it a child with no colour rule of its own
     inherits the browser default — BLACK — onto a near-black sky, which is exactly
     what happened to the stats line the moment it stopped using its own spans.
     check-fixed-surface.mjs cannot see that: there is no offending declaration to
     find, only a missing one. */
  color: var(--on-dark-text);
  flex: 1;
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
  padding: 24px calc(16px + var(--safe-right)) 32px calc(16px + var(--safe-left));
  display: flex;
  flex-direction: column;
  gap: 22px;
}

/* ── Hero ────────────────────────────────────────────────────────────────── */
.vote-hero { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px; }

.vote-hero__badge {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 6px 14px;
  border-radius: var(--radius-pill);
  background: rgba(var(--brand-teal-rgb), 0.12);
  border: 1px solid rgba(var(--brand-teal-rgb), 0.35);
  color: var(--on-dark-text);   /* was PPW cyan #5FE3E1 (2026-10-22) */
  font: 700 11px/1 var(--font-latin);
  letter-spacing: 0.18em;
}
.vote-hero__badge-dot {
  width: 6px; height: 6px; border-radius: var(--radius-full, 999px);
  background: currentColor;
  box-shadow: 0 0 8px currentColor;
}

.vote-hero__title {
  margin: 4px 0 0;
  font: 800 27px/1.25 var(--font-thai);
  color: var(--white);
}
.vote-hero__sub {
  margin: 0;
  max-width: 30ch;
  font: 400 14px/1.65 var(--font-thai);
  color: var(--on-dark-muted);
}

/* One line of small chips, not three big tiles — see buildStatsLine(). */
.vote-stats { display: flex; justify-content: center; flex-wrap: wrap; gap: 8px; }
.vote-stat {
  padding: 5px 13px;
  border-radius: 999px;
  border: 1px solid rgba(var(--white-rgb), 0.14);
  background: rgba(var(--white-rgb), 0.05);
  color: var(--on-dark-text);
  font: 600 12px var(--font-thai);
}
/* --text-primary, not --accent-gold: measured 2.89:1 in light mode on this chip's
   own 5%% tint (24px/800 needs 3.0). There is no --accent-gold-text token the way
   there is --accent-teal-text, and inventing a darker gold hex here would create a
   colour with no light coverage anywhere else (CLAUDE.md). */
/* ── Reward preview — 49 CSS dots, zero assets (§9.4) ────────────────────── */
/* ── Entry backdrop: the star chart, live (2026-08-25) ───────────────────── */
/* ⚠️ THE ENTRY SCREEN IS A FIXED-DARK SURFACE NOW.
   It used to follow the theme, on the site's ordinary background. Putting the
   star chart behind it makes it dark in BOTH themes, which means every colour
   painted on top has to be a literal — a var(--text-primary) here would flip to
   near-black ink over a near-black sky in light mode and vanish. Same rule, and
   the same reason, as the game and result surfaces below. check-fixed-surface.mjs
   enforces it. */
.vote-entrysky {
  position: absolute; inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}
/* A veil over the middle of the field. At full brightness the chart is the
   subject — which is right on the result screen and wrong here, where it sits
   behind a headline, three numbers and a button. Darkening the centre keeps the
   stars visible at the edges and the words readable over them. */
.vote-entrysky::after {
  content: '';
  position: absolute; inset: 0;
  background:
    radial-gradient(ellipse 78% 46% at 50% 42%, rgba(5, 5, 5, 0.86), rgba(5, 5, 5, 0.32) 62%, transparent 80%),
    linear-gradient(rgba(5, 5, 5, 0.28), rgba(5, 5, 5, 0.28));
}
/* Two nested transforms are impossible on one element, so the parallax rides on
   `translate` (its own property) and the drift on the keyframes — the same split
   the result chart uses. */
/* Deliberately smaller and dimmer than the same stars on the result screen. There
   they ARE the content; here they are the room it is in. */
.vote-entrystar {
  position: absolute;
  width: calc(1.5px + var(--k) * 4.5px);
  height: calc(1.5px + var(--k) * 4.5px);
  margin: calc((1.5px + var(--k) * 4.5px) / -2) 0 0 calc((1.5px + var(--k) * 4.5px) / -2);
  border-radius: 50%;
  background: hsl(var(--orb-h) 80% calc(56% + var(--k) * 18%));
  opacity: calc(0.16 + var(--k) * 0.34);
  box-shadow: 0 0 calc(2px + var(--k) * 7px) hsl(var(--orb-h) 80% 62% / calc(0.16 + var(--k) * 0.3));
  translate: calc(var(--px, 0px) * var(--depth, 1)) calc(var(--py, 0px) * var(--depth, 1));
  transition: translate 0.45s cubic-bezier(0.22, 0.61, 0.36, 1);
  animation: vote-drift var(--drift-time, 9s) ease-in-out infinite alternate;
  animation-delay: var(--drift-delay, 0s);
}

/* ── Entry duel preview ──────────────────────────────────────────────────── */
/* Two real cards, fanned. `animation: none` because vote-card-in settles on
   `transform: none` with a both-fill, which would silently erase the fan. */
/* TWO FANNED CARDS, OVERLAPPING — the composition the mockup was chosen on.
   They were briefly pulled apart into a row; that was not asked for and it is not
   what was picked. The instruction was only to move the VS mark off the card, so
   the mark now sits BELOW the pair, where it labels them without covering either.

   The preview also strips the crew line and the night chips: this is a still
   picture of what the game looks like, not a card anyone has to read and decide
   on, and the fan hides half of each line anyway. Title, rarity and orb are what
   carry the idea. */
.vote-duel {
  align-self: center;
  width: 100%;
  display: flex; flex-direction: column; align-items: center;
  pointer-events: none;
}
.vote-duel__fan {
  position: relative;
  width: 100%;
  height: 210px;
  display: flex; align-items: center; justify-content: center;
}
.vote-duel .vote-card {
  position: absolute;
  width: 156px;
  padding: 10px 10px 14px;
  animation: none;              /* vote-card-in settles on transform:none */
  cursor: default;
}
.vote-duel .vote-card:nth-of-type(1) { transform: translate(-46px, -22px) rotate(-7deg); }
.vote-duel .vote-card:nth-of-type(2) { transform: translate(46px, 8px) rotate(6deg); z-index: 2; }
.vote-duel .vote-orb { width: 64px; margin: 2px 10px 0; }
.vote-duel .vote-card__title { font-size: 14px; }
.vote-duel .vote-card__crew,
.vote-duel .vote-card__band-nights,
.vote-duel .vote-card__vary { display: none; }

/* ── Listen to the setlist ───────────────────────────────────────────────── */
/* The only links on the page that leave the site, so they read as a footer block
   rather than as three more buttons competing with the one that starts the game. */
.vote-listen {
  display: flex; flex-direction: column; align-items: center; gap: 9px;
  padding-top: 6px;
}
.vote-listen__head {
  display: inline-flex; align-items: center; gap: 7px;
  font: 600 12px var(--font-thai);
  color: var(--on-dark-muted);
}
.vote-listen__head img { width: 15px; height: 15px; }
.vote-listen__row { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
.vote-listen__btn {
  padding: 8px 15px;
  border-radius: 999px;
  border: 1px solid rgba(var(--white-rgb), 0.16);
  background: rgba(var(--white-rgb), 0.05);
  color: var(--on-dark-text);
  font: 600 12px var(--font-latin);
  letter-spacing: 0.01em;
  text-decoration: none;
}
@media (hover: hover) { .vote-listen__btn:hover { background: rgba(var(--white-rgb), 0.1); border-color: rgba(var(--brand-teal-rgb), 0.45); } }
.vote-listen__btn:focus-visible { outline: 3px solid var(--brand-teal); outline-offset: 3px; }

/* ── Secondary row ───────────────────────────────────────────────────────── */
.vote-quick { display: flex; gap: 8px; }
.vote-quick__btn {
  flex: 1 1 0;
  min-height: 44px;
  border-radius: 12px;
  border: 1px solid rgba(var(--white-rgb), 0.16);
  background: rgba(var(--white-rgb), 0.04);
  color: var(--on-dark-soft);
  font: 600 13px var(--font-thai);
  cursor: pointer;
}
@media (hover: hover) { .vote-quick__btn:hover { background: rgba(var(--white-rgb), 0.09); color: var(--white); } }
.vote-quick__btn:focus-visible { outline: 3px solid var(--brand-teal); outline-offset: 3px; }

/* ── Actions ─────────────────────────────────────────────────────────────── */
.vote-actions { display: flex; flex-direction: column; gap: 10px; }
.vote-actions__note {
  margin: 0; text-align: center;
  font: 400 12px/1.6 var(--font-thai);
  color: var(--on-dark-muted);
}

/* Fixed gradient + fixed ink, not theme-following — the same two stops
   auth.css's .noticket-modal__btn-primary already uses safely in both themes.
   Pairing var(--accent-gold) with fixed dark text is the exact combination that
   collides in light mode (CLAUDE.md's fixed-surface rule). */
.vote-cta {
  width: 100%;
  min-height: 52px;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 16px 26px;
  border: none;
  border-radius: var(--radius-xl);
  background: linear-gradient(120deg, var(--cta-grad-hi), var(--cta-grad-lo));
  color: var(--brand-gold-ink);
  font: 700 16px var(--font-thai);
  text-decoration: none;
  cursor: pointer;
}
@media (hover: hover) { .vote-cta:hover { filter: brightness(1.06); } }
.vote-cta__arrow { display: inline-flex; font-size: 15px; }

.vote-secondary {
  width: 100%;
  min-height: 48px;
  padding: 14px 20px;
  border-radius: var(--radius-xl);
  border: 1px solid rgba(var(--white-rgb), 0.16);
  background: rgba(var(--white-rgb), 0.05);
  color: var(--on-dark-text);
  font: 600 14px var(--font-thai);
  cursor: pointer;
}
@media (hover: hover) { .vote-secondary:hover { background: rgba(var(--white-rgb), 0.09); } }

/* Guest entry. The 50% consequence is part of the control itself, not a
   footnote somewhere else on the page (§7.5). */
.vote-guest {
  width: 100%;
  min-height: 56px;
  padding: 12px 20px;
  /* gap 8px = form standard (2026-10-25 spacing sweep; was 3px). */
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  border-radius: var(--radius-xl);
  border: 1px dashed rgba(var(--white-rgb), 0.22);
  background: transparent;
  cursor: pointer;
}
@media (hover: hover) { .vote-guest:hover { background: rgba(var(--white-rgb), 0.05); } }
.vote-guest__label { font: 600 14px var(--font-thai); color: var(--white); }
/* The caution signal lives in the gold TINT behind the text, not in the text colour.
   Gold ink measured 3.19:1 here in light mode and 11px needs 4.5 — a tinted pill with
   --text-primary on it keeps the warning loud AND legible in both themes. The user
   asked for this warning explicitly, so it must not be quietly muted to pass. */
.vote-guest__warn {
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  background: rgba(var(--brand-gold-rgb), 0.18);
  color: var(--white);
  font: 700 12px var(--font-thai);
}

/* ── Disabled state ──────────────────────────────────────────────────────── */
/* Scoped to its own wrapper, NEVER to #vote-root — a flex-column root with
   align-items:stretch has silently stretched unrelated children before on this
   project (see the .lg-back regression noted in homesafe.css). */
.vote-gate-shell {
  flex: 1;
  display: flex; flex-direction: column; justify-content: center;
  padding: 0 calc(16px + var(--safe-right)) 0 calc(16px + var(--safe-left));
}
.vote-disabled {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  text-align: center; padding: 48px 32px;
  width: calc(100% - 32px); max-width: 420px; margin: 0 auto;
  background:
    radial-gradient(circle at 50% 0%, rgba(var(--accent-gold-rgb), 0.14), transparent 60%),
    var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-5xl);
}
.vote-disabled__icon {
  width: 78px; height: 78px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px; font-size: 38px;
  background: rgba(var(--accent-gold-rgb), 0.13);
}
.vote-disabled__title { font: 800 19px var(--font-thai); color: var(--text-primary); }
.vote-disabled__sub   { font: 400 13px/1.6 var(--font-thai); color: var(--text-secondary); }

/* ── Onboarding level 2 · how to play ────────────────────────────────────── */
/* A full-viewport scrim: it must cover the notch and the home indicator, so the
   SCRIM is padded rather than the card's own height (CLAUDE.md safe-area rule). */
/* ⚠️ THE BRIEFING MUST FIT THE SCREEN, AT EVERY SIZE.
   Measured 2026-08-25 in real 320-, 360- and 375-wide layout viewports (a
   same-origin iframe — Chrome's window will not go below 500px, so resizing it
   proves nothing): the card-anatomy slide rendered 658px tall at 375×667, 688px
   at 360×640 and 729px at 320×568 — 48px and 161px of it off-screen, split top
   and bottom, and UNREACHABLE because a centred flex item overflows in both
   directions at once. Three layers, in this order:
     1. `safe center` — if anything ever overflows anyway it aligns to the start,
        so the top is never the half that gets cut off;
     2. the card can never be taller than the scrim's content box;
     3. the ART region is the part that gives: it shrinks first and scrolls only
        if it still does not fit, so the title and the button never move. */
.vote-howto {
  position: fixed; inset: 0;
  z-index: 1050; /* same takeover layer as .vote-game — see the note there */
  display: flex; align-items: safe center; justify-content: center;
  overflow-y: auto;
  padding:
    calc(20px + var(--safe-top)) calc(16px + var(--safe-right))
    calc(20px + var(--safe-bottom)) calc(16px + var(--safe-left));
  background: rgba(4, 4, 14, 0.82);
  backdrop-filter: blur(6px);
}
.vote-howto__card {
  width: 100%; max-width: 380px;
  max-height: 100%;
  overflow: hidden;
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  text-align: center;
  padding: 34px 26px 22px;
  border-radius: var(--radius-5xl);
  border: 1px solid var(--border);
  background: var(--bg-secondary);
  box-shadow: 0 24px 60px rgba(var(--shadow-rgb), calc(0.4 * var(--shadow-mult)));
}
.vote-howto__art {
  width: 84px; height: 84px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px;
  background: rgba(var(--accent-teal-rgb), 0.12);
}
.vote-howto__emoji { font-size: 40px; line-height: 1; display: inline-flex; }
.vote-howto__emoji img { width: 40px; height: 40px; }
/* flex: 0 0 auto — a flex item's default is 0 1 auto, so under `max-height` these
   would squeeze before the art region did, and the words are the last thing that
   should give. */
/* margin-top on top of the card's 12px gap: the heading follows a bordered
   diagram on the wide slides, and 12px alone read as the heading belonging to
   the box rather than introducing it. */
.vote-howto__title { flex: 0 0 auto; margin: 8px 0 0; font: 800 19px/1.35 var(--font-thai); color: var(--text-primary); }
.vote-howto__body  { flex: 0 0 auto; margin: 0; max-width: 26ch; font: 400 13px/1.7 var(--font-thai); color: var(--text-secondary); }

.vote-howto__dots { flex: 0 0 auto; display: flex; gap: 6px; margin-top: 4px; }
.vote-howto__dot {
  width: 6px; height: 6px; border-radius: 999px;
  background: rgba(var(--surface-rgb), 0.22);
  transition: background 0.2s ease, width 0.2s ease;
}
.vote-howto__dot.is-on { width: 18px; background: var(--accent-teal); }

.vote-howto__actions { flex: 0 0 auto; display: flex; flex-direction: column; gap: 6px; width: 100%; margin-top: 8px; }
.vote-howto__next {
  width: 100%; min-height: 48px;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  border: none; border-radius: var(--radius-xl);
  background: linear-gradient(120deg, var(--cta-grad-hi), var(--cta-grad-lo));
  color: var(--brand-gold-ink);
  font: 700 15px var(--font-thai);
  cursor: pointer;
}
@media (hover: hover) { .vote-howto__next:hover { filter: brightness(1.06); } }
.vote-howto__next-arrow { display: inline-flex; font-size: 14px; }
/* The quietest control on the card, so it should not occupy a full-width 44px
   band under the primary button. Narrow, centred, and only as tall as it needs to
   be — it still clears a comfortable tap target with its padding. */
.vote-howto__skip {
  align-self: center;
  width: auto; min-width: 88px;
  padding: 7px 18px;
  border: none; background: transparent;
  color: var(--text-secondary);
  font: 600 12px var(--font-thai);
  border-radius: 999px;
  cursor: pointer;
}
@media (hover: hover) { .vote-howto__skip:hover { color: var(--text-primary); } }
.vote-howto__skip[hidden] { display: none; }

/* ── Focus visibility (§8.4) ─────────────────────────────────────────────── */
/* Keyboard is a first-class input here, so the ring must be unmistakable and
   must survive on top of the gradient buttons. */
.vote-cta:focus-visible,
.vote-secondary:focus-visible,
.vote-guest:focus-visible,
.vote-quick__btn:focus-visible,
.vote-howto__next:focus-visible,
.vote-howto__skip:focus-visible {
  outline: 3px solid var(--accent-teal);
  outline-offset: 3px;
}

/* ── Motion (§8.4) ───────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .vote-entrystar { animation: none; transition: none; }
  .vote-howto__dot { transition: none; }
}

/* ── Wider viewports ─────────────────────────────────────────────────────── */
/* Desktop widens the same column and gives the hero more room. It does NOT get
   a separate layout — one code path for every input (§8.1). */
@media (min-width: 768px) {
  .vote-page { max-width: 620px; padding-top: 40px; gap: 26px; }
  .vote-hero__title { font-size: 33px; }
  .vote-hero__sub { font-size: 15px; }
  .vote-stat__num { font-size: 24px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE GAME — phase C
   ══════════════════════════════════════════════════════════════════════════

   ⚠️ THIS WHOLE SECTION IS A FIXED DARK SURFACE IN BOTH THEMES.
   It is an immersive "space" view, deliberately not theme-following, the same
   way p/lightbox.js is. CLAUDE.md's rule therefore applies in full: every
   color / border / fill painted on it must be a LITERAL, never a var(--…)
   token, or it flips to near-black ink in light mode and disappears. That rule
   shipped broken four times on this project before there was a checker for it —
   run `node scripts/check-fixed-surface.mjs` after touching anything here.

   The ONE exception is the orb hues below, which come from CSS custom
   properties set per card by vote-card.js. Those are per-song identity values
   computed from a frozen seed, not theme values — see §3.2. */

.vote-game {
  position: fixed;
  inset: 0;
  /* Layering (measured, not guessed): .ppw-nav is z-index 1000, the auth.css modal
     shell is 1100, guide-modal 1200, the toast host 9999. This is a full-screen
     takeover with its own exit control, so it must sit ABOVE the nav — at 60 the
     nav punched straight through and covered the phase tag and counter entirely.
     It must stay BELOW 1100 so the exit-confirm modal still lands on top of it. */
  z-index: 1050;
  display: flex;
  flex-direction: column;
  background:
    radial-gradient(ellipse 120% 80% at 50% 120%, rgba(var(--brand-gold-rgb), 0.10), transparent 60%),
    radial-gradient(ellipse 90% 60% at 8% -10%, rgba(var(--brand-teal-rgb), 0.10), transparent 55%),
    var(--fixed-bg-0);
  color: var(--on-dark-text);
  /* Nothing should touch a screen edge. */
  padding:
    calc(16px + var(--safe-top)) calc(20px + var(--safe-right))
    calc(20px + var(--safe-bottom)) calc(20px + var(--safe-left));
}

/* ── Head + progress ─────────────────────────────────────────────────────── */
.vote-gamehead { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex: 0 0 auto; }
/* back + exit travel together on the right; with three loose children,
   space-between left the back button stranded in the middle of the bar. */
.vote-gamehead__actions { display: flex; align-items: center; gap: 8px; }
.vote-gamehead__meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.vote-gamehead__phase {
  font: 700 12px/1.2 var(--font-thai);
  color: var(--brand-teal);
  letter-spacing: 0.04em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.vote-gamehead__count { font: 600 11px/1 var(--font-latin); color: #9A9A9A; letter-spacing: 0.1em; }
.vote-gamehead__exit {
  flex: 0 0 auto;
  width: 40px; height: 40px;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(var(--white-rgb), 0.12);
  border-radius: 999px;
  background: rgba(var(--white-rgb), 0.05);
  color: var(--on-dark-text);
  font-size: 14px;
  cursor: pointer;
}
@media (hover: hover) { .vote-gamehead__exit:hover { background: rgba(var(--white-rgb), 0.1); } }

.vote-gamebar {
  flex: 0 0 auto;
  height: 3px; margin: 14px 0 0;
  border-radius: 999px;
  background: rgba(var(--white-rgb), 0.09);
  overflow: hidden;
}
.vote-gamebar__fill {
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--brand-teal), var(--brand-gold));
  transition: width 0.32s ease;
}

.vote-stage { flex: 1 1 auto; min-height: 0; display: flex; align-items: center; justify-content: center; padding: 16px 0; }

/* Screen-reader-only live region. */
.vote-live {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden; white-space: nowrap;
}

/* ── Arena ───────────────────────────────────────────────────────────────── */
/* The arena is ONE centred group: prompt, cards, hint sitting together in the
   middle of the screen. Without justify-content the column spread its children to
   the extremes — the prompt ended up glued to the top edge and the hint to the
   bottom, with a gulf between them and the cards. */
.vote-arena {
  width: 100%; max-width: 900px;
  height: 100%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  /* 14px read as cramped: the question, the cards and the hint were three lines
     stacked on top of each other rather than three separate things. */
  gap: 30px;
  min-height: 0;
  padding: 0 4px;
}
.vote-arena__round {
  flex: 0 0 auto;
  padding: 4px 12px;
  border-radius: 999px;
  border: 1px solid rgba(var(--brand-gold-rgb), 0.4);
  background: rgba(var(--brand-gold-rgb), 0.14);
  color: var(--brand-gold-pale);
  font: 700 11px var(--font-thai);
}
/* pre-line so the line break written into the prompt string actually renders.
   The lang-dict KEY keeps a single space, not the break: lang-toggle collapses
   whitespace before matching, so a key carrying the break could never match. */
.vote-arena__prompt { flex: 0 0 auto; margin: 0; font: 700 15px/1.3 var(--font-thai); color: var(--on-dark-text); text-align: center; white-space: pre-line; }
.vote-arena__hint { flex: 0 0 auto; margin: 10px 0 0; font: 500 11px var(--font-thai); color: var(--on-dark-dim); text-align: center; }
/* The bracket screens ("เหลือ N ใบสุดท้าย") add a chip above the question, so the
   column is one element taller and the hint ends up crowding the card edge —
   measured at 14px on a short screen against 36px on a tall one (reported
   2026-08-25). Give it back the breathing room on exactly those screens. */
.vote-arena.is-bracket .vote-arena__hint { margin-top: 16px; }

/* Portrait phones stack the cards top/bottom so BOTH stay within thumb reach
   and fully visible — side-by-side on a narrow screen is either unreadable or
   unreachable (§8.1). Landscape and desktop go side by side, further down. */
/* flex: 0 1 auto — the rack hugs its cards. With flex: 1 it swallowed the whole
   stage, which is what pushed the prompt and hint apart to the screen edges. */
.vote-rack {
  flex: 0 1 auto;
  width: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--rack-gap);
}

/* ── Card ─────────────────────────────────────────────── */
/* A CARD: portrait, contents stacked downward, like something you could hold.
   The horizontal slab tried before read as a block, not a card.

   What the ORIGINAL vertical card got wrong was proportion, not direction: it was
   too tall, margin-top:auto on the footer opened a hole through the middle, and
   the orb was the largest thing on it while the title — the only thing anyone
   decides on — was small. So: vertical again, but compact, no auto margins
   anywhere, a moderate orb, and the title as the hero. */
.vote-card {
  position: relative;
  flex: 0 1 auto;
  width: 100%;
  max-width: 300px;
  margin-inline: auto;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 14px 16px 18px;
  border: 1px solid rgba(var(--white-rgb), 0.12);
  border-radius: 20px;
  background:
    radial-gradient(120% 70% at 50% 0%, rgba(var(--white-rgb), 0.08), transparent 66%),
    var(--fixed-bg-2);
  color: var(--on-dark-text);
  font-family: var(--font-thai);
  text-align: center;
  cursor: pointer;
  transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, opacity 0.22s ease;
}
@media (hover: hover) { .vote-card:hover { transform: translateY(-3px); border-color: rgba(var(--white-rgb), 0.3); } }
.vote-card:focus-visible { outline: 3px solid var(--brand-teal); outline-offset: 2px; }
/* The arena takes programmatic focus at the start of each screen (see vote.js);
   it is not Tab-reachable, so it must not draw a ring of its own. */
.vote-arena:focus, .vote-arena:focus-visible { outline: none; }
.vote-card.is-chosen {
  border-color: rgba(var(--brand-gold-rgb), 0.9);
  box-shadow: 0 0 0 2px rgba(var(--brand-gold-rgb), 0.35), 0 0 48px rgba(var(--brand-gold-rgb), 0.45);
  transform: scale(1.02);
}
/* The loser dims but never vanishes — the visual promise that every card still
   carries a score (constraint #3). */
.vote-card.is-dimmed { opacity: 0.3; transform: scale(0.98); }

.vote-card__band {
  width: 100%;
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 6px;
  font: 700 10px var(--font-thai);
  letter-spacing: 0.04em;
}
.vote-card__band-glyph { display: inline-flex; font-size: 13px; line-height: 1; }
.vote-card__band-glyph img { width: 13px; height: 13px; }
.vote-card__band-label { color: #A9AEC6; }
.vote-card__band-nights { margin-left: auto; display: flex; gap: 3px; }
.vote-card__night {
  padding: 1px 5px;
  border-radius: 4px;
  background: rgba(var(--white-rgb), 0.08);
  color: #9A9A9A;
  font: 700 9px var(--font-latin);
}
/* Rarity is carried by an icon AND a label as well as this hue — never hue alone. */
.vote-card[data-rarity="star"]   .vote-card__band-label { color: var(--brand-gold); }
.vote-card[data-rarity="home"]   .vote-card__band-label { color: #7FB2FF; }
.vote-card[data-rarity="binary"] .vote-card__band-label { color: var(--brand-teal); }
.vote-card[data-rarity="comet"]  .vote-card__band-label { color: #FF6B66; }

/* Orb above, text below — the card reads top to bottom. */
/* flex: 1 1 auto + justify-content: center — when the two cards of a duel are
   stretched to the same height (row mode), the shorter one's leftover space used
   to pile up UNDER its content, so its orb and title sat pinned to the top edge
   while the taller card's sat lower. The band stays at the top: it is the card's
   header strip, not part of the song. */
.vote-card__body {
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;
}
.vote-card__text { display: flex; flex-direction: column; align-items: center; gap: 3px; min-width: 0; }

/* ── Orb — pure CSS, hues from the frozen seed (§9.4) ────────── */
/* The rings sit OUTSIDE the sphere, so the orb needs its own breathing room or the
   card edge slices them off. */
.vote-orb {
  position: relative;
  /* The rings tilt on their OWN short period, ~6-9s, derived from the same frozen
     seed as --orb-spin so a given song always moves the same way. */
  --ring-tilt: calc(var(--orb-spin, 40s) * 0.19);
  flex: 0 0 auto;
  width: clamp(56px, 13vh, 92px);
  aspect-ratio: 1;
  margin: 6px 16px 2px;
  border-radius: 50%;
  background:
    radial-gradient(circle at var(--orb-x) var(--orb-y),
      hsl(var(--orb-h) 95% 78%) 0%,
      hsl(var(--orb-h) 85% 55%) 38%,
      hsl(var(--orb-h2) 70% 28%) 78%,
      hsl(var(--orb-h2) 65% 16%) 100%);
  box-shadow:
    inset -5px -7px 16px rgba(var(--black-rgb), 0.45),
    0 0 26px hsl(var(--orb-h) 80% 50% / 0.4);
}
/* The rings drift too. A card whose orb has rings but no satellites was the only
   completely still thing on the screen, next to cards whose satellites orbit —
   which read as that card having failed to load rather than as a different kind
   of orb. The tilt animates rather than a full spin: a flat ellipse spun on its
   own axis looks like it is wobbling, while easing the tilt angle reads as the
   ring turning slowly in space. Each ring gets its own period so a two-ring orb
   never moves as one piece, and both are derived from the frozen orbSeed via
   --orb-spin, so a given song always drifts the same way. */
/* ── The finale: the three songs that close the show ──────────────────────
   Their colour MOVES. Everything else on a card is a fixed identity — an orb's
   hue is how you recognise a song across screens — so this is deliberately the
   only exception, and it is reserved for the three songs the concert ends on.
   The hue rotates through a narrow band around the song's own colour rather than
   the whole wheel: the card must still be recognisably itself, it just never
   settles. A second, slower layer of warm light drifts across it so the two
   never line up and the loop is not countable. */
.vote-orb--finale {
  animation: vote-finale-hue calc(var(--orb-spin, 40s) * 0.55) ease-in-out infinite alternate;
}
.vote-orb--finale::after {
  content: '';
  position: absolute; inset: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 70% 78%,
    hsl(calc(var(--orb-h) + 46) 95% 72% / 0.55), transparent 62%);
  mix-blend-mode: screen;
  animation: vote-finale-sweep calc(var(--orb-spin, 40s) * 0.9) ease-in-out infinite alternate;
}
@keyframes vote-finale-hue {
  from { filter: hue-rotate(-16deg) saturate(1); }
  to   { filter: hue-rotate(16deg) saturate(1.18); }
}
@keyframes vote-finale-sweep {
  from { transform: translate(-14%, 10%) scale(0.86); opacity: 0.45; }
  to   { transform: translate(12%, -12%) scale(1.12); opacity: 0.9; }
}

.vote-orb__ring {
  position: absolute; inset: -14%;
  border: 1px solid hsl(var(--orb-h2) 80% 70% / 0.5);
  border-radius: 50%;
  transform: rotate(-18deg) scaleY(0.34);
  /* --ring-tilt, not --orb-spin. The first version eased a 16-degree wobble over
     the orb's own 34-51s period — a third of a degree per second, technically
     moving and invisible in practice. */
  animation: vote-ring-tilt-1 var(--ring-tilt) ease-in-out infinite alternate;
}
.vote-orb__ring--2 {
  inset: -24%; transform: rotate(14deg) scaleY(0.26); opacity: 0.6;
  animation: vote-ring-tilt-2 calc(var(--ring-tilt) * 1.37) ease-in-out infinite alternate;
  animation-delay: -2.4s;
}
/* Ring 3 exists because the count is now the night count (1-3), not orbSeed % 3
   (0-2) — without it a three-night song would silently draw only two. */
.vote-orb__ring--3 {
  inset: -34%; transform: rotate(-6deg) scaleY(0.18); opacity: 0.42;
  animation: vote-ring-tilt-3 calc(var(--ring-tilt) * 0.79) ease-in-out infinite alternate;
  animation-delay: -5.1s;
}
/* A wide arc through flat, and scaleY holds still so it reads as a ring TURNING
   rather than a shape wobbling. Ring 2 runs the arc backwards, so a multi-ring orb
   never moves as one piece. */
@keyframes vote-ring-tilt-1 {
  from { transform: rotate(-27deg) scaleY(0.34); }
  to   { transform: rotate(27deg)  scaleY(0.34); }
}
@keyframes vote-ring-tilt-2 {
  from { transform: rotate(24deg)  scaleY(0.26); }
  to   { transform: rotate(-24deg) scaleY(0.26); }
}
@keyframes vote-ring-tilt-3 {
  from { transform: rotate(-19deg) scaleY(0.18); }
  to   { transform: rotate(19deg)  scaleY(0.18); }
}
.vote-orb__sat {
  position: absolute; top: 50%; left: 50%;
  width: 4px; height: 4px; margin: -2px;
  border-radius: 50%;
  background: #FFF3D6;
  box-shadow: 0 0 5px rgba(255, 243, 214, 0.9);
  animation: vote-orbit var(--orb-spin, 40s) linear infinite;
  animation-delay: calc(var(--i) * -3s);
}
@keyframes vote-orbit {
  from { transform: rotate(calc(var(--i) * (360deg / var(--n))))          translateX(30px); }
  to   { transform: rotate(calc(var(--i) * (360deg / var(--n)) + 360deg)) translateX(30px); }
}

/* ── Card text — the title is the hero ─────────────────── */
.vote-card__title {
  font: 800 clamp(16px, 2.6vh, 21px)/1.25 var(--font-thai);
  color: var(--white);
}
/* The star-catalogue look comes from letter-spacing, NOT from injecting a space
   between every character. The old spacedLatin() did the latter, which made every
   single letter its own word — so the line could break anywhere at all and
   "PLOOK JAI SURAH PAH" wrapped as "PLOOK JAI SU / RAH PAH" on a narrow card.
   With real letter-spacing the words stay whole and break only at real spaces.
   The negative margin-right cancels the trailing space letter-spacing adds after
   the final character, which would otherwise push centred text off-centre. */
.vote-card__latin {
  font: 600 9px/1.4 var(--font-latin);
  color: var(--on-dark-dim);
  letter-spacing: 0.26em;
  margin-right: -0.26em;
  overflow-wrap: anywhere;
}
.vote-card__crew { display: flex; flex-wrap: wrap; justify-content: center; gap: 3px 10px; margin-top: 3px; }
.vote-card__crew-item {
  display: inline-flex; align-items: center; gap: 4px;
  font: 600 10px var(--font-thai);
  color: var(--on-dark-soft);
}
.vote-card__crew-item--guest { color: var(--on-dark-muted); }
.vote-card__crew-glyph { display: inline-flex; font-size: 8px; opacity: 0.8; }
.vote-card__vary { font: 500 9px var(--font-thai); color: var(--on-dark-dim); }

.vote-card.is-attended { border-color: rgba(var(--brand-teal-rgb), 0.45); }
.vote-card__attended {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(var(--brand-teal-rgb), 0.16);
  color: #E0E0E0;
  font: 700 9px var(--font-thai);
}
.vote-card__attended-glyph { display: inline-flex; font-size: 9px; }

/* ── The Skip card (2026-08-25) ────────────────────────────────────────────
   Offered beside a solo song so "ไม่โดน" is a real answer instead of a screen you
   can only agree with. It is deliberately NOT a song card: no orb, no rarity, no
   nights — it is the absence of a choice, and dressing it as a card would invite
   a moment of reading it as a fifty-second song. Dashed border, muted, and it
   never competes with the real card for attention. */
.vote-skip {
  position: relative;
  flex: 0 1 auto;
  width: 100%;
  max-width: 300px;
  margin-inline: auto;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  padding: 18px 16px;
  border: 1px dashed rgba(var(--white-rgb), 0.26);
  border-radius: 20px;
  background: rgba(var(--white-rgb), 0.03);
  color: var(--on-dark-soft);
  font-family: var(--font-thai);
  text-align: center;
  cursor: pointer;
  transition: transform 0.16s ease, border-color 0.16s ease, opacity 0.22s ease;
}
@media (hover: hover) { .vote-skip:hover { transform: translateY(-3px); border-color: rgba(var(--white-rgb), 0.45); } }
.vote-skip:focus-visible { outline: 3px solid var(--brand-teal); outline-offset: 2px; }
.vote-skip.is-chosen { border-color: rgba(var(--brand-gold-rgb), 0.9); border-style: solid; color: var(--white); }
.vote-skip.is-dimmed { opacity: 0.3; transform: scale(0.98); }
.vote-skip__glyph { display: inline-flex; font-size: 18px; color: var(--on-dark-dim); }
.vote-skip__title { font: 800 16px var(--font-latin); letter-spacing: 0.04em; color: var(--on-dark-text); }
.vote-skip__sub   { font: 500 11px var(--font-thai); color: var(--on-dark-muted); }
/* Same corner badge as a real card, so the number key means the same thing here. */
.vote-skip__num {
  position: absolute; top: -14px; right: -14px;
  width: 26px; height: 26px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  border: 1px solid rgba(var(--white-rgb), 0.22);
  background: var(--fixed-bg-0);
  color: var(--on-dark-muted);
  font: 700 11px var(--font-latin);
}
.vote-skip:focus-visible .vote-skip__num { border-color: rgba(var(--white-rgb), 0.5); color: var(--white); }
@media (hover: hover) { .vote-skip:hover .vote-skip__num { border-color: rgba(var(--white-rgb), 0.5); color: var(--white); } }
.vote-skip.is-chosen .vote-skip__num { border-color: rgba(var(--brand-gold-rgb), 0.9); color: var(--brand-gold-pale); }
@media (max-height: 660px) { .vote-skip { padding: 12px 14px; gap: 3px; } }
@media (max-height: 580px) { .vote-skip { padding: 9px 12px; } }

/* VS mark between the two cards of a duel.

   POSITION: a real flex item sitting between the cards, then pulled back by a
   negative margin so the visual gap is unchanged. It used to be absolutely
   positioned at the rack's 50% — which is the middle of the GAP only when both
   cards happen to be the same height. A two-line title makes one card taller, and
   the mark then slid toward it, which is why it looked centred on some screens
   and stuck to a card edge on others. The margin is derived from the gap and the
   mark's own size, so changing either keeps it exact:
     2·gap + size + 2·margin = gap   ->   margin = -(gap + size) / 2

   COLOUR: it used to be #10111F on a var(--fixed-bg-2) card — a 1-step difference, so it
   read as part of whichever card it overlapped. It has to be DISTINCT, not loud:
   a solid teal fill was tried and was too shouty for a label nobody needs to act
   on. So the chip takes the surface colour from BEHIND the cards, which makes it
   read as a gap punched between them, with a thin teal ring and muted teal
   lettering to separate it. Fixed-dark surface, so every value is a literal. */
.vote-rack { --rack-gap: 20px; --vs-size: 34px; }
.vote-vs {
  position: relative;
  z-index: 2;
  align-self: center;
  flex: 0 0 auto;
  margin-block: calc((var(--rack-gap) + var(--vs-size)) / -2);
  width: var(--vs-size); height: var(--vs-size);
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  border: 1px solid rgba(var(--brand-teal-rgb), 0.38);
  background: var(--fixed-bg-0);
  color: var(--on-dark-soft);   /* was PPW teal #6FBFBE (2026-10-22) */
  font: 700 10px var(--font-latin);
  letter-spacing: 0.06em;
  pointer-events: none;
}

/* ── Interstitials (§6.3/§6.4) ───────────────────────────────────────────── */
.vote-inter {
  width: 100%; max-width: 420px;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  text-align: center;
  padding: 38px 30px 28px;
  border: 1px solid rgba(var(--white-rgb), 0.12);
  border-radius: 22px;
  background:
    radial-gradient(circle at 50% 0%, rgba(var(--brand-teal-rgb), 0.16), transparent 62%),
    var(--fixed-bg-2);
}
.vote-inter[data-kind="phase"]   { background: radial-gradient(circle at 50% 0%, rgba(var(--brand-gold-rgb), 0.18), transparent 62%), var(--fixed-bg-2); }
.vote-inter[data-kind="anomaly"] { background: radial-gradient(circle at 50% 0%, rgba(255, 107, 102, 0.16), transparent 62%), var(--fixed-bg-2); }
.vote-inter__kicker {
  font: 700 10px var(--font-latin);
  letter-spacing: 0.22em;
  color: var(--brand-teal);
}
.vote-inter[data-kind="phase"]   .vote-inter__kicker { color: var(--brand-gold); }
.vote-inter[data-kind="anomaly"] .vote-inter__kicker { color: #FF6B66; }
.vote-inter__glyph { font-size: 34px; color: #FF6B66; display: inline-flex; }
.vote-inter__title { margin: 0; font: 800 23px/1.3 var(--font-thai); color: var(--white); }
.vote-inter__sub   { margin: 0; max-width: 26ch; font: 400 13px/1.7 var(--font-thai); color: var(--on-dark-muted); }
.vote-inter__count { font: 600 11px var(--font-thai); color: var(--on-dark-dim); }
.vote-inter__cta {
  margin-top: 10px;
  width: 100%; min-height: 48px;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  border: none; border-radius: 12px;
  background: linear-gradient(120deg, var(--cta-grad-hi), var(--cta-grad-lo));
  color: var(--brand-gold-ink);
  font: 700 15px var(--font-thai);
  cursor: pointer;
}
@media (hover: hover) { .vote-inter__cta:hover { filter: brightness(1.06); } }
.vote-inter__cta:focus-visible { outline: 3px solid var(--brand-teal); outline-offset: 3px; }

/* ── Short screens: RE-LAYOUT, never scroll (binding, 2026-08-25) ──────────
   THE RULE: the voting screen must be entirely visible on the screen it is on.
   Not clipped, not overflowing, never scrolled. A card you have to scroll to see
   is a card you cannot compare against the other one, which is the whole game.

   Measured in real layout viewports (a same-origin iframe — Chrome's 500px window
   floor makes window resizing useless for this):

     375x667   0px overflow      375x600   2px
     360x640   0px               360x560  31px
     320x568  25px               320x480  91px

   So it fits a modern phone and stops fitting a short one. The answer is to spend
   LESS HEIGHT, not to hand the player a scrollbar. What gives, in order: the
   arena's breathing room, then the card's padding, then the orb — the only thing
   on the card carrying no information (see vote-card.js). Type sizes are NOT
   reduced; 11-13px Thai is already the floor and the user's rule is that a
   tightened layout must still be readable. fitArena() in vote.js is the backstop
   for whatever these queries cannot predict (a title that wraps to three lines). */
@media (max-height: 660px) {
  /* The 30px between prompt / cards / hint was asked for explicitly and stays on
     any screen with room for it — this only claws it back where there is not. */
  .vote-arena { gap: 18px; }
  .vote-stage { padding: 8px 0; }
  .vote-rack  { --rack-gap: 14px; }
  .vote-card  { padding: 10px 14px 14px; gap: 6px; }
  .vote-orb   { width: clamp(48px, 11vh, 92px); margin: 2px 14px 0; }
}
@media (max-height: 580px) {
  .vote-arena { gap: 12px; }
  /* Was 2px, which put the hint 14px from the cards once the arena gap shrank
     too — close enough to read as part of the card rather than a caption. */
  .vote-arena__hint { margin-top: 8px; }
  .vote-arena.is-bracket .vote-arena__hint { margin-top: 12px; }
  .vote-card  { padding: 8px 12px 12px; }
  .vote-orb   { width: clamp(40px, 9vh, 92px); margin: 0 12px; }
  .vote-vs    { --vs-size: 28px; }
}

/* ── Motion (§8.4) ───────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .vote-orb__sat, .vote-orb__ring,
  .vote-orb__ring--2, .vote-orb__ring--3,
  .vote-orb--finale, .vote-orb--finale::after { animation: none; }
  .vote-card { transition: none; }
  .vote-gamebar__fill { transition: none; }
}

/* ── Landscape + desktop: cards side by side (§8.1) ──────────────────────── */
@media (min-width: 700px), (orientation: landscape) and (min-height: 380px) {
  .vote-rack { --rack-gap: 18px; flex-direction: row; align-items: stretch; justify-content: center; }
  /* Row mode: the same collapse, on the other axis. */
  .vote-vs { margin-block: 0; margin-inline: calc((var(--rack-gap) + var(--vs-size)) / -2); }
  /* max-width keeps each one a PORTRAIT card instead of letting it stretch into a
     wide panel — that stretch is what made a row of cards read as slabs. */
  /* margin-inline:auto is what centres the single card in COLUMN mode. As a row
     flex item it does something else entirely: auto margins absorb ALL the free
     space, so on a 1280px screen the two cards drifted 155px apart with the VS
     mark stranded in the middle of the hole. justify-content:center already
     centres the pair here, so the auto margins have no job left. */
  .vote-card { flex: 1 1 0; max-width: 300px; padding: 18px 18px 22px; margin-inline: 0; }
  /* .vote-skip is the other card on a solo screen and needs the identical
     treatment — it was added with the Skip rule (2026-08-25) and kept the
     column-mode margin-inline:auto, so it reproduced this bug exactly:
     measured 137px of auto margin each side, 155px between the two cards. */
  .vote-skip { flex: 1 1 0; max-width: 300px; margin-inline: 0; }
  .vote-arena__prompt { font-size: 19px; }
}
@media (min-width: 700px) {
  .vote-game { padding-left: calc(36px + var(--safe-left)); padding-right: calc(36px + var(--safe-right)); }
  .vote-card__latin { font-size: 10px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE RESULT — phase D
   ══════════════════════════════════════════════════════════════════════════
   Same fixed-dark surface rule as the game section above: literals only, no
   theme tokens for any colour. This one SCROLLS, so unlike .vote-game it is not
   position:fixed — it replaces the page content instead. */

/* ⚠️ THE BACKGROUND IS THE OTHER HALF OF THE RULE.
   The first version of this block set `color: var(--on-dark-text)` — a literal near-white —
   and no background at all, so in LIGHT mode the page's own pale gradient showed
   through and the entire result screen measured 1.26:1. Every literal light ink
   below is only correct because this surface paints its own dark ground, exactly
   like .vote-game does. Do not remove this background.
   Full-bleed, with the reading column constrained on the children instead, so the
   dark surface reaches the viewport edges rather than sitting as a dark stripe on
   a light page. */
.vote-result {
  min-height: calc(100vh  - var(--navbar-total-h));
  min-height: calc(100dvh - var(--navbar-total-h));
  display: flex;
  flex-direction: column;
  gap: 18px;
  width: 100%;
  padding:
    22px calc(16px + var(--safe-right))
    28px calc(16px + var(--safe-left));
  color: var(--on-dark-text);
  background:
    radial-gradient(ellipse 120% 70% at 50% 0%, rgba(var(--brand-teal-rgb), 0.08), transparent 60%),
    radial-gradient(ellipse 120% 60% at 50% 100%, rgba(var(--brand-gold-rgb), 0.08), transparent 60%),
    var(--fixed-bg-0);
}
.vote-result > * { width: 100%; max-width: 560px; margin-inline: auto; }

.vote-result__head { text-align: center; display: flex; flex-direction: column; gap: 4px; }
.vote-result__kicker { font: 700 10px var(--font-latin); letter-spacing: 0.22em; color: var(--brand-teal); }
.vote-result__title { margin: 0; font: 800 25px/1.3 var(--font-thai); color: var(--white); }

/* ── Champion hero ───────────────────────────────────────────────────────── */
.vote-champ {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  text-align: center;
  padding: 26px 22px 22px;
  border: 1px solid rgba(var(--brand-gold-rgb), 0.34);
  border-radius: 22px;
  background:
    radial-gradient(circle at 50% 0%, rgba(var(--brand-gold-rgb), 0.2), transparent 66%),
    var(--fixed-bg-2);
}
/* The champion orb is now the CARD's orb (buildOrb), so this only resizes it and
   gives it a bigger glow. Its gradient, rings, satellites and finale animation all
   come from .vote-orb — duplicating the gradient here is what let the two drift
   apart in the first place. Overrides .vote-orb's own margin, which is tuned for
   sitting beside card text rather than centred above a title. */
.vote-orb--champ {
  width: clamp(96px, 30vw, 150px);
  margin: 0 0 6px;
  box-shadow:
    inset -8px -10px 22px rgba(var(--black-rgb), 0.45),
    0 0 52px hsl(var(--orb-h) 85% 55% / 0.5);
}
.vote-champ__label  { font: 700 11px var(--font-thai); color: var(--brand-gold); letter-spacing: 0.06em; }
.vote-champ__title  { margin: 0; font: 800 24px/1.3 var(--font-thai); color: var(--white); }
.vote-champ__latin  { font: 600 11px var(--font-latin); color: #9A9A9A; letter-spacing: 0.12em; }
.vote-champ__credit { font: 500 12px var(--font-thai); color: var(--on-dark-muted); }

/* ── Rarity breakdown ────────────────────────────────────────────────────── */
.vote-breakdown { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.vote-breakdown__head { font: 700 12px var(--font-thai); color: var(--on-dark-muted); }
.vote-breakdown__row { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; }
.vote-breakdown__chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 11px;
  border-radius: 999px;
  border: 1px solid rgba(var(--white-rgb), 0.12);
  background: rgba(var(--white-rgb), 0.05);
  font: 700 11px var(--font-thai);
  color: var(--on-dark-soft);
}
.vote-breakdown__glyph { display: inline-flex; font-size: 12px; }
.vote-breakdown__glyph img { width: 12px; height: 12px; }
.vote-breakdown__chip[data-rarity="star"]   { border-color: rgba(var(--brand-gold-rgb), 0.4); color: var(--brand-gold); }
.vote-breakdown__chip[data-rarity="home"]   { border-color: rgba(127, 178, 255, 0.4); color: #7FB2FF; }
.vote-breakdown__chip[data-rarity="binary"] { border-color: rgba(var(--brand-teal-rgb), 0.45); color: var(--brand-teal); }
.vote-breakdown__chip[data-rarity="comet"]  { border-color: rgba(255, 107, 102, 0.4); color: #FF6B66; }

/* ── Star chart ──────────────────────────────────────────────────────────── */
/* ONE frame around the sky, the caption and the detail — they describe the same
   thing, so they are one panel, not three stacked boxes. */
.vote-chart {
  display: flex; flex-direction: column;
  border-radius: 22px;
  border: 1px solid rgba(var(--white-rgb), 0.1);
  overflow: hidden;
  background: #0A0A0A;
}
/* Sky and caption on ONE surface. The gradients used to live on the sky alone, so
   the caption sat on the panel's flat base colour and drew a hard line across the
   middle of what is meant to be a single window. */
.vote-chart__view {
  position: relative;
  background:
    radial-gradient(circle at 50% 42%, rgba(var(--brand-teal-rgb), 0.07), transparent 62%),
    radial-gradient(ellipse at 50% 104%, rgba(var(--brand-gold-rgb), 0.1), transparent 60%),
    #0A0A0A;
}
.vote-chart__sky {
  position: relative;
  width: 100%;
  aspect-ratio: 1;
  overflow: hidden;
}
/* The space feel. Three soft washes on three different long periods, so the field
   is never twice in the same state and the loop cannot be counted. Only transform
   and opacity animate — compositor work, no layout, no paint. */
.vote-chart__nebula {
  position: absolute; inset: -18%;
  pointer-events: none;
  background:
    radial-gradient(ellipse 46% 38% at 28% 32%, rgba(var(--brand-teal-rgb), 0.22), transparent 68%),
    radial-gradient(ellipse 52% 40% at 74% 62%, rgba(168, 106, 255, 0.20), transparent 70%),
    radial-gradient(ellipse 60% 44% at 50% 104%, rgba(var(--brand-gold-rgb), 0.18), transparent 66%);
  filter: blur(14px);
  animation: vote-nebula-a 34s ease-in-out infinite alternate;
}
.vote-chart__nebula::after {
  content: '';
  position: absolute; inset: 0;
  background:
    radial-gradient(ellipse 38% 30% at 68% 22%, rgba(255, 214, 150, 0.16), transparent 70%),
    radial-gradient(ellipse 44% 34% at 22% 78%, rgba(96, 148, 255, 0.18), transparent 72%);
  animation: vote-nebula-b 51s ease-in-out infinite alternate;
}
@keyframes vote-nebula-a {
  from { transform: translate(-3%, 2%) scale(1);    opacity: 0.75; }
  to   { transform: translate(4%, -3%) scale(1.14); opacity: 1; }
}
@keyframes vote-nebula-b {
  from { transform: translate(3%, -2%) scale(1.1); opacity: 0.6; }
  to   { transform: translate(-4%, 3%) scale(0.95); opacity: 1; }
}
/* Every star is a real button: keyboard-reachable and screen-reader labelled with
   its rank and score, so the information the brightness carries is never
   colour-and-size only (§8.4). */
.vote-chart__star {
  position: absolute;
  transform: translate(-50%, -50%);
  /* Pointer parallax. `translate` is its own property, so it composes with the
     centring transform above instead of fighting it. --px/--py are written once
     per frame on .vote-chart__sky (vote-result.js); --depth is per-star, so a
     brighter star travels further and the field gains depth. */
  translate: calc(var(--px, 0px) * var(--depth, 1)) calc(var(--py, 0px) * var(--depth, 1));
  transition: translate 0.35s cubic-bezier(0.22, 0.61, 0.36, 1);
  /* --k is 0 at the 10-point floor and 1 at the champion. Nothing is ever
     invisible: the dimmest star still renders. */
  width: calc(4px + var(--k) * 12px);
  height: calc(4px + var(--k) * 12px);
  padding: 0;
  border: none;
  border-radius: 50%;
  background: none;
  cursor: pointer;
}
/* The visible star. It is a separate element because it runs the DRIFT
   animation on `translate`, and the button above is already using `translate`
   for the parallax — one element cannot run two independent ones. */
.vote-chart__dot {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* The COLOUR is pinned to the card's core tone (2026-08-25 ruling): the same hue
     the orb uses, at a fixed saturation and lightness, so a star reads as the same
     colour as the card the player chose. Lightness used to ride on --k, which made
     a low-scoring song look like a washed-out different colour than its own card.
     Score is still legible, and still only from this dot — it now travels on
     OPACITY and the glow radius instead, which change how present a star feels
     without changing what colour it is. */
  background: hsl(var(--orb-h) 90% 62%);
  opacity: calc(0.34 + var(--k) * 0.66);
  box-shadow: 0 0 calc(3px + var(--k) * 22px) hsl(var(--orb-h) 90% 62% / calc(0.3 + var(--k) * 0.6));
  transition: scale 0.14s ease;
}
/* Hover/focus grows the dot via `scale`, again a standalone property, so it does
   not cancel the drift running on `translate`. */
.vote-chart__star:focus-visible .vote-chart__dot { scale: 1.7; }
@media (hover: hover) { .vote-chart__star:hover .vote-chart__dot { scale: 1.7; } }
.vote-chart__star:focus-visible { outline: 2px solid var(--brand-teal); outline-offset: 3px; }
.vote-chart__star.is-champion .vote-chart__dot {
  box-shadow: 0 0 26px rgba(255, 236, 190, 0.95), 0 0 60px hsl(var(--orb-h) 90% 62% / 0.8);
}
/* The tap target must clear 44px even when the star itself is 4px wide (§8.2). */
.vote-chart__star::after {
  content: '';
  position: absolute; top: 50%; left: 50%;
  width: 44px; height: 44px;
  transform: translate(-50%, -50%);
}
.vote-chart__legend {
  margin: 0;
  /* Even top and bottom, and tight: this is a caption under a picture, not a
     paragraph. Line-height comes down with the padding or the band stays tall
     however little padding it has. */
  padding: 7px 14px 8px;
  text-align: center;
  font: 500 9.9px/1.35 var(--font-thai);   /* 10% down from 11px, 2026-08-25 ask */
  color: var(--on-dark-dim);
}

/* ── Detail ──────────────────────────────────────────────────────────────── */
/* Compact, and part of the chart's frame rather than a box of its own: a hairline
   rule instead of a border, no radius, no second background. It was a 132px-tall
   centred stack; it is now two rows. */
.vote-detail-slot { border-top: 1px solid rgba(var(--white-rgb), 0.1); }
.vote-detail {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 2px 10px;
  padding: 10px 14px 11px;
  text-align: left;
}
.vote-detail__head { grid-row: 1; display: flex; align-items: center; gap: 6px; }
.vote-detail__rank   { font: 800 13px var(--font-latin); color: var(--brand-gold); }
.vote-detail__glyph  { display: inline-flex; font-size: 12px; }
.vote-detail__glyph img { width: 12px; height: 12px; }
.vote-detail__rarity { display: none; }        /* the glyph already says the tier */
.vote-detail__title  { grid-row: 1; font: 800 14px/1.25 var(--font-thai); color: var(--white);
                       overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vote-detail__latin  { display: none; }        /* the title is directly above it on the card */
/* justify-self: end on both. Column 3 is `auto`, so it is as wide as its WIDEST
   occupant — the "แพ้ให้ …" line — and the shorter points block was left-aligned
   inside that width, which reads as a stray middle column. */
.vote-detail__points {
  grid-row: 1; grid-column: 3;
  justify-self: end;
  display: flex; align-items: baseline; justify-content: flex-end; gap: 3px;
}
.vote-detail__points-num   { font: 800 18px var(--font-latin); color: var(--brand-teal); }
.vote-detail__points-label { font: 400 9px var(--font-latin); color: var(--on-dark-dim); }
.vote-detail__credit { grid-row: 2; grid-column: 2; font: 500 10px var(--font-thai); color: var(--on-dark-muted);
                       overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vote-detail__beaten { grid-row: 2; grid-column: 3; justify-self: end;
                       font: 500 10px var(--font-thai); color: var(--on-dark-dim);
                       text-align: right; white-space: nowrap; }

/* ── Ranked table ────────────────────────────────────────────────────────── */
.vote-result__section { font: 700 13px var(--font-thai); color: var(--on-dark-muted); text-align: center; }
/* `margin: 0` here would cancel `.vote-result > *`'s `margin-inline: auto` — equal
   specificity, and this rule comes later — leaving the table flush left while the
   heading above it stayed centred. Keep the block reset, keep the centring. */
.vote-table { list-style: none; margin: 0 auto; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.vote-table__row { margin: 0; }
.vote-table__btn {
  width: 100%;
  min-height: 48px;
  display: flex; align-items: center; gap: 10px;
  padding: 8px 12px;
  border: 1px solid rgba(var(--white-rgb), 0.08);
  border-radius: 12px;
  background: rgba(var(--white-rgb), 0.03);
  color: var(--on-dark-text);
  text-align: left;
  cursor: pointer;
}
@media (hover: hover) { .vote-table__btn:hover { background: rgba(var(--white-rgb), 0.07); } }
.vote-table__btn:focus-visible { outline: 2px solid var(--brand-teal); outline-offset: 2px; }
.vote-table__row.is-champion .vote-table__btn {
  border-color: rgba(var(--brand-gold-rgb), 0.5);
  background: rgba(var(--brand-gold-rgb), 0.1);
}
.vote-table__rank {
  flex: 0 0 26px;
  font: 700 12px var(--font-latin);
  color: var(--on-dark-dim);
  text-align: right;
}
.vote-table__glyph { flex: 0 0 auto; display: inline-flex; font-size: 13px; }
.vote-table__glyph img { width: 13px; height: 13px; }
.vote-table__mid { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.vote-table__title  { font: 700 13px var(--font-thai); color: var(--white); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vote-table__credit { font: 500 10px var(--font-thai); color: var(--on-dark-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Which nights a song was played (2026-08-25). Literal colours, like everything
   else in this file — it all paints on the fixed dark surface, see the warning at
   the top. Matches .vote-card__night so a chip means the same thing during the
   game and afterwards; kept as its own class because the card's version is
   positioned by the card band and this one sits inline beside a credit. */
.vote-nights { display: inline-flex; gap: 3px; flex: 0 0 auto; }
.vote-night {
  flex: 0 0 auto;
  padding: 1px 5px;
  border-radius: 4px;
  background: rgba(var(--white-rgb), 0.08);
  color: #9A9A9A;
  font: 700 9px var(--font-latin);
}
/* `min-width: 0` on both the row and the credit, or the credit's ellipsis never
   engages: a flex item's default min-width is auto, so a long credit pushes the
   row wider instead of truncating and the points column gets shoved off. */
.vote-table__sub { display: flex; align-items: center; gap: 6px; min-width: 0; }
.vote-table__sub .vote-table__credit { min-width: 0; }
/* Row 2, column 2 — the slot .vote-detail__credit itself used to occupy before it
   was wrapped in this row (2026-08-25). Without a placement the wrapper is
   auto-placed into COLUMN 1, and column 1 is `auto`, so a long credit stretched it
   and shoved the song title (column 2) away from the rank it belongs beside.
   `min-width: 0` on the row AND the credit, or the ellipsis never engages and the
   row widens the column instead of truncating. */
.vote-detail__credit-row {
  grid-row: 2; grid-column: 2;
  display: flex; align-items: center; gap: 6px; min-width: 0;
}
.vote-detail__credit-row .vote-detail__credit { min-width: 0; }
/* One line per night for the two songs whose lineup changed between nights. */
/* Same placement for the multi-night variant, which is a column of those rows.
   Its own children are flex items, not grid items, so they need none. */
.vote-detail__credits {
  grid-row: 2; grid-column: 2;
  display: flex; flex-direction: column; gap: 3px; min-width: 0;
}
.vote-detail__credits .vote-detail__credit-row { grid-row: auto; grid-column: auto; }
.vote-table__pts { flex: 0 0 auto; font: 800 14px var(--font-latin); color: var(--brand-teal); }

/* ── The podium ────────────────────────────────────────────────────────────
   The top three carry real weight instead of being three more rows with small
   numbers at the left. Keyed on `data-podium`, which the builders set from the
   RANK, not from the loop index — a three-way tie for first therefore puts all
   three up here rather than cutting the third off by position. Metal colours are
   literals: this is the fixed-dark result surface. */
.vote-table__row[data-podium] .vote-table__btn {
  min-height: 58px;
  padding: 10px 12px;
  border-width: 1px;
}
.vote-table__row[data-podium] .vote-table__rank {
  flex: 0 0 30px;
  font-size: 16px;
  font-weight: 800;
}
.vote-table__row[data-podium] .vote-table__title { font-size: 15px; }
.vote-table__row[data-podium] .vote-table__pts   { font-size: 17px; }

.vote-table__row[data-podium="1"] .vote-table__btn {
  border-color: rgba(var(--brand-gold-rgb), 0.65);
  background: linear-gradient(100deg, rgba(var(--brand-gold-rgb), 0.2), rgba(var(--brand-gold-rgb), 0.07));
  box-shadow: 0 0 22px rgba(var(--brand-gold-rgb), 0.16);
}
.vote-table__row[data-podium="1"] .vote-table__rank { color: var(--brand-gold-pale); }
.vote-table__row[data-podium="2"] .vote-table__btn {
  border-color: rgba(206, 214, 235, 0.42);
  background: linear-gradient(100deg, rgba(206, 214, 235, 0.13), rgba(206, 214, 235, 0.04));
}
.vote-table__row[data-podium="2"] .vote-table__rank { color: #D6DDF0; }
.vote-table__row[data-podium="3"] .vote-table__btn {
  border-color: rgba(214, 150, 100, 0.42);
  background: linear-gradient(100deg, rgba(214, 150, 100, 0.13), rgba(214, 150, 100, 0.04));
}
.vote-table__row[data-podium="3"] .vote-table__rank { color: #E0A87C; }

/* THE SELECTED STAR. Whatever the detail panel is currently describing, whether
   it was reached from the chart or from a row far down the table. Loud on purpose:
   the whole point is being findable among 51 dots at a glance.
   ⚠️ Drawn on the DOT, not on the button. The button carries only the pointer
   parallax; the DRIFT lives on the dot — so a ring on the button stays put while
   the star it is supposed to be marking wanders out of it. As a child of the dot
   it inherits that drift and travels with the star. */
.vote-chart__star.is-selected { z-index: 5; }
/* A GLOW, not an outline (2026-08-25 ask — the ring was tried first and replaced).
   A hard circle drew a boundary, and on a field of 51 dots a boundary invites the
   eye to ask what is inside it: a neighbour 24px away fell within the first
   version's 26px ring and read as the marked star. Light has no edge to be inside
   of. It also suits the subject — the thing being pointed at is a star.

   Built as a radial gradient on a pseudo rather than an animated box-shadow: only
   `transform` and `opacity` move, which the compositor handles without repainting
   51 elements every frame. */
.vote-chart__star.is-selected .vote-chart__dot::before {
  content: '';
  position: absolute; top: 50%; left: 50%;
  width: 24px; height: 24px;
  margin: -12px 0 0 -12px;
  border-radius: 50%;
  /* The star's OWN hue, not a fixed teal — --orb-h is a song's identity colour and
     a teal halo repainted every selection in someone else's colour. Soft: this
     marks a star, it does not replace it, and the dot at the centre already
     brightens on its own. */
  background: radial-gradient(circle,
    rgba(var(--white-rgb), 0.4) 0%,
    hsl(var(--orb-h) 100% 88% / 0.34) 20%,
    hsl(var(--orb-h) 95% 62% / 0.22) 42%,
    hsl(var(--orb-h) 90% 58% / 0.09) 64%,
    transparent 80%);
  filter: blur(1.5px);
  animation: vote-selected-glow 2.2s ease-in-out infinite;
  pointer-events: none;
}
@keyframes vote-selected-glow {
  0%, 100% { transform: scale(0.88); opacity: 0.6; }
  50%      { transform: scale(1.12); opacity: 0.95; }
}
/* The star at the centre of it stops being dim, whatever its score — a halo over a
   0.3-opacity dot lights up an empty patch of sky. */
.vote-chart__star.is-selected .vote-chart__dot {
  scale: 1.3;
  opacity: 1;
  box-shadow: 0 0 6px rgba(var(--white-rgb), 0.8), 0 0 14px hsl(var(--orb-h) 90% 62% / 0.8);
}

/* On the chart. The three best stars get a ring rather than just more brightness —
   past a certain point every bright dot looks like every other bright dot, and
   the whole job here is being findable. */
.vote-chart__star[data-podium] .vote-chart__dot::after {
  content: '';
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  border: 1.5px solid currentColor;
  opacity: 0.75;
  animation: vote-podium-ring 2.8s ease-in-out infinite;
}
.vote-chart__star[data-podium="1"] .vote-chart__dot { color: var(--brand-gold-pale); }
.vote-chart__star[data-podium="2"] .vote-chart__dot { color: #D6DDF0; }
.vote-chart__star[data-podium="3"] .vote-chart__dot { color: #E0A87C; }
@keyframes vote-podium-ring {
  0%, 100% { transform: scale(1);    opacity: 0.75; }
  50%      { transform: scale(1.28); opacity: 0.25; }
}

/* ── Actions ─────────────────────────────────────────────────────────────── */
.vote-result__actions { display: flex; gap: 8px; margin-top: 6px; }
/* .vote-secondary is defined on the entry screen against theme tokens; on this
   fixed-dark surface it needs literal values or it flips to dark-on-dark in
   light mode — the exact failure CLAUDE.md's fixed-surface rule describes. */
.vote-result__actions .vote-secondary {
  border-color: rgba(var(--white-rgb), 0.14);
  background: rgba(var(--white-rgb), 0.05);
  color: var(--on-dark-text);
  display: flex; align-items: center; justify-content: center;
  text-decoration: none;
}
@media (hover: hover) { .vote-result__actions .vote-secondary:hover { background: rgba(var(--white-rgb), 0.1); } }
/* Was #656A83, which measured 3.74:1 here — below the 4.5 an 11px line needs. */
.vote-result__note { margin: 0 auto; text-align: center; font: 500 11px var(--font-thai); color: var(--on-dark-muted); }

@media (prefers-reduced-motion: reduce) {
  .vote-chart__star, .vote-chart__dot { transition: none; }
}

@media (min-width: 700px) {
  .vote-result > * { max-width: 640px; }
  .vote-chart__sky { aspect-ratio: 4 / 3; }
}

/* ── Share (§F, 2026-08-25) ─────────────────────────────────────────────── */
/* The share row itself is share.js's shared component — never a second one. All
   that belongs here is the frame around it. */
.vote-share {
  display: flex; flex-direction: column; align-items: center; gap: 9px;
  padding: 14px 12px;
  border-radius: 18px;
  border: 1px solid rgba(var(--white-rgb), 0.1);
  background: rgba(var(--white-rgb), 0.04);
}
.vote-share__label { font: 700 12px var(--font-thai); color: var(--on-dark-soft); text-align: center; }

/* ── Submit status ───────────────────────────────────────────────────────── */
/* Deliberately quiet. The chart above it is the thing the player earned; this is
   bookkeeping, and it must never read as an error that undid the result. */
.vote-submit {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 10px 12px;
  border-radius: 14px;
  border: 1px solid rgba(var(--white-rgb), 0.08);
  background: rgba(var(--white-rgb), 0.03);
}
.vote-submit__line { margin: 0; text-align: center; font: 500 11.5px/1.6 var(--font-thai); color: var(--on-dark-muted); }
.vote-submit[data-tone="ok"]   { border-color: rgba(var(--brand-teal-rgb), 0.32); }
.vote-submit[data-tone="ok"]   .vote-submit__line { color: #E0E0E0; }
.vote-submit[data-tone="warn"] { border-color: rgba(var(--brand-gold-rgb), 0.4); }
.vote-submit[data-tone="warn"] .vote-submit__line { color: var(--brand-gold-pale); }
.vote-submit__retry {
  min-height: 40px; padding: 8px 18px;
  border-radius: 999px;
  border: 1px solid rgba(var(--white-rgb), 0.2);
  background: rgba(var(--white-rgb), 0.05);
  color: var(--on-dark-text);
  font: 600 12px var(--font-thai);
  cursor: pointer;
}
@media (hover: hover) { .vote-submit__retry:hover { background: rgba(var(--white-rgb), 0.1); } }
.vote-submit__retry:focus-visible { outline: 3px solid var(--brand-teal); outline-offset: 3px; }
.vote-submit__verify { width: 100%; max-width: 320px; }

/* ── Viewer bar (someone else's shared chart) ────────────────────────────── */
/* Fixed to the bottom edge, and it pads its own safe-area inset rather than
   letting the home indicator sit on top of the button — CLAUDE.md's second
   safe-area pattern (a floating control ADDS the inset to its offset). */
.vote-viewbar {
  position: fixed;
  left: 0; right: 0; bottom: var(--inapp-bottom);
  z-index: 60;
  display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
  gap: 10px 14px;
  padding: 12px calc(16px + var(--safe-right)) calc(12px + var(--safe-bottom)) calc(16px + var(--safe-left));
  border-top: 1px solid rgba(var(--white-rgb), 0.12);
  background: rgba(6, 6, 20, 0.92);
  backdrop-filter: blur(10px);
}
.vote-viewbar__text { font: 700 13px var(--font-thai); color: var(--on-dark-text); text-align: center; }
.vote-viewbar__cta {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 42px;
  padding: 10px 20px;
  border-radius: 999px;
  background: linear-gradient(120deg, var(--cta-grad-hi), var(--cta-grad-lo));
  color: var(--brand-gold-ink);
  font: 700 13px var(--font-thai);
  text-decoration: none;
}
@media (hover: hover) { .vote-viewbar__cta:hover { filter: brightness(1.06); } }
.vote-viewbar__cta:focus-visible { outline: 3px solid var(--brand-teal); outline-offset: 3px; }
/* The bar covers the end of the page, so the page has to end above it. */
body:has(.vote-viewbar) #vote-root { padding-bottom: calc(96px + var(--inapp-bottom)); }

/* ── Everyone's combined tables (§5.3) ───────────────────────────────────── */
/* Still the fixed-dark result surface: literals only. */
.vote-global-slot { display: flex; flex-direction: column; gap: 10px; }
/* Height reservation only — the centring comes from `.ppw-loading`, which this
   is always paired with. Same reasoning as the Turnstile verify step: the
   loading state and the thing that replaces it should occupy the same box, so
   the swap is a content change rather than the modal resizing under the cursor. */
.vote-global-loading { min-height: 240px; }
/* The popup's own rhythm. It used to inherit the modal card's padding and stack
   everything on a flat 10px, so the head count, the footnote, the tabs and the
   table all read as one undifferentiated column. */
.vote-global {
  display: flex; flex-direction: column;
  gap: 14px;
  /* No horizontal padding of its own — .my-modal__scroll around it already
     carries 20px a side. Adding more here would inset the table twice. */
  padding: 2px 0 4px;
}
.vote-global .vote-table { margin-top: 2px; }
/* No frame. It was boxed to give it presence and read as a panel competing with
   the table underneath; the head count carries itself at this size. */
.vote-global__meta {
  text-align: center;
  padding: 2px 4px 0;
  font: 800 17px/1.35 var(--font-thai);
  color: var(--white);
}
/* Its own line. Inline it ran straight into the head count with no separator —
   "1,284 คนจัดอันดับแล้วคะแนนรวมทั้งหมด…" reads as one broken sentence. */
.vote-global__meta-sub {
  display: block;
  margin-top: 6px;
  font: 500 11px/1.6 var(--font-thai);
  color: var(--on-dark-muted);
}
.vote-global__weighting { margin: -4px 0 0; text-align: center; font: 500 10.5px/1.6 var(--font-thai); color: var(--on-dark-dim); }
.vote-global__tabs { display: flex; flex-wrap: wrap; justify-content: flex-start; gap: 6px; }
/* 10% down from 40px / 8-14px / 12px, on the 2026-08-25 ask. Still clears a
   comfortable tap target at 36px. */
.vote-global__tab {
  min-height: 32px;
  padding: 6px 12px;
  border: 1px solid rgba(var(--white-rgb), 0.12);
  border-radius: 999px;
  background: rgba(var(--white-rgb), 0.04);
  color: var(--on-dark-soft);
  font: 700 9.7px var(--font-thai);
  cursor: pointer;
}
/* Search across Thai / English / romanisation. Literal colours like everything
   else in this file — it paints on the fixed dark surface. */
.vote-global__search {
  width: 100%;
  padding: 9px 12px;
  border: 1px solid rgba(var(--white-rgb), 0.12);
  border-radius: 10px;
  background: rgba(var(--white-rgb), 0.04);
  color: var(--on-dark-text);
  font: 500 13px var(--font-thai);
}
.vote-global__search::placeholder { color: var(--on-dark-dim); }
.vote-global__search:focus-visible { outline: 2px solid rgba(var(--brand-teal-rgb), 0.6); outline-offset: 1px; }
@media (hover: hover) { .vote-global__tab:hover { background: rgba(var(--white-rgb), 0.09); } }
.vote-global__tab:focus-visible { outline: 2px solid var(--brand-teal); outline-offset: 2px; }
.vote-global__tab.is-on {
  border-color: rgba(var(--brand-teal-rgb), 0.6);
  background: rgba(var(--brand-teal-rgb), 0.16);
  color: #E0E0E0;
}
/* The unit sits at normal weight beside a bold number — it labels the value, it
   is not part of it. */
.vote-table__unit {
  margin-left: 3px;
  font: 400 9px var(--font-latin);
  letter-spacing: 0.02em;
  opacity: 0.7;
}
/* One explanation, under both tables. */
.vote-hint {
  /* auto, not 0: .vote-result's children are centred by `margin-inline: auto`,
     and a `margin: 10px 0 0` here overrode that and pinned it left. Same trap the
     result table hit. */
  margin: 10px auto 0;
  max-width: 34ch;
  text-align: center;
  font: 500 11px/1.7 var(--font-thai);
  color: var(--on-dark-muted);
}

.vote-global__empty { margin: 0; text-align: center; font: 600 14px/1.7 var(--font-thai); color: var(--on-dark-text); padding: 22px 12px 4px; }
.vote-global__empty-sub { margin: 0; text-align: center; font: 500 12px/1.7 var(--font-thai); color: var(--on-dark-muted); padding: 0 12px 22px; }
/* The combined table's rows are read-only, so they are <div>s rather than
   buttons — .vote-table__btn styles them, but the hover/pointer affordance would
   promise an interaction that does not exist. */
.vote-global .vote-table__btn { cursor: default; }
@media (hover: hover) { .vote-global .vote-table__btn:hover { background: rgba(var(--white-rgb), 0.03); } }
.vote-table__diff {
  flex: 0 0 auto;
  min-width: 34px;
  text-align: right;
  font: 700 11px var(--font-latin);
}
.vote-table__diff[data-dir="up"]   { color: var(--status-green); }
.vote-table__diff[data-dir="down"] { color: #E08B6B; }
.vote-table__diff[data-dir="same"] { color: var(--on-dark-muted); }

/* ══════════════════════════════════════════════════════════════════════════
   IMMERSIVE SURFACE — background fix (F1)
   ══════════════════════════════════════════════════════════════════════════

   The reported "bg ทับซ้อนกัน" was two separate stacking problems, both measured:

   1. <body> carries the site-wide --space-gradient: THIRTEEN layers, every one
      `background-attachment: fixed`. The game and result surfaces then painted
      their own radial glows on top of it, so two independent sets of fixed
      gradients showed through each other as blotches.
   2. The legal footer is a SIBLING of the dark surface inside #vote-root, and it
      has no background of its own — so the bottom of the page was painted by the
      body gradient, leaving a hard seam right under the dark panel.

   Fix: while an immersive screen is up, stop the body gradient entirely and paint
   ONE ground on #vote-root, footer included. Overlaying was the bug; the answer is
   not to overlay. */
body.vote-immersive {
  background-image: none;
  background-color: var(--fixed-bg-0);
}
body.vote-immersive #vote-root {
  background:
    radial-gradient(ellipse 120% 70% at 50% 0%, rgba(var(--brand-teal-rgb), 0.09), transparent 62%),
    radial-gradient(ellipse 120% 60% at 50% 100%, rgba(var(--brand-gold-rgb), 0.09), transparent 62%),
    var(--fixed-bg-0);
}
/* The footer now sits ON the dark ground, so its own text has to be legible there
   rather than against the site background it was designed for. */
body.vote-immersive #vote-root > .lg-footer-link a { color: var(--on-dark-muted); }

/* .vote-result no longer paints its own ground — #vote-root does, once, for the
   whole page. Two grounds is what produced the seam. */
.vote-result { background: none; }

/* ── Ambient starfield (F1) ──────────────────────────────────────────────── */
/* Deliberately cheap: ONE element, three repeating-radial layers, no DOM per star,
   and only `opacity` animates. It is decorative and aria-hidden. */
.vote-stars {
  /* absolute, not fixed: it belongs to the surface it is mounted into, so it
     cannot be hidden by that surface own opaque background. */
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(1.2px 1.2px at 20% 30%, rgba(var(--white-rgb),0.55), transparent 100%),
    radial-gradient(1px 1px at 70% 18%, rgba(var(--white-rgb),0.4), transparent 100%),
    radial-gradient(1.4px 1.4px at 44% 72%, rgba(var(--white-rgb),0.5), transparent 100%),
    radial-gradient(1px 1px at 86% 62%, rgba(var(--brand-teal-rgb),0.45), transparent 100%),
    radial-gradient(1.2px 1.2px at 12% 84%, rgba(200,155,255,0.45), transparent 100%),
    radial-gradient(1px 1px at 58% 44%, rgba(var(--white-rgb),0.35), transparent 100%),
    radial-gradient(1.3px 1.3px at 32% 12%, rgba(var(--brand-gold-rgb),0.4), transparent 100%),
    radial-gradient(1px 1px at 92% 34%, rgba(var(--white-rgb),0.4), transparent 100%);
  background-repeat: repeat;
  background-size: 340px 300px, 420px 380px, 500px 460px, 380px 340px, 460px 420px, 300px 280px, 520px 480px, 360px 320px;
  animation: vote-stars-breathe 7s ease-in-out infinite;
}
@keyframes vote-stars-breathe {
  0%, 100% { opacity: 0.5; }
  50%      { opacity: 0.9; }
}
/* Everything the page draws must sit above the starfield.
   .vote-game is DELIBERATELY excluded from this list: it is `position: fixed;
   inset: 0`, and putting it in a `position: relative` rule collapsed it to a
   content-height block — the whole arena bunched up at the top of the screen with
   the starfield showing underneath. It already outranks the starfield via its own
   z-index; it needs nothing from here. */
.vote-result, .vote-page, .vote-gate-shell { position: relative; z-index: 1; }
/* the surfaces that HOST a starfield need a positioning context for it */
#vote-root, .vote-game { position: relative; }
.vote-game { position: fixed; }

/* ── Motion (F1): cards arrive, they don't just appear ──────────────────── */
.vote-card { animation: vote-card-in 0.34s cubic-bezier(0.22, 1, 0.36, 1) both; }
/* nth-of-TYPE, not nth-child: the VS <div> now sits between the two <button>
   cards, so nth-child(2) is the mark and the second card never got its delay. */
.vote-card:nth-of-type(2) { animation-delay: 0.06s; }
@keyframes vote-card-in {
  from { opacity: 0; transform: translateY(14px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}
.vote-inter { animation: vote-inter-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; }
@keyframes vote-inter-in {
  from { opacity: 0; transform: translateY(18px) scale(0.98); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .vote-stars { animation: none; opacity: 0.7; }
  .vote-card, .vote-inter { animation: none; }
}

/* Back button in the game head (F1). Sits between the progress meta and the exit
   ✕ so the two destructive-ish controls are not adjacent. */
.vote-gamehead__back {
  flex: 0 0 auto;
  min-height: 40px;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 14px;
  border: 1px solid rgba(var(--white-rgb), 0.12);
  border-radius: 999px;
  background: rgba(var(--white-rgb), 0.05);
  color: var(--on-dark-soft);
  font: 700 12px var(--font-thai);
  cursor: pointer;
}
@media (hover: hover) { .vote-gamehead__back:hover:not(:disabled) { background: rgba(var(--white-rgb), 0.1); color: var(--on-dark-text); } }
.vote-gamehead__back:focus-visible { outline: 2px solid var(--brand-teal); outline-offset: 2px; }
.vote-gamehead__back:disabled { opacity: 0.32; cursor: default; }
.vote-gamehead__back-arrow { display: inline-flex; font-size: 11px; }

/* The login option on the entry screen reuses .vote-guest's dashed treatment, but
   it is an <a>, so it needs the link resets a <button> got for free. */
a.vote-guest { text-decoration: none; }

/* Sector interstitial now names whose block it is. */
.vote-inter__crew {
  font: 700 11px var(--font-thai);
  color: var(--brand-gold);
  letter-spacing: 0.02em;
}

/* ══════════════════════════════════════════════════════════════════════════
   F2 — number badges, the processing beat, and the onboarding diagrams
   ══════════════════════════════════════════════════════════════════════════ */

/* The number the keyboard shortcut refers to, ON the card. Without it the hint
   "or press a number key" named a mapping that was nowhere on screen. */
/* Top-RIGHT corner, floating just outside the card.
   Centred on the top edge it sat exactly where the VS mark sits once the cards
   stack on a phone, and the two collided. The corner is the one spot that is free
   in both layouts: the VS is always horizontally centred, and the D1/D2/D3 chips
   live INSIDE the card below this. */
.vote-card__num {
  position: absolute;
  top: -14px; right: -14px;
  z-index: 3;
  min-width: 26px; height: 26px;
  padding: 0 7px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px;
  border: 1px solid rgba(var(--white-rgb), 0.2);
  background: var(--fixed-bg-2);   /* was PPW navy #14162A (2026-10-22) */
  color: var(--on-dark-muted);
  font: 800 12px var(--font-latin);
}
/* Card 1 only. `right: auto` is required, not decorative: the base rule sets
   `right`, and setting `left` alone would pin the badge by BOTH edges and stretch
   it across the card's whole width. */
.vote-card__num--left { left: -14px; right: auto; }
.vote-card:focus-visible .vote-card__num { border-color: rgba(var(--brand-teal-rgb), 0.7);
  color: #E0E0E0; }
@media (hover: hover) { .vote-card:hover .vote-card__num { border-color: rgba(var(--brand-teal-rgb), 0.7);
  color: #E0E0E0; } }
.vote-card.is-chosen .vote-card__num { border-color: rgba(var(--brand-gold-rgb), 0.9); color: var(--brand-gold-pale); }

/* ── Processing beat ─────────────────────────────────────────────────────── */
.vote-processing {
  display: flex; flex-direction: column; align-items: center; gap: 20px;
  text-align: center;
  padding: 40px 28px;
  cursor: pointer;
}
.vote-processing__dial { position: relative; width: 84px; height: 84px; }
.vote-processing__ring {
  position: absolute; inset: 0;
  border-radius: 50%;
  border: 1px solid rgba(var(--brand-teal-rgb), 0.35);
  border-top-color: rgba(var(--brand-teal-rgb), 0.95);
  animation: vote-spin 1.5s linear infinite;
}
.vote-processing__ring--2 {
  inset: 12px;
  border-color: rgba(var(--brand-gold-rgb), 0.28);
  border-top-color: rgba(var(--brand-gold-rgb), 0.9);
  animation-duration: 2.1s;
  animation-direction: reverse;
}
.vote-processing__ring--3 {
  inset: 24px;
  border-color: rgba(255, 107, 102, 0.28);
  border-top-color: rgba(255, 107, 102, 0.9);
  animation-duration: 2.8s;
}
.vote-processing__core {
  position: absolute; top: 50%; left: 50%;
  width: 12px; height: 12px; margin: -6px;
  border-radius: 50%;
  background: #FFF3D6;
  box-shadow: 0 0 18px rgba(255, 243, 214, 0.9);
  animation: vote-core-pulse 1.5s ease-in-out infinite;
}
@keyframes vote-spin { to { transform: rotate(360deg); } }
@keyframes vote-core-pulse {
  0%, 100% { transform: scale(1);   opacity: 0.85; }
  50%      { transform: scale(1.5); opacity: 1; }
}
.vote-processing__line { font: 800 18px/1.4 var(--font-thai); color: var(--white); }
.vote-processing__sub  { font: 500 12px var(--font-thai); color: var(--on-dark-muted); }

/* ── Onboarding: the journey funnel ───────────────────────── */
.vote-howto__art {
  flex: 0 0 auto;
}
/* The one region allowed to give. Everything else in the card is pinned. */
.vote-howto__art--wide {
  width: 100%; height: auto;
  flex: 0 1 auto; min-height: 0;
  display: flex;
  overflow: hidden;
  border-radius: 0; background: none; padding: 0;
}
/* WHAT gives inside it matters. The first version scrolled the whole art region,
   which pushed the sample card — the very thing the slide is pointing at — up out
   of view, so the reader met a clipped diagram and two captions about corners they
   could not see. Only the LEGEND scrolls now; the diagram above it is always
   whole. */
.vote-anatomy { min-height: 0; }
/* The worked example. No frame of its own — the frame is .vote-anatomy above.
   The sample card keeps its border so it still reads as a card sitting on the
   panel rather than merging into it. */
.vote-anatomy__demo {
  flex: 0 0 auto;
  display: flex; flex-direction: column; gap: 6px;
}
.vote-anatomy__card, .vote-anatomy__hints { flex: 0 0 auto; }
.vote-anatomy__legend {
  flex: 0 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--surface-rgb), 0.3) transparent;
}
/* A region that scrolls with no sign that it scrolls is a region whose bottom rows
   do not exist. Set from paint() in vote.js — CSS cannot ask whether an element
   overflows. */
.is-scrollable {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent 100%);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent 100%);
}
.vote-howto__more {
  flex: 0 0 auto;
  margin: 0;
  font: 600 10px var(--font-thai);
  color: var(--accent-teal-text);
  letter-spacing: 0.02em;
}

/* Short screens: shrink before anything has to scroll. 700px is where the anatomy
   slide stopped fitting in the measurements above. */
@media (max-height: 700px) {
  .vote-howto__card   { padding: 22px 20px 18px; gap: 9px; }
  .vote-howto__title  { font-size: 17px; }
  .vote-howto__body   { font-size: 12px; line-height: 1.6; }
  .vote-howto__art    { width: 64px; height: 64px; }
  .vote-howto__emoji  { font-size: 32px; }
  .vote-howto__emoji img { width: 32px; height: 32px; }
  .vote-anatomy       { gap: 9px; padding: 10px; }
  .vote-anatomy__demo { gap: 7px; }
  .vote-anatomy__legend { padding-top: 9px; gap: 7px; }
  .vote-anatomy__card { padding: 10px 12px 12px; gap: 6px; }
  .vote-anatomy__legend { gap: 6px; padding-top: 8px; }
  /* The orb in the DIAGRAM is a plain decorative circle — the slide is explaining
     the two corner badges, and the orb is not one of them. On a short screen it is
     46px of height spent on nothing, so it goes before anything readable does. */
  .vote-anatomy__orb  { display: none; }
  .vote-journey__step { padding: 9px 4px 8px; }
  .vote-journey__num  { font-size: 19px; }
}
.vote-journey {
  display: flex; align-items: stretch; justify-content: center; gap: 5px;
  width: 100%;
}
.vote-journey__step {
  flex: 1 1 0;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 12px 4px 10px;
  border-radius: 14px;
  background: rgba(var(--surface-rgb), 0.05);
  border: 1px solid var(--border);
}
.vote-journey__step.is-final {
  background: rgba(var(--accent-gold-rgb), 0.14);
  border-color: rgba(var(--accent-gold-rgb), 0.5);
}
.vote-journey__num   { font: 800 22px/1 var(--font-latin); color: var(--text-primary); }
.vote-journey__label { font: 700 7px var(--font-latin); letter-spacing: 0.06em; color: var(--accent-teal-text); }
.vote-journey__note  { font: 500 9px var(--font-thai); color: var(--text-secondary); }
.vote-journey__arrow { align-self: center; color: var(--text-secondary); font-size: 13px; }

/* ── Onboarding: card anatomy ─────────────────────────── */
/* Rebuilt for legibility: the sample card is bigger and clearly a card, the two
   callouts are labelled rather than just outlined, and the legend is a real
   two-column list instead of a run-on row. */
/* ONE outer box holding both halves — the worked example AND what each part
   means. Two separate boxes were tried first and read as two unrelated panels;
   this is one explanation, so it gets one frame, with a hairline rule inside it
   separating the example from the key. */
.vote-anatomy {
  display: flex; flex-direction: column; gap: 12px;
  width: 100%; text-align: left;
  padding: 10px;
  border-radius: 18px;
  border: 1px solid var(--border);
  background: rgba(var(--surface-rgb), 0.05);
}

.vote-anatomy__card {
  position: relative;
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  padding: 10px 12px 12px;
  border-radius: 16px;
  border: 1px solid var(--border);
  background: rgba(var(--surface-rgb), 0.06);
}
.vote-anatomy__band { width: 100%; display: flex; align-items: center; }
.vote-anatomy__badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(var(--accent-teal-rgb), 0.14);
  border: 1px solid rgba(var(--accent-teal-rgb), 0.6);
  font: 700 10px var(--font-latin);
  color: var(--text-primary);
}
.vote-anatomy__nights { margin-left: auto; display: flex; gap: 3px; }
.vote-anatomy__night {
  padding: 2px 7px;
  border-radius: 5px;
  background: rgba(var(--accent-gold-rgb), 0.16);
  border: 1px solid rgba(var(--accent-gold-rgb), 0.6);
  color: var(--text-primary);
  font: 700 10px var(--font-latin);
}
.vote-anatomy__orb {
  width: 46px; height: 46px;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, #8fd7ff, #3a6ea8 60%, #1b2f4d);
}
.vote-anatomy__title { font: 800 14px var(--font-thai); color: var(--text-primary); }

/* Small captions pointing at each callout, so the dashed outline is not the only
   thing telling you what to look at. */
.vote-anatomy__hints { display: flex; justify-content: space-between; gap: 10px; }
.vote-anatomy__hint {
  flex: 1 1 0;
  font: 600 10px/1.4 var(--font-thai);
  color: var(--text-secondary);
}
.vote-anatomy__hint:last-child { text-align: right; }
.vote-anatomy__hint b { color: var(--accent-teal-text); font-weight: 700; }
.vote-anatomy__hint.is-gold b { color: var(--accent-gold-text); }

/* A LIST inside a box. The LIST part is what stopped this slide overflowing a
   phone — no per-row border, no fixed key column, each entry one wrapping line
   (see the note in buildCardAnatomy()). The BOX is what makes it read as one
   group, matching the worked example above it: two solid blocks, clearly
   separate, rather than a boxed diagram followed by four loose lines. */
.vote-anatomy__legend {
  list-style: none; margin: 0;
  display: flex; flex-direction: column; gap: 7px;
  padding: 12px 0 0;
  border-top: 1px solid var(--border);
}
.vote-anatomy__row {
  display: flex; align-items: baseline; gap: 8px;
  padding: 0;
}
/* A round mark has no baseline worth aligning to — centre these two rows. */
.vote-anatomy__row:has(.vote-anatomy__orbmini) { align-items: center; }
/* currentColor on a hand-drawn dingbat resolves to whatever it inherits — inside
   this panel that was the near-black card ink, so BINARY rendered as a black
   spark next to two full-colour emoji. Each tier now states its own colour. */
/* The mark on the row that explains the orb. A plain still sphere on purpose: a
   miniature carrying its own rings and orbiting dots was tried and read as a
   second thing to decipher rather than a label for the sentence beside it. */
.vote-anatomy__orbmini {
  position: relative;
  flex: 0 0 18px;
  width: 18px; height: 18px;
  /* No vertical margin: the row already centres it, and 3px top+bottom pushed the
     legend's scrollHeight 1-2px past its clientHeight — enough for a desktop
     scrollbar to appear on a list that fits perfectly well. */
  margin: 0 8px 0 0;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 30%,
    hsl(190 95% 78%), hsl(205 80% 52%) 55%, hsl(220 65% 22%));
}

.vote-anatomy__glyph { display: inline-flex; font-size: 15px; flex: 0 0 18px; justify-content: center; }
.vote-anatomy__glyph img { width: 15px; height: 15px; }
.vote-anatomy__row[data-rarity=star]   .vote-anatomy__glyph { color: var(--accent-gold-text); }
.vote-anatomy__row[data-rarity=binary] .vote-anatomy__glyph { color: var(--accent-teal); }
.vote-anatomy__row[data-rarity=comet]  .vote-anatomy__glyph { color: #A970E8; }
.vote-anatomy__key  { flex: 0 0 auto; font: 700 10px var(--font-latin); color: var(--text-primary); letter-spacing: 0.02em; }
.vote-anatomy__desc { flex: 1 1 auto; font: 500 11px/1.45 var(--font-thai); color: var(--text-secondary); }
.vote-anatomy__row .vote-anatomy__night { flex: 0 0 auto; }

/* ── Star chart reveal (F2) ──────────────────────────────────────────────── */
/* Stars light up one after another, so the chart is drawn rather than just
   present. Staggered per index by an inline --n set in vote-result.js. */
/* `backwards`, not `both`: the fill only has to hold opacity at 0 through the
   stagger delay. A forwards fill would freeze `scale` at the animation's end and
   the hover rule below it would then have nothing to grow. */
.vote-chart__dot {
  animation:
    vote-star-in 0.5s ease-out backwards,
    vote-drift var(--drift-time, 9s) ease-in-out infinite alternate;
  animation-delay: calc(var(--i, 0) * 22ms), var(--drift-delay, 0s);
}
@keyframes vote-star-in {
  from { opacity: 0; scale: 0.2; }
}
/* Drift. Distance, period and a NEGATIVE start delay all come from the song's
   frozen orbSeed, so every star is at a different point of its own cycle and the
   field never breathes in unison — that would read as a loading spinner, not a
   sky. The travel is single-digit pixels on purpose. */
@keyframes vote-drift {
  from { translate: calc(var(--drift-x) * -1) calc(var(--drift-y) * -1); }
  to   { translate: var(--drift-x) var(--drift-y); }
}
.vote-chart__star.is-champion .vote-chart__dot {
  animation:
    vote-star-in 0.5s ease-out backwards,
    vote-drift var(--drift-time, 9s) ease-in-out infinite alternate,
    vote-champ-pulse 2.6s ease-in-out infinite;
  animation-delay: calc(var(--i, 0) * 22ms), var(--drift-delay, 0s), 1.2s;
}
@keyframes vote-champ-pulse {
  0%, 100% { box-shadow: 0 0 26px rgba(255, 236, 190, 0.95), 0 0 60px hsl(var(--orb-h) 90% 62% / 0.8); }
  50%      { box-shadow: 0 0 40px rgba(255, 236, 190, 1),    0 0 90px hsl(var(--orb-h) 90% 62% / 0.95); }
}

@media (prefers-reduced-motion: reduce) {
  .vote-processing__ring, .vote-processing__core { animation: none; }
  .vote-chart__dot, .vote-chart__star.is-champion .vote-chart__dot { animation: none; }
  .vote-chart__nebula, .vote-chart__nebula::after { animation: none; }
  .vote-chart__star[data-podium] .vote-chart__dot::after { animation: none; }
  .vote-chart__star.is-selected .vote-chart__dot::before { animation: none; }
}

/* The how-to art box grows for the two diagram slides; the emoji slides keep the
   round tinted disc. */
.vote-howto__art--wide {
  width: 100%;
  height: auto;
  min-height: 0;
  border-radius: 0;
  background: none;
}
.vote-howto__card { max-width: 400px; }

/* Every screen that waits for input now says which key advances it — the cards
   advertise a number, so an interstitial that silently accepted Enter was an
   inconsistency. */
.vote-inter__hint {
  margin: 8px 0 0;
  font: 500 10px var(--font-thai);
  color: var(--on-dark-dim);
}
.vote-processing__hint {
  margin-top: 4px;
  font: 500 10px var(--font-thai);
  color: var(--on-dark-dim);
}

/* ══════════════════════════════════════════════════════════════════════════
   LIGHT MODE (added 2026-08-25)

   The boundary is the PLATE, not the page. Every star on this feature already
   sits on a surface that paints its own dark background — the song card and the
   champion box (var(--fixed-bg-2)), the star map (#0A0A0A), the sector interstitial — so
   the page underneath them is free to be light without a single star colour
   changing. Measured, because the alternative was tempting and wrong: a star is
   `hsl(H 90% 62%)`, and directly on a light ground its hues read 1.01 / 1.06 /
   1.23 / 2.54 / 3.40 : 1. On its own plate it stays 4.7-16.1 : 1.

   So: plates stay dark, everything around them follows the theme.

   The one exception is the decorative starfield, which has no plate. The site
   already answered that in 2026-08-08: --star-rgb is white in dark and GOLD in
   light, created precisely so a decorative star reads as warm sparkle rather
   than merely visible. This file was drawing them as literal white.

   Ground: --bg-secondary, not --bg-primary. In dark this page sits one step
   deeper than the rest of the site (var(--fixed-bg-0) vs var(--fixed-bg-1)) to say "full screen";
   light keeps that relationship by sitting one step softer (#DBD4E9 vs
   #E7E2F0) — which is also the owner's ask: light, not glaring.

   No colour here is invented. Everything points at a token that has already
   been tuned, except three podium numbers and one arrow that have no token at
   all; those carry a measured contrast figure in a comment.

   Additive only — no base rule is edited, so dark mode resolves exactly as it
   did before (asserted by diffing computed styles across 2,049 elements).
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 1. The combined-ranking modal, in light mode ───────────────────────── */

/* White alpha tints are invisible on a light card. --surface-rgb is the token
   that flips those channels to dark ink, and is how every other stylesheet
   here writes a self-tint. */
:root[data-theme="light"] .vote-global-slot .vote-table__btn {
  border-color: rgba(var(--surface-rgb), 0.08);
  background: rgba(var(--surface-rgb), 0.03);
  color: var(--text-primary);
}
@media (hover: hover) { :root[data-theme="light"] .vote-global-slot .vote-table__btn:hover { background: rgba(var(--surface-rgb), 0.07); } }
:root[data-theme="light"] .vote-global-slot .vote-table__rank,
:root[data-theme="light"] .vote-global-slot .vote-table__credit,
:root[data-theme="light"] .vote-global-slot .vote-table__diff[data-dir="same"] {
  color: var(--text-secondary);
}
:root[data-theme="light"] .vote-global-slot .vote-table__title {
  color: var(--text-primary);
}
/* The points are an identity colour used as bare text — the site already has a
   paired -text token for exactly this (the same split as tierColor vs
   tierTextColor). The fill/border uses keep the vivid literal. */
:root[data-theme="light"] .vote-global-slot .vote-table__pts,
:root[data-theme="light"] .vote-global-slot .vote-table__unit {
  color: var(--accent-teal-text);
}
/* Not var(--success-text): the site token measures 4.24 against a podium row's
   own tint (rgb(234,230,243)), not the flat card it was tuned on. Darkened here
   only, in the light block, so the token itself is untouched. 5.32 tinted. */
:root[data-theme="light"] .vote-global-slot .vote-table__diff[data-dir="up"] {
  color: #1B6B3E;
}
/* No site token for this one — a light value that exists only in a light block
   is the documented shape (LIGHT_MODE_COLOR_BIBLE §7). */
:root[data-theme="light"] .vote-global-slot .vote-table__diff[data-dir="down"] {
  color: #9E3814;   /* 5.65 on the tinted row; #B4451E measured 4.49, a hair short */
}

:root[data-theme="light"] .vote-global-slot .vote-night {
  background: rgba(var(--surface-rgb), 0.08);
  color: var(--text-secondary);
}

/* Podium. The medals are pale on purpose against the dark sky; on a light card
   they need their dark counterparts or they disappear into the surface. */
:root[data-theme="light"] .vote-global-slot .vote-table__row[data-podium="1"] .vote-table__rank { color: #8A5A00; }
:root[data-theme="light"] .vote-global-slot .vote-table__row[data-podium="2"] .vote-table__rank { color: #55607A; }
:root[data-theme="light"] .vote-global-slot .vote-table__row[data-podium="3"] .vote-table__rank { color: #8A4A22; }
:root[data-theme="light"] .vote-global-slot .vote-table__row[data-podium="1"] .vote-table__btn {
  border-color: rgba(var(--accent-gold-rgb), 0.65);
  background: linear-gradient(100deg, rgba(var(--accent-gold-rgb), 0.2), transparent 60%);
}
:root[data-theme="light"] .vote-global-slot .vote-table__row[data-podium="2"] .vote-table__btn {
  border-color: rgba(85, 96, 122, 0.42);
  background: linear-gradient(100deg, rgba(85, 96, 122, 0.13), transparent 60%);
}
:root[data-theme="light"] .vote-global-slot .vote-table__row[data-podium="3"] .vote-table__btn {
  border-color: rgba(138, 74, 34, 0.42);
  background: linear-gradient(100deg, rgba(138, 74, 34, 0.13), transparent 60%);
}
:root[data-theme="light"] .vote-global-slot .vote-table__row.is-champion .vote-table__btn {
  border-color: rgba(var(--accent-gold-rgb), 0.5);
  background: rgba(var(--accent-gold-rgb), 0.1);
}

:root[data-theme="light"] .vote-global-slot .vote-global__meta,
:root[data-theme="light"] .vote-global-slot .vote-global__empty {
  color: var(--text-primary);
}
:root[data-theme="light"] .vote-global-slot .vote-global__meta-sub,
:root[data-theme="light"] .vote-global-slot .vote-global__weighting,
:root[data-theme="light"] .vote-global-slot .vote-global__empty-sub,
:root[data-theme="light"] .vote-global-slot .vote-hint {
  color: var(--text-secondary);
}

:root[data-theme="light"] .vote-global-slot .vote-global__tab {
  border-color: rgba(var(--surface-rgb), 0.12);
  background: rgba(var(--surface-rgb), 0.04);
  color: var(--text-secondary);
}
@media (hover: hover) { :root[data-theme="light"] .vote-global-slot .vote-global__tab:hover { background: rgba(var(--surface-rgb), 0.08); } }
:root[data-theme="light"] .vote-global-slot .vote-global__tab.is-on {
  border-color: rgba(var(--accent-teal-rgb), 0.6);
  background: rgba(var(--accent-teal-rgb), 0.16);
  color: var(--accent-teal-text);
}
:root[data-theme="light"] .vote-global-slot .vote-global__search {
  border-color: rgba(var(--surface-rgb), 0.12);
  background: rgba(var(--surface-rgb), 0.04);
  color: var(--text-primary);
}
:root[data-theme="light"] .vote-global-slot .vote-global__search::placeholder {
  color: var(--text-secondary);
}

/* ── The ground, and the two washes painted on it ───────────────────────── */

:root[data-theme="light"] body.vote-immersive { background-color: var(--bg-secondary); }
/* Alpha boosted like the site's own --space-gradient does in light (~1.8x):
   the accents are the darker light variants there, so the same alpha that reads
   as atmosphere on near-black is invisible on lavender. */
:root[data-theme="light"] body.vote-immersive #vote-root {
  background:
    radial-gradient(ellipse 120% 70% at 50% 0%, rgba(var(--accent-teal-rgb), 0.16), transparent 62%),
    radial-gradient(ellipse 120% 60% at 50% 100%, rgba(var(--accent-gold-rgb), 0.16), transparent 62%),
    var(--bg-secondary);
}
:root[data-theme="light"] .vote-game {
  background:
    radial-gradient(ellipse 120% 80% at 50% 120%, rgba(var(--accent-gold-rgb), 0.18), transparent 60%),
    radial-gradient(ellipse 90% 60% at 8% -10%, rgba(var(--accent-teal-rgb), 0.18), transparent 55%),
    var(--bg-secondary);
  color: var(--text-primary);
}
/* The entry veil exists to darken the middle of the starfield so the headline
   reads over it. Same job, opposite direction, same ground colour. */
:root[data-theme="light"] .vote-entrysky::after {
  background:
    radial-gradient(ellipse 78% 46% at 50% 42%, rgba(219, 212, 233, 0.86), rgba(219, 212, 233, 0.32) 62%, transparent 80%),
    linear-gradient(rgba(219, 212, 233, 0.28), rgba(219, 212, 233, 0.28));
}

/* ── Text that sits on the ground (never text inside a plate) ───────────── */

:root[data-theme="light"] .vote-page,
:root[data-theme="light"] .vote-hero__title,
:root[data-theme="light"] .vote-guest__label,
:root[data-theme="light"] .vote-gamehead__back,
:root[data-theme="light"] .vote-gamehead__exit,
:root[data-theme="light"] .vote-arena__prompt,
:root[data-theme="light"] .vote-skip__title,
:root[data-theme="light"] .vote-result__title,
:root[data-theme="light"] .vote-table__title,
:root[data-theme="light"] .vote-table__btn,
:root[data-theme="light"] .vote-stat,
:root[data-theme="light"] .vote-secondary,
:root[data-theme="light"] .vote-guest__warn,
:root[data-theme="light"] .vote-quick__btn,
:root[data-theme="light"] .vote-processing__line {
  color: var(--text-primary);
}
:root[data-theme="light"] .vote-hero__sub,
:root[data-theme="light"] .vote-gamehead__count,
:root[data-theme="light"] .vote-arena__hint,
:root[data-theme="light"] .vote-skip__sub,
:root[data-theme="light"] .vote-skip__glyph,
:root[data-theme="light"] .vote-breakdown__head,
:root[data-theme="light"] .vote-result__section,
:root[data-theme="light"] .vote-result__note,
:root[data-theme="light"] .vote-table__rank,
:root[data-theme="light"] .vote-table__credit,
:root[data-theme="light"] .vote-table__diff[data-dir="same"],
:root[data-theme="light"] .vote-share__label,
:root[data-theme="light"] .vote-submit__line,
:root[data-theme="light"] .vote-hint,
:root[data-theme="light"] .vote-listen__head,
:root[data-theme="light"] .vote-processing__sub,
:root[data-theme="light"] body.vote-immersive #vote-root > .lg-footer-link,
:root[data-theme="light"] body.vote-immersive #vote-root > .lg-footer-link a {
  color: var(--text-secondary);
}

/* ── Identity colours used as bare text on the ground ───────────────────── */
/* The fills, borders and glyphs keep their vivid literal — this is only the
   `-text` half of the split the site already uses for tier colours. */
:root[data-theme="light"] .vote-hero__badge,
:root[data-theme="light"] .vote-result__kicker,
:root[data-theme="light"] .vote-gamehead__phase,
:root[data-theme="light"] .vote-listen__btn,
:root[data-theme="light"] .vote-quick__btn .vote-quick__btn-icon,
:root[data-theme="light"] .vote-table__pts,
:root[data-theme="light"] .vote-table__unit,
:root[data-theme="light"] .vote-submit__retry {
  color: var(--accent-teal-text);
}
:root[data-theme="light"] .vote-table__diff[data-dir="up"]   { color: #1B6B3E; }   /* 5.32:1 on the podium tint; --success-text measured 4.24 there */
:root[data-theme="light"] .vote-table__diff[data-dir="down"] { color: #9E3814; }   /* 5.65:1 — #B4451E measured 4.49, a hair short */
:root[data-theme="light"] .vote-table__row[data-podium="1"] .vote-table__rank { color: #8A5A00; }
:root[data-theme="light"] .vote-table__row[data-podium="2"] .vote-table__rank { color: #55607A; }
:root[data-theme="light"] .vote-table__row[data-podium="3"] .vote-table__rank { color: #8A4A22; }

/* ── Self-tints on the ground ───────────────────────────────────────────── */
/* rgba(var(--white-rgb),a) is invisible on lavender. --surface-rgb is the token that
   flips those channels to the site's indigo ink, and is how every other
   stylesheet here writes a self-tint. Alphas are raised the way --border is
   (0.08 -> 0.14): a light ground is far less contrasty against a faint tint. */
:root[data-theme="light"] .vote-stat,
:root[data-theme="light"] .vote-secondary,
:root[data-theme="light"] .vote-gamehead__exit,
:root[data-theme="light"] .vote-gamehead__back,
:root[data-theme="light"] .vote-skip,
:root[data-theme="light"] .vote-breakdown__chip,
:root[data-theme="light"] .vote-share,
:root[data-theme="light"] .vote-submit,
:root[data-theme="light"] .vote-submit__retry,
:root[data-theme="light"] .vote-table__btn {
  background: var(--bg-elevated);
  border-color: rgba(var(--surface-rgb), 0.14);
}
:root[data-theme="light"] .vote-gamebar { background: rgba(var(--surface-rgb), 0.14); }
:root[data-theme="light"] .vote-table__row .vote-night {
  background: rgba(var(--surface-rgb), 0.08);
  color: var(--text-secondary);
}

/* ── The decorative starfield, which is the one thing with no plate ─────── */
/* --star-rgb: white in dark, GOLD in light. Created 2026-08-08 for exactly this
   problem; this file was drawing literal white and so had opted out of it. */
:root[data-theme="light"] .vote-stars {
  background-image:
    radial-gradient(1.2px 1.2px at 20% 30%, rgba(var(--star-rgb), 0.75), transparent 100%),
    radial-gradient(1px 1px at 70% 18%, rgba(var(--star-rgb), 0.6), transparent 100%),
    radial-gradient(1.4px 1.4px at 44% 72%, rgba(var(--star-rgb), 0.7), transparent 100%),
    radial-gradient(1px 1px at 86% 62%, rgba(var(--star-rgb), 0.55), transparent 100%),
    radial-gradient(1.1px 1.1px at 12% 82%, rgba(var(--star-rgb), 0.6), transparent 100%);
}
:root[data-theme="light"] .vote-entrystar { filter: saturate(0.9) brightness(0.72); }

/* These two carry coloured ink (teal wordmarks, a teal glyph), so they get a
   lighter tint than the neutral controls above — at 0.06 the ink measured 4.38
   and 4.16, both a hair under. */
:root[data-theme="light"] .vote-listen__btn,
:root[data-theme="light"] .vote-quick__btn {
  background: var(--bg-elevated);
  border-color: rgba(var(--surface-rgb), 0.14);
}

/* Rarity labels are small text, so --accent-gold's ~3.2:1 tuning (right for a
   fill or a border) is not enough — measured 3.52 on --bg-elevated. Each is the
   darkest form of its own hue that clears 4.5 there. */
:root[data-theme="light"] .vote-breakdown__chip[data-rarity="star"]   { color: #8A5A00; }   /* 6.51 */
:root[data-theme="light"] .vote-breakdown__chip[data-rarity="home"]   { color: var(--accent-blue); }   /* 5.82 */
:root[data-theme="light"] .vote-breakdown__chip[data-rarity="binary"] { color: var(--accent-teal-text); }   /* 6.14 */
:root[data-theme="light"] .vote-breakdown__chip[data-rarity="comet"]  { color: #6B3FA0; }   /* 6.41 */

/* ── The song card's plate, in light mode ───────────────────────────────── */
/* var(--fixed-bg-2) is a near-black NAVY. Against a lavender page it stopped reading as
   "a dark card" and started reading as "a blue hole" (owner, 2026-08-25). This
   is the same value desaturated and warmed into the page's own hue family — the
   site's ink is rgb(20,15,50), so the plate is a muted form of that rather than
   a colour from somewhere else. Card vs ground measures 10.37:1, so it still
   reads as firmly dark; the title on it is 14.92:1. */
:root[data-theme="light"] .vote-card {
  background:
    radial-gradient(120% 70% at 50% 0%, rgba(var(--white-rgb), 0.10), transparent 66%),
    #2A2A2A;
  border-color: rgba(var(--white-rgb), 0.16);
}
/* The plate is lighter than it was, so the dimmest ink on it no longer clears
   4.5 (3.89). One step up, only here. */
:root[data-theme="light"] .vote-card__latin,
:root[data-theme="light"] .vote-card__vary { color: var(--text-secondary); }

/* ── The Skip card is a control on the ground, not a plate ──────────────── */
:root[data-theme="light"] .vote-skip {
  background: var(--bg-elevated);
  border-color: rgba(var(--surface-rgb), 0.30);
  color: var(--text-secondary);
}
:root[data-theme="light"] .vote-skip__title { color: var(--text-primary); }
:root[data-theme="light"] .vote-skip__num { background: var(--bg-secondary); color: var(--text-primary); }

/* ── The bracket's round chip ───────────────────────────────────────────── */
/* var(--brand-gold-pale) on a gold tint measured 1.13 against the light ground — reported as
   "เหลือ xxx ใบสุดท้าย มันจางไป". The tint keeps its hue and gains the light
   accent's channels; the label goes to the darkest gold that clears 4.5 on it. */
:root[data-theme="light"] .vote-arena__round {
  background: rgba(var(--accent-gold-rgb), 0.18);
  color: var(--accent-gold);   /* was PPW amber #6E4600 (2026-10-22) */
}

/* The champion box and the sector interstitial are the same plate as the card
   (var(--fixed-bg-2)) and read as the same blue hole against a lavender page, so they
   follow it to #2A2A2A. Each keeps its own wash — gold for the champion and the
   phase card, teal for a sector, purple for the anomaly — only the base moves. */
:root[data-theme="light"] .vote-champ {
  background:
    radial-gradient(circle at 50% 0%, rgba(var(--accent-gold-rgb), 0.24), transparent 66%),
    #2A2A2A;
}
:root[data-theme="light"] .vote-inter {
  background:
    radial-gradient(circle at 50% 0%, rgba(var(--accent-teal-rgb), 0.20), transparent 62%),
    #2A2A2A;
}
:root[data-theme="light"] .vote-inter[data-kind="phase"] {
  background:
    radial-gradient(circle at 50% 0%, rgba(var(--accent-gold-rgb), 0.22), transparent 62%),
    #2A2A2A;
}
:root[data-theme="light"] .vote-inter[data-kind="anomaly"] {
  background:
    radial-gradient(circle at 50% 0%, rgba(200, 155, 255, 0.20), transparent 62%),
    #2A2A2A;
}
/* Same lift as .vote-card__latin — the dimmest ink on the plate measured 3.89
   once the plate lightened. */
:root[data-theme="light"] .vote-inter__count { color: var(--text-secondary); }

/* ═══ Red-fog theme (events/jasper-temptation-2026, 2026-10-12) ═══════════════
   Owner ruling: the game follows the site — neutral black ground, a metal-red
   family for orbs (hue remapped in vote-card.js themeOrbHue) and rarity colours
   red / silver / copper / white. Medal colours and up/down diff colours are
   semantic and stay. Every rule is scoped so PPW is untouched. */
:root[data-motif-pattern="red-fog"][data-project] .vote-hero__badge { color: var(--text-primary); }
:root[data-motif-pattern="red-fog"][data-project] .vote-hero__badge-dot { color: var(--accent-gold-text); }
/* CTA: red gradient needs white ink (fixed-bg-3 ink was 1.86:1 on it). */
:root[data-motif-pattern="red-fog"][data-project] .vote-cta { color: #FFFFFF; }
:root[data-motif-pattern="red-fog"][data-project][data-theme="light"] .vote-actions__note { color: var(--text-secondary); }
:root[data-motif-pattern="red-fog"][data-project] .vote-entrysky::after {
  background:
    radial-gradient(ellipse 78% 46% at 50% 42%, rgba(5, 5, 5, 0.86), rgba(5, 5, 5, 0.32) 62%, transparent 80%),
    linear-gradient(rgba(5, 5, 5, 0.28), rgba(5, 5, 5, 0.28));
}
:root[data-motif-pattern="red-fog"][data-project] .vote-howto { background: rgba(5, 5, 5, 0.82); }
:root[data-motif-pattern="red-fog"][data-project][data-theme="light"] .vote-howto { background: rgba(239, 236, 236, 0.9); }
:root[data-motif-pattern="red-fog"][data-project] .vote-viewbar { background: rgba(8, 8, 8, 0.92); }
:root[data-motif-pattern="red-fog"][data-project]:not([data-theme="light"]) .vote-gamehead__count,
:root[data-motif-pattern="red-fog"][data-project] .vote-champ__latin { color: #8A8A8A; }
/* Every red-gradient action needs white ink (fixed-bg-3 measured 1.86:1). */
:root[data-motif-pattern="red-fog"][data-project] .vote-howto__next,
:root[data-motif-pattern="red-fog"][data-project] .vote-inter__cta,
:root[data-motif-pattern="red-fog"][data-project] .vote-viewbar__cta { color: #FFFFFF; }
/* Crew line sits on a dark card in both themes — use the bright red. */
:root[data-motif-pattern="red-fog"][data-project] .vote-inter__crew { color: var(--brand-gold-pale); }
:root[data-motif-pattern="red-fog"][data-project] .vote-card__band-label { color: #B5B5B5; }
:root[data-motif-pattern="red-fog"][data-project] .vote-card[data-rarity="home"]  .vote-card__band-label { color: #D9824A; }
:root[data-motif-pattern="red-fog"][data-project] .vote-card[data-rarity="comet"] .vote-card__band-label { color: #F2F2F2; }
:root[data-motif-pattern="red-fog"][data-project] .vote-breakdown__chip[data-rarity="home"]  { border-color: rgba(217, 130, 74, 0.4); color: #D9824A; }
:root[data-motif-pattern="red-fog"][data-project] .vote-breakdown__chip[data-rarity="comet"] { border-color: rgba(242, 242, 242, 0.4); color: #F2F2F2; }
:root[data-motif-pattern="red-fog"][data-project] .vote-anatomy__row[data-rarity=comet] .vote-anatomy__glyph { color: #F2F2F2; }
:root[data-motif-pattern="red-fog"][data-project] .vote-inter[data-kind="anomaly"] { background: radial-gradient(circle at 50% 0%, rgba(var(--accent-gold-rgb), 0.16), transparent 62%), var(--fixed-bg-2); }
:root[data-motif-pattern="red-fog"][data-project] .vote-inter[data-kind="anomaly"] .vote-inter__kicker,
:root[data-motif-pattern="red-fog"][data-project] .vote-inter__glyph { color: #F2F2F2; }
:root[data-motif-pattern="red-fog"][data-project] .vote-chart { background: #080808; }
:root[data-motif-pattern="red-fog"][data-project] .vote-chart__view {
  background:
    radial-gradient(circle at 50% 42%, rgba(var(--brand-teal-rgb), 0.05), transparent 62%),
    radial-gradient(ellipse at 50% 104%, rgba(var(--brand-gold-rgb), 0.14), transparent 60%),
    #080808;
}
:root[data-motif-pattern="red-fog"][data-project] .vote-chart__nebula {
  background:
    radial-gradient(ellipse 46% 38% at 28% 32%, rgba(var(--brand-teal-rgb), 0.10), transparent 68%),
    radial-gradient(ellipse 52% 40% at 74% 62%, rgba(var(--accent-gold-rgb), 0.20), transparent 70%),
    radial-gradient(ellipse 60% 44% at 50% 104%, rgba(61, 0, 0, 0.5), transparent 66%);
}
:root[data-motif-pattern="red-fog"][data-project] .vote-chart__nebula::after {
  background:
    radial-gradient(ellipse 38% 30% at 68% 22%, rgba(217, 130, 74, 0.12), transparent 70%),
    radial-gradient(ellipse 44% 34% at 22% 78%, rgba(var(--accent-gold-rgb), 0.14), transparent 72%);
}
:root[data-motif-pattern="red-fog"][data-project]:not([data-theme="light"]) .vote-stars {
  background-image:
    radial-gradient(1.2px 1.2px at 20% 30%, rgba(var(--white-rgb),0.55), transparent 100%),
    radial-gradient(1px 1px at 70% 18%, rgba(var(--white-rgb),0.4), transparent 100%),
    radial-gradient(1.4px 1.4px at 44% 72%, rgba(var(--white-rgb),0.5), transparent 100%),
    radial-gradient(1px 1px at 86% 62%, rgba(var(--brand-teal-rgb),0.45), transparent 100%),
    radial-gradient(1.2px 1.2px at 12% 84%, rgba(var(--accent-gold-rgb),0.5), transparent 100%),
    radial-gradient(1px 1px at 58% 44%, rgba(var(--white-rgb),0.35), transparent 100%),
    radial-gradient(1.3px 1.3px at 32% 12%, rgba(var(--brand-gold-rgb),0.4), transparent 100%),
    radial-gradient(1px 1px at 92% 34%, rgba(var(--white-rgb),0.4), transparent 100%);
}
:root[data-motif-pattern="red-fog"][data-project] .vote-processing__ring--3 { border-color: rgba(var(--accent-gold-rgb), 0.28); border-top-color: rgba(var(--accent-gold-rgb), 0.9); }
:root[data-motif-pattern="red-fog"][data-project] .vote-anatomy__orb { background: radial-gradient(circle at 34% 30%, #FF8A80, #C0201C 60%, #3D0000); }
:root[data-motif-pattern="red-fog"][data-project]:not([data-theme="light"]) .vote-submit[data-tone="ok"] .vote-submit__line { color: #E0E0E0; }
/* light theme */
:root[data-motif-pattern="red-fog"][data-project][data-theme="light"] .vote-entrysky::after {
  background:
    radial-gradient(ellipse 78% 46% at 50% 42%, rgba(239, 236, 236, 0.86), rgba(239, 236, 236, 0.32) 62%, transparent 80%),
    linear-gradient(rgba(239, 236, 236, 0.28), rgba(239, 236, 236, 0.28));
}
:root[data-motif-pattern="red-fog"][data-project][data-theme="light"] .vote-breakdown__chip[data-rarity="home"]  { color: #8A4A22; }
:root[data-motif-pattern="red-fog"][data-project][data-theme="light"] .vote-breakdown__chip[data-rarity="comet"] { color: #3D3D3D; }
:root[data-motif-pattern="red-fog"][data-project][data-theme="light"] .vote-card {
  background: radial-gradient(120% 70% at 50% 0%, rgba(var(--white-rgb), 0.10), transparent 66%), #1A1A1A;
}
:root[data-motif-pattern="red-fog"][data-project][data-theme="light"] .vote-champ {
  background: radial-gradient(circle at 50% 0%, rgba(var(--accent-gold-rgb), 0.24), transparent 66%), #1A1A1A;
}
:root[data-motif-pattern="red-fog"][data-project][data-theme="light"] .vote-inter {
  background: radial-gradient(circle at 50% 0%, rgba(var(--accent-teal-rgb), 0.20), transparent 62%), #1A1A1A;
}
:root[data-motif-pattern="red-fog"][data-project][data-theme="light"] .vote-inter[data-kind="phase"],
:root[data-motif-pattern="red-fog"][data-project][data-theme="light"] .vote-inter[data-kind="anomaly"] {
  background: radial-gradient(circle at 50% 0%, rgba(var(--accent-gold-rgb), 0.22), transparent 62%), #1A1A1A;
}
:root[data-motif-pattern="red-fog"][data-project][data-theme="light"] .vote-inter__crew { color: var(--on-dark-text); }
