:root {
  --bg: #000000;
  --panel: #1a1a1a;
  --text: #f5e9d3;
  --muted: #9ca3af;
  --accent: #d1d5db;
  --border: #3a3a3a;
  --lane: #3d2856;
  --lane-alt: #362248;
  /* track-1.png is fully opaque, so --lane only shows through when a theme opts
     into a blend mode. Derby keeps `normal` — the sprite renders untouched. */
  --track-blend: normal;
  --track-shade: transparent;
  --track-wash: linear-gradient(var(--track-shade), var(--track-shade));
  --wash-blend: normal;
  --rail: #ffffff;
  --label-box: #1d1d24;
  --live: #7bed9f;
  --ticker-accent: #ffd166;
  --track-gap: 0px;
  --horse-height: 60px;
  --sprite-scale: 2;
  --label-col: 124px;
  --label-mid: 23%;  /* vertical centre of the name-tag — aligned with the horse's middle */
  --ticker-h: 2.6rem;
  --error: #ff6b6b;
  --error-wash: rgba(255, 107, 107, 0.1);
  --pixel-font: "VT323", "Courier New", ui-monospace, monospace;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--pixel-font);
  font-size: 20px;
  line-height: 1.25;
  min-height: 100vh;
  image-rendering: pixelated;
  -webkit-font-smoothing: none;
  -moz-osx-font-smoothing: grayscale;
}

#app { min-height: 100vh; display: flex; flex-direction: column; }

/* ── Home ──────────────────────────────────────────── */

.home {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 32px;
  padding: 32px;
  text-align: center;
}
.home h1 {
  font-size: clamp(2.5rem, 8vw, 4rem);
  color: var(--accent);
  letter-spacing: 0.08em;
  margin: 0;
  display: flex;
  align-items: center;
  gap: 0.4em;
}
.horse-face {
  height: 1em;
  width: auto;
  image-rendering: pixelated;
  shape-rendering: crispEdges;
  flex-shrink: 0;
}
.horse-face-flip {
  display: inline-flex;
  transform: scaleX(-1);
}
.home p { color: var(--muted); margin: 0; max-width: 36ch; }
.home form { display: flex; gap: 8px; }
.home input {
  background: var(--panel);
  color: var(--text);
  border: 2px solid var(--border);
  padding: 12px 16px;
  font: inherit;
  font-size: 1.25rem;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  width: 180px;
}
.home input:focus { outline: none; border-color: var(--accent); }
.home button {
  background: var(--accent);
  color: var(--bg);
  border: none;
  padding: 12px 24px;
  font: inherit;
  font-size: 1rem;
  font-weight: bold;
  cursor: pointer;
}
.home button:hover { filter: brightness(1.1); }

.btn {
  background: var(--accent);
  color: var(--bg);
  border: none;
  padding: 8px 14px;
  font: inherit;
  font-size: 0.9rem;
  font-weight: bold;
  letter-spacing: 0.03em;
  cursor: pointer;
  border-radius: 3px;
}
.btn:hover { filter: brightness(1.1); }

.home-divider {
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.15em;
  font-size: 0.75rem;
  margin: -8px 0 -8px;
}
.install-line.terminal {
  margin-top: auto;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  background: #050a05;
  border: 2px solid #1f5a1f;
  box-shadow: 0 0 12px rgba(74, 222, 128, 0.18) inset, 0 0 0 1px #000;
  color: #4ade80;
  font-size: 1rem;
  letter-spacing: 0.05em;
  text-shadow: 0 0 6px rgba(74, 222, 128, 0.55);
}
.terminal-prompt { color: #4ade80; opacity: 0.7; }
.terminal-cmd { color: #4ade80; }
.terminal-cursor {
  display: inline-block;
  color: #4ade80;
  animation: terminal-blink 1s steps(2, start) infinite;
}
@keyframes terminal-blink {
  to { visibility: hidden; }
}

/* ── Org view ──────────────────────────────────────── */

.org {
  flex: 1;
  display: flex;
  flex-direction: column;
}
.org-header {
  padding: 16px 24px;
  border-bottom: 2px solid var(--panel);
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 16px;
}
.org-header h1 {
  margin: 0;
  color: var(--accent);
  font-size: 1.5rem;
  letter-spacing: 0.05em;
  display: flex;
  align-items: center;
  gap: 0.4em;
}
.org-body {
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 32px;
}
.org-status { color: var(--muted); }
/* Live section pins to the viewport top; the bg + padding mask the rows
   scrolling beneath it (negative margins keep the resting layout unchanged). */
.org-section-live {
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--bg);
  /* mask the full 32px org-body gap below so scrolling rows don't peek through */
  padding: 12px 0 32px;
  margin: -12px 0 -32px;
}
.org-section-live:not(:last-child) { border-bottom: 2px solid var(--panel); }
.org-section h2 {
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 1rem;
  margin: 0 0 12px;
}
.race-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.race-row a {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 14px;
  padding: 12px 16px;
  background: var(--panel);
  border: 2px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  text-decoration: none;
}
.race-row a:hover { border-color: var(--accent); }
.race-row-sprite {
  flex: 0 0 auto;
  width: 56px;
  height: 40px;
  display: flex;
  align-items: flex-end;
}
.race-row-sprite .horse-sprite {
  width: 100%;
  height: 100%;
}
.race-row-ident {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  flex: 1 1 200px;
}
.race-row-name { font-weight: bold; }
.race-row-meta {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 0.75rem;
}
.race-row-code {
  color: var(--accent);
  letter-spacing: 0.05em;
}
.race-row-meta .race-row-date { color: var(--muted); }
.race-row-meta .race-row-date::before,
.race-row-meta .race-row-countdown::before { content: '· '; font-weight: normal; color: var(--muted); }
.race-row-label {
  font-size: 0.7rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.race-row-info {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  margin-left: auto;
  text-align: right;
  font-size: 0.8rem;
  color: var(--muted);
}
.race-row-countdown { font-weight: bold; color: var(--text); }
.race-row-winner,
.race-row-leader {
  color: var(--text);
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

/* ── Race view ─────────────────────────────────────── */

.race {
  display: grid;
  /* minmax(0, 1fr), not the implicit auto: an auto track sizes to its widest
     item's max-content, so one long race name stretched this column — and the
     whole page — far past the viewport. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto auto 1fr;
  min-height: 100vh;
}
.race-header {
  min-width: 0;   /* grid items default to min-width:auto, which re-blows the track */
  padding: 6px 24px;
  border-bottom: 2px solid var(--panel);
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* Title and controls share one line while the title can still hold its
     floor (see h1 min-width). Once it can't, the controls wrap to a second
     row rather than the title being squeezed to nothing — no breakpoint
     needed, flexbox picks the moment from the two widths. */
  flex-wrap: wrap;
  gap: 16px;
}
.race-header .meta { flex-shrink: 0; }
/* min-width is the wrap trigger: the title shrinks and marquees down to this
   floor, and below it the controls take a second row instead. ~16rem keeps a
   useful amount of name on screen — roughly small-laptop width and up stays
   on one line. */
.race-header h1 { margin: 0; color: var(--accent); font-size: 1.25rem; letter-spacing: 0.05em; display: flex; align-items: center; gap: 0.4em; min-width: 16rem; max-width: 100%; flex: 1 1 0; }
/* Race names have no server-side length limit, so the heading must not be able
   to set the header's size. Without this an unbroken name overflows the page
   horizontally, and a long one wraps to several lines and shoves the crowd and
   track down. The row clips and marquees the name — the same treatment the
   lane name-tags get — so a long name is still readable in full. */
.race-header h1 .race-name-row {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
}
/* Reduced motion: no marquee, so fall back to an ellipsis rather than a name
   that is silently cut mid-word. The title attribute carries the full text. */
@media (prefers-reduced-motion: reduce) {
  .race-header h1 .race-name-scroll {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    animation: none;
  }
}
/* nowrap on purpose: the header must stay one row. Everything inside is
   allowed to shrink or drop out (see the narrow-screen block at the end of
   this file) rather than pushing a button onto a second line. */
.race-header .meta { color: var(--muted); display: flex; align-items: center; gap: 20px; flex-wrap: nowrap; min-width: 0; }
/* Buttons keep their labels — a truncated "Gra…" is worse than no Graphs at
   all. The select is the only thing allowed to shrink; everything else that
   cannot fit is dropped outright further down. */
.race-header .meta .btn { white-space: nowrap; flex-shrink: 0; }
.race-header .meta .theme-select { min-width: 0; }
.race-header .meta > span { white-space: nowrap; flex-shrink: 0; }
.race-header .meta b { color: var(--text); font-weight: normal; }
.race-status { text-transform: uppercase; letter-spacing: 0.1em; }
.race-status--pending { color: var(--accent); }
.race-status--live { color: var(--live); }
.race-status--finished { color: var(--muted); }

.crowd {
  width: 100%;
  justify-self: center;
  height: calc(var(--sprite-scale) * 32px);
  display: flex;
  overflow: hidden;
  image-rendering: pixelated;
}
.crowd-cap, .crowd-person {
  flex: 0 0 calc(var(--sprite-scale) * 32px);
  height: 100%;
  background-size: calc(var(--sprite-scale) * 32px) calc(var(--sprite-scale) * 128px);
  background-position: 0 0;
  background-repeat: no-repeat;
  image-rendering: pixelated;
  /* Cycle length and start offset are per-sprite (see render/crowd.ts) so the
     crowd cheers out of step instead of as one block. */
  animation: crowd-cheer var(--cheer-duration, 1s) steps(4) infinite;
  animation-delay: var(--cheer-delay, 0s);
}
.crowd-cap-left  { background-image: url('img/crowd-1-left.png'); }
.crowd-cap-right { background-image: url('img/crowd-1-right.png'); }
.crowd-body {
  flex: 1 1 auto;
  display: flex;
}
.crowd-person { background-image: url('img/crowd-1.png'); }
.race.finished .crowd-cap,
.race.finished .crowd-person { animation-play-state: paused; }
@keyframes crowd-cheer {
  from { background-position-y: 0; }
  to   { background-position-y: calc(var(--sprite-scale) * -128px); }
}

.track {
  padding: 0 4px 24px 4px;
  display: flex;
  flex-direction: column;
  gap: var(--track-gap);
}

.lane {
  /* Never let a crowded field compress lanes below the sprite height — the
     track scrolls (autoscroll drives it in TV mode) instead of squashing
     horses on top of each other. */
  flex: 0 0 var(--horse-height);
  height: var(--horse-height);
  display: flex;
  align-items: stretch;
}
/* Fixed name-tag column at the start of every lane — names/levels/tokens all
   line up here instead of floating in front of / behind the moving horse. */
.lane-info {
  flex: 0 0 var(--label-col);
  position: relative;
  pointer-events: none;
}
/* Bordered chip, vertically aligned with the horse's middle (not the lane
   centre line), giving each lane a distinct edge. */
.lane-info-box {
  position: absolute;
  top: var(--label-mid);
  left: 4px;
  right: 6px;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: 1px 6px;
  background: var(--label-box); /* slightly lifted off the page's pure black */
  border: 1px solid var(--border);
  border-radius: 4px;
  overflow: hidden;
  line-height: 1.0;
  letter-spacing: 0.02em;
}
/* Each row clips; .row-scroll marquees its content when it overflows. */
.horse-name-row {
  overflow: hidden;
  white-space: nowrap;
}
/* Total tokens: permanent (no longer part of the rotator), sits under the name.
   Negative margins pull the lower two rows up so the three rows sit tight
   (the pixel glyphs don't fill their line box, so a small overlap never clips). */
.horse-tokens-row {
  overflow: hidden;
  white-space: nowrap;
  margin-top: -2px;
}
.row-scroll {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  will-change: transform;
}
.row-scroll.is-scrolling {
  animation: label-marquee 7s ease-in-out infinite;
}
@keyframes label-marquee {
  0%, 16%   { transform: translateX(0); }
  46%, 54%  { transform: translateX(var(--shift, 0)); }
  84%, 100% { transform: translateX(0); }
}
/* Row 2 stacks its views in one cell and cross-fades between them. */
.horse-stats-row {
  display: grid;
  align-items: center;
  overflow: hidden;
  margin-top: -2px;
}
.stat-view {
  grid-area: 1 / 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  animation: stat-cycle 12s ease-in-out infinite;
}
.stat-view:nth-child(2) { animation-delay: -9s; }   /* level    — 2nd */
.stat-view:nth-child(3) { animation-delay: -6s; }   /* pace     — 3rd */
.stat-view:nth-child(4) { animation-delay: -3s; }   /* position — 4th */
@keyframes stat-cycle {
  0%   { opacity: 1; }
  21%  { opacity: 1; }
  25%  { opacity: 0; }
  96%  { opacity: 0; }
  100% { opacity: 1; }
}
/* League lanes rotate 5 frames (adds the in-division league position) instead of 4. */
.horse-stats-row.is-league .stat-view { animation-name: stat-cycle-5; }
.horse-stats-row.is-league .stat-view:nth-child(2) { animation-delay: -9.6s; }
.horse-stats-row.is-league .stat-view:nth-child(3) { animation-delay: -7.2s; }
.horse-stats-row.is-league .stat-view:nth-child(4) { animation-delay: -4.8s; }
.horse-stats-row.is-league .stat-view:nth-child(5) { animation-delay: -2.4s; }
@keyframes stat-cycle-5 {
  0%   { opacity: 1; }
  16%  { opacity: 1; }
  20%  { opacity: 0; }
  96%  { opacity: 0; }
  100% { opacity: 1; }
}
/* The running strip (dirt + rails) begins to the right of the label column. */
.lane-track {
  position: relative;
  flex: 1 1 auto;
  /* Two layers over --lane: a wash, then the dirt sprite. --track-blend re-tints
     the (opaque) sprite from --lane; the wash then adjusts the result, because
     `luminosity` preserves the sprite's own high luminance. Most themes only need
     a flat darkening colour (--track-shade), so that is the default wash; a theme
     wanting real colour variation across the lane replaces the whole image via
     --track-wash (see Rainbow Road). Blending is confined to this element's own
     backgrounds — .horse children are untouched (a `filter` here would have
     recoloured the horses too). */
  background:
    var(--track-wash),
    var(--lane) url('img/track-1.png') repeat-x center / auto 100%;
  background-blend-mode: var(--wash-blend), var(--track-blend);
  image-rendering: pixelated;
  border-radius: 4px;
}
.lane-track::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 4px;
  background: var(--rail);
}
.lane-track::after {
  content: "";
  position: absolute;
  right: 0; top: 0; bottom: 0;
  width: 20px;
  background:
    conic-gradient(#fff 25%, #000 25% 50%, #fff 50% 75%, #000 75%) 0 0 / 10px 10px;
  background-color: #fff;
}
.horse {
  position: absolute;
  /* Sprite scales with the lane (height:80% of the lane) so lanes can be any
     height. Vertical placement is driven entirely by `top` — the body-bob
     keyframes then only nudge ±1px without re-specifying the base position.
     The drawn hooves sit ~at the box bottom (≈ top + 80%), so top:-30% lands
     the FEET on the lane's vertical centre with the body above (verified by
     outlined-box screenshot). */
  top: -30%;
  transform: translate(-50%, 0);
  transition: left 3s linear;
  height: 80%;
  aspect-ratio: 32 / 24;
  z-index: 1;
}
.horse-sprite { width: 100%; height: 100%; display: block; }

/* Soft ground shadow under each horse — body + head ovals rendered as a single
   SVG group with one opacity so overlap doesn't darken (the group composites
   into a private buffer first, then applies the group opacity). */
.horse::after {
  content: "";
  position: absolute;
  left: -5px;
  right: 5px;
  top: calc(100% - 4px);
  height: 8px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 10' preserveAspectRatio='none'><g opacity='0.45'><ellipse cx='50' cy='5' rx='30' ry='3.5' fill='black'/><ellipse cx='85' cy='5' rx='12' ry='2.5' fill='black'/></g></svg>");
  background-size: 100% 100%;
  background-repeat: no-repeat;
  z-index: -1;
  pointer-events: none;
}

/* Dust trail kicked up by the back hoof (bottom-left of sprite) */
.horse-dust {
  position: absolute;
  left: 8%;
  bottom: -1px;
  width: 7px;
  height: 5px;
  background: rgba(75, 55, 45, 0.85);
  border-radius: 50%;
  pointer-events: none;
  z-index: 0;
  opacity: 0;
}
.horse-dust::before,
.horse-dust::after {
  content: "";
  position: absolute;
  inset: 0;
  background: inherit;
  border-radius: inherit;
}
.horse.live .horse-dust          { animation: dust-puff 0.7s ease-out infinite; }
.horse.live .horse-dust::before  { animation: dust-puff 0.7s ease-out infinite; animation-delay: 0.23s; }
.horse.live .horse-dust::after   { animation: dust-puff 0.7s ease-out infinite; animation-delay: 0.46s; }
@keyframes dust-puff {
  0%   { opacity: 0;    transform: translate(0, 0)        scale(0.4); }
  20%  { opacity: 0.75;                                              }
  100% { opacity: 0;    transform: translate(-16px, -5px) scale(1.6); }
}

/* Stamina reserve, a child of .horse so it tracks the sprite with no
   positioning maths of its own. Geometry settled against real screenshots
   at 6x zoom — see the stamina task brief before changing any of it. */
.stamina-bar {
  position: absolute;
  left: 50%;
  top: 100%;
  transform: translate(calc(-50% - 10px), 3px);  /* 10px back, 3px below the sprite */
  width: 38px;
  height: 3px;
  background: rgba(255, 255, 255, .34);          /* light channel, see below */
  border-radius: 999px;                          /* true capsule at any height */
  overflow: hidden;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .85);      /* NOT outline — see below */
  pointer-events: none;
}
.stamina-bar-fill {
  height: 100%;
  border-radius: 999px;
}
.stamina-bar[data-band="green"] .stamina-bar-fill { background: #7ee081; }
.stamina-bar[data-band="amber"] .stamina-bar-fill { background: #ff9f1c; }
.stamina-bar[data-band="red"]   .stamina-bar-fill { background: #ff3b30; }


.horse-label {
  color: var(--text);
  font-family: "Bitcount Prop Single", var(--pixel-font);
  font-size: 0.53rem;
  font-weight: 600;
}
.horse-level {
  display: inline-block;
  margin-left: 0;
  padding: 0 4px;
  font-family: "Barlow Semi Condensed", var(--pixel-font);
  font-weight: 600;
  font-size: 0.32rem;
  text-transform: uppercase;
  color: #fff;
  background: #2563eb;
  border-radius: 999px;
  letter-spacing: 0.02em;
  line-height: 1.4;
}
.user-label {
  color: var(--text);
  opacity: 0.6;
  font-size: 0.52rem;
}
.horse-tokens {
  color: #fbbf24;              /* amber — reads as coins, distinct from the cream name/grey pace */
  font-size: 0.53rem;
  font-weight: 600;
  opacity: 1;
}
.horse-pace {
  color: var(--accent);
  font-size: 0.52rem;
  font-weight: bold;
}
.horse-position {
  font-size: 0.55rem;
  font-weight: bold;
  color: #aab0b8;            /* everything past 3rd — light grey */
}
.horse-position.pos-1 { color: #ffd700; }  /* gold   */
.horse-position.pos-2 { color: #c8ccd2; }  /* silver */
.horse-position.pos-3 { color: #cd7f32; }  /* bronze */
.horse-league-pos {
  font-size: 0.55rem;
  font-weight: bold;
  color: #a68bd8;
}

/* ── Pending state extras ─────────────────────────── */

.pending-banner {
  text-align: center;
  padding: 16px;
  background: var(--panel);
  border-bottom: 2px solid var(--accent);
  font-size: 1.25rem;
  color: var(--accent);
  letter-spacing: 0.1em;
}

/* ── Finished state extras ────────────────────────── */

.finished .horse { transition: left 1s ease-out, transform 1s ease-out, opacity 1s ease-out; }

.podium {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.85);
  display: flex;
  flex-direction: column;
  align-items: center;
  /* `safe center` keeps tall standings (many racers) from clipping at the top
     of the scroll container so the whole list — and the dismiss button — stay
     reachable. */
  justify-content: safe center;
  gap: 24px;
  padding: 24px 24px calc(var(--ticker-h) + 24px);
  overflow-y: auto;
  animation: fade-in 1s ease-out forwards;
  /* Sit above the achievement ticker (z-index 9999) so the dismiss button is
     never covered by it. */
  z-index: 10000;
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

.podium h2 { color: var(--accent); margin: 0; font-size: 2rem; letter-spacing: 0.1em; }
.podium ol {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  align-items: flex-end;
  gap: 16px;
}
.podium li {
  background: var(--panel);
  padding: 16px;
  border-radius: 6px;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  min-width: 180px;
}
.podium li:nth-child(1) { border: 2px solid gold; transform: translateY(-16px); }
.podium li:nth-child(2) { border: 2px solid silver; }
.podium li:nth-child(3) { border: 2px solid #cd7f32; }
.podium .place { font-size: 2rem; }
.podium .name { font-weight: bold; }
.podium .podium-user { color: var(--muted); font-size: 0.7em; }
.podium .tokens { color: var(--muted); }
.podium .xp-line {
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: center;
  margin-top: 4px;
}
.podium .level-chip {
  display: inline-block;
  padding: 2px 6px;
  font-size: 0.7em;
  color: #67e8f9;
  border: 1px solid #67e8f9;
  border-radius: 3px;
  letter-spacing: 0.04em;
}
.podium .xp-gained { color: #67e8f9; font-weight: bold; font-size: 0.85em; }
.podium .xp-bar {
  width: 160px;
  height: 8px;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 4px;
  overflow: hidden;
}
.podium .xp-bar-fill {
  height: 100%;
  background: #67e8f9;
  transition: width 1.5s ease-out;
}
.podium .xp-bar-text { color: var(--muted); font-size: 0.7em; }
.podium .level-up-banner {
  margin-top: 6px;
  color: gold;
  font-weight: bold;
  font-size: 1rem;
  letter-spacing: 0.18em;
  text-shadow: 0 0 12px rgba(255, 215, 0, 0.6);
  animation: level-up-pulse 0.8s ease-in-out 0.3s infinite alternate;
}
@keyframes level-up-pulse {
  from { transform: scale(1); }
  to { transform: scale(1.12); }
}

.podium .standings-table {
  border-collapse: collapse;
  color: var(--text);
  background: var(--panel);
  border-radius: 6px;
  overflow: hidden;
  max-width: 90vw;
}
.podium .standings-table th,
.podium .standings-table td {
  padding: 6px 14px;
  font-size: 0.85em;
  text-align: left;
}
.podium .standings-table th {
  font-size: 0.75em;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.podium .standings-table tbody tr:nth-child(even) { background: rgba(255, 255, 255, 0.03); }
.podium .standings-table .rank { color: var(--muted); }
.podium .standings-table .horse-name { font-weight: bold; }
.podium .standings-table .horse-name .jockey {
  color: var(--muted);
  font-weight: normal;
  font-size: 0.9em;
}
.podium .standings-table .levelled-up { color: gold; font-weight: bold; }

.podium .dismiss {
  background: var(--accent);
  color: var(--bg);
  border: none;
  padding: 8px 16px;
  font: inherit;
  cursor: pointer;
  /* Pinned to a corner so it stays reachable even when a chart face is
     covering the whole screen. */
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 100;
}
.podium .dismiss:hover { filter: brightness(1.1); }

/* Rotating detail panel: standings + charts share one grid cell so the
   container sizes to the tallest face and faces can crossfade in place. */
.detail-cycle { display: grid; width: min(960px, 94vw); }
.detail-cycle > .detail-face { grid-area: 1 / 1; opacity: 0; transition: opacity 0.6s ease; pointer-events: none; }
.detail-cycle > .detail-face.is-active { opacity: 1; pointer-events: auto; }
@media (prefers-reduced-motion: reduce) { .detail-cycle > .detail-face { transition: none; } }

.chart-face { display: flex; flex-direction: column; gap: 8px; }
.chart-title { color: var(--accent); letter-spacing: 0.08em; text-transform: uppercase; font-size: 0.9rem; }
.chart-svg { width: 100%; height: auto; background: #0c0c0c; border: 1px solid #222; }
.chart-grid { stroke: #222; stroke-width: 1; }
.chart-line { stroke-linejoin: round; stroke-linecap: round; }
.chart-axis { fill: #888; font-size: 10px; }
.chart-legend { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: 0.85rem; color: #ccc; }
.legend-item { display: inline-flex; align-items: center; gap: 5px; }
.legend-chip { width: 10px; height: 10px; display: inline-block; }

/* When the panel flips to a chart, take over the whole viewport (the podium and
   standings sit behind it). The standings face stays under the podium in-flow. */
.detail-cycle > .chart-face {
  position: fixed;
  inset: 0;
  z-index: 50;
  justify-content: center;
  gap: 18px;
  padding: clamp(20px, 4vw, 64px);
  background: #0a0a0a;
}
.chart-face .chart-title { font-size: 1.4rem; text-align: center; }
.chart-face .chart-svg { height: 64vh; border: none; }
.chart-face .chart-legend { justify-content: center; gap: 8px 20px; font-size: 1.05rem; }
.chart-face .legend-chip { width: 14px; height: 14px; }

/* End-screen Season Standings face (league fixtures only) — reuses the
   .div-grid/.div-card/zone styling from the org standings renderer. */
.season-face { text-align: left; }
.season-face-head { text-transform: uppercase; letter-spacing: 0.18em; font-size: 0.8rem; color: #a68bd8; margin: 0 0 10px; }
.season-champion { display: flex; align-items: center; gap: 12px; border: 2px solid #ffd166; background: rgba(255, 209, 102, 0.08); padding: 10px 14px; margin: 0 0 12px; }
.season-champion-trophy { font-size: 1.8rem; }
.season-champion-label { font-size: 0.7rem; letter-spacing: 0.15em; color: #ffd166; }
.season-champion-name { font-size: 1.3rem; color: #fff; }
.season-champion-name small { color: #9ca3af; font-size: 0.85rem; }

/* ── Confetti ─────────────────────────────────────── */

.confetti {
  position: fixed;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 5;
}
.confetti span {
  position: absolute;
  top: -20px;
  width: 8px;
  height: 12px;
  animation: fall 3s linear forwards;
}
@keyframes fall {
  to { transform: translateY(110vh) rotate(720deg); }
}

/* ── Animations ───────────────────────────────────── */

/* Skeletal pivot points */
.leg-back              { transform-origin: 6px 16px; }   /* back hip  */
.leg-back-lower        { transform-origin: 6px 18px; }   /* back knee */
.leg-front             { transform-origin: 18px 16px; }  /* front hip */
.leg-front-lower       { transform-origin: 18px 18px; }  /* front knee */

/* Smooth sub-pixel rendering on animated legs */
.leg-back, .leg-front  { shape-rendering: auto; }

/* ── Hip swing ──────────────────────── */
@keyframes hip-back {
  0%   { transform: rotate(18deg); }   /* pushed back  */
  50%  { transform: rotate(-16deg); }  /* swung forward */
  100% { transform: rotate(18deg); }
}
@keyframes hip-front {
  0%   { transform: rotate(-16deg); }  /* reaching forward */
  50%  { transform: rotate(18deg); }   /* pushed back      */
  100% { transform: rotate(-16deg); }
}

/* ── Knee bend (nested inside hip group) ── */
@keyframes knee-back {
  0%        { transform: rotate(0deg); }     /* straight on ground */
  30%       { transform: rotate(-28deg); }   /* folds up as leg lifts */
  50%       { transform: rotate(-12deg); }   /* partially bent at full forward */
  80%       { transform: rotate(-20deg); }   /* bends on downswing */
  100%      { transform: rotate(0deg); }     /* straight on landing */
}
@keyframes knee-front {
  0%        { transform: rotate(0deg); }     /* straight at full reach */
  20%       { transform: rotate(20deg); }    /* bends as it pushes back */
  50%       { transform: rotate(0deg); }     /* straight at back */
  70%       { transform: rotate(28deg); }    /* folds as leg lifts forward */
  100%      { transform: rotate(0deg); }     /* straightens on landing */
}

/* ── Body bob ───────────────────────── */
@keyframes body-bob {
  0%, 100% { transform: translate(-50%, 0); }
  25%      { transform: translate(-50%, -1px); }
  75%      { transform: translate(-50%, 0.5px); }
}

/* ── Apply to live / pending horses ── */
.horse.live .leg-back,
.horse.pending .leg-back {
  animation: hip-back 0.4s ease-in-out infinite;
}
.horse.live .leg-back-lower,
.horse.pending .leg-back-lower {
  animation: knee-back 0.4s ease-in-out infinite;
}
.horse.live .leg-front,
.horse.pending .leg-front {
  animation: hip-front 0.4s ease-in-out infinite;
}
.horse.live .leg-front-lower,
.horse.pending .leg-front-lower {
  animation: knee-front 0.4s ease-in-out infinite;
}
.horse.live, .horse.pending {
  animation: body-bob 0.4s ease-in-out infinite;
}

/* ── Catalog view ─────────────────────────────────── */

:root {
  --rarity-common: #9ca3af;
  --rarity-rare: #60a5fa;
  --rarity-epic: #c084fc;
  --rarity-legendary: #fbbf24;
  --rarity-limited: #f472b6;
}

.catalog {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 24px;
  gap: 16px;
  max-width: 1280px;
  width: 100%;
  margin: 0 auto;
}
.catalog-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  border-bottom: 2px solid var(--panel);
  padding-bottom: 12px;
}
.catalog-header h1 {
  margin: 0;
  color: var(--accent);
  font-size: 1.75rem;
  letter-spacing: 0.08em;
  display: flex;
  align-items: center;
  gap: 0.4em;
}
.catalog-intro {
  color: var(--muted);
  margin: 0;
  max-width: 70ch;
  font-size: 0.95rem;
  line-height: 1.4;
}
.catalog-intro em { color: var(--text); font-style: normal; }

.catalog-controls {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 4px 0 8px;
}
.catalog-control-label {
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.75rem;
}
.palette-controls {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.palette-btn {
  background: var(--panel);
  color: var(--text);
  border: 2px solid var(--border);
  padding: 6px 14px;
  font: inherit;
  font-size: 0.95rem;
  letter-spacing: 0.04em;
  cursor: pointer;
  border-radius: 4px;
}
.palette-btn:hover { border-color: var(--accent); }
.palette-btn.active {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--bg);
}

.catalog-sections {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.rarity-heading {
  margin: 24px 0 8px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
  font-size: 1rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  display: flex;
  align-items: baseline;
  gap: 12px;
}
.rarity-heading .rarity-name { color: var(--accent); }
.rarity-heading.rarity-common .rarity-name    { color: var(--rarity-common); }
.rarity-heading.rarity-rare .rarity-name      { color: var(--rarity-rare); }
.rarity-heading.rarity-epic .rarity-name      { color: var(--rarity-epic); }
.rarity-heading.rarity-legendary .rarity-name { color: var(--rarity-legendary); }
.rarity-heading.rarity-limited .rarity-name { color: var(--rarity-limited); }
.rarity-meta {
  color: var(--muted);
  font-size: 0.7rem;
  letter-spacing: 0.06em;
}

.hat-row {
  display: flex;
  gap: 16px;
  padding: 12px 16px;
  background: var(--panel);
  border: 2px solid var(--border);
  border-radius: 6px;
  align-items: stretch;
}
.hat-meta {
  width: 13rem;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 4px;
}
.hat-name-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.hat-name {
  color: var(--text);
  font-size: 1.15rem;
  letter-spacing: 0.03em;
}
.hat-id {
  color: var(--muted);
  font-size: 0.7rem;
  font-family: ui-monospace, SFMono-Regular, monospace;
  letter-spacing: 0;
}
.hat-count {
  color: var(--muted);
  font-size: 0.8rem;
}
.rarity-badge {
  display: inline-block;
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  padding: 2px 8px;
  border-radius: 3px;
  color: var(--bg);
  text-transform: uppercase;
}
.rarity-badge.rarity-common    { background: var(--rarity-common); }
.rarity-badge.rarity-rare      { background: var(--rarity-rare); }
.rarity-badge.rarity-epic      { background: var(--rarity-epic); }
.rarity-badge.rarity-legendary { background: var(--rarity-legendary); }
.rarity-badge.rarity-limited { background: var(--rarity-limited); }

.hat-exclusive {
  margin-left: 0.5rem;
  padding: 0.1rem 0.4rem;
  border: 1px solid currentColor;
  border-radius: 3px;
  font-size: 0.7em;
  letter-spacing: 0.08em;
  opacity: 0.85;
}

.variants-strip {
  flex: 1;
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: flex-end;
}
.variant-card {
  width: 88px;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 6px 6px 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.variant-card-dark  { background: #0a0a0a; }
.variant-card-light { background: #3a3a3a; }
.variant-card .horse-sprite {
  width: 100%;
  height: auto;
  image-rendering: pixelated;
  shape-rendering: crispEdges;
}
.variant-card .variant-name {
  font-size: 0.7rem;
  color: var(--text);
  text-align: center;
  letter-spacing: 0.02em;
}

.catalog-footer {
  color: var(--muted);
  font-size: 0.85rem;
  margin: 24px 0 8px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
  line-height: 1.5;
}

.home .catalog-link {
  color: var(--muted);
  font-size: 0.85rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  border-bottom: 1px dashed var(--border);
  padding-bottom: 2px;
}
.home .catalog-link:hover { color: var(--accent); border-color: var(--accent); }

/* ── Error / fallback ─────────────────────────────── */

.error {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 32px;
  text-align: center;
}
.error h2 { color: #ff6b6b; margin: 0; }
.error a { color: var(--accent); }

/* ── TV mode (toggled via .tv on <body>; auto-applied on 21:9+, ≥1920px) ─ */

body.tv {
  --sprite-scale: 3;
  --horse-height: 66px;
  --label-col: 168px;
  --track-gap: 0px;
  font-size: 18px;
}

body.tv .race-header { padding: 8px 48px; }
body.tv .race-header h1 { font-size: 1.5rem; }
body.tv .race-header .meta { gap: 32px; font-size: 1rem; }

/* Lock the race to viewport height; track scrolls, header stays fixed */
body.tv .race { height: 100vh; min-height: 100vh; }
body.tv .track {
  padding: 12px 6px;
  overflow-y: auto;
  min-height: 0;
  scrollbar-width: none;
}
body.tv .track::-webkit-scrollbar { display: none; }

body.tv .lane-info-box { gap: 1px; padding: 3px 7px; }
body.tv .horse-label {
  font-size: 0.82rem;
  letter-spacing: 0.05em;
}
body.tv .horse-level { font-size: 0.5rem; padding: 1px 6px; }
body.tv .user-label { font-size: 0.75rem; }
body.tv .horse-tokens { font-size: 0.78rem; }
body.tv .horse-pace { font-size: 0.85rem; }
body.tv .horse-position { font-size: 0.85rem; }

body.tv .pending-banner { padding: 24px; font-size: 1.75rem; }

body.tv .home { gap: 48px; padding: 48px; }
body.tv .home p { font-size: 1.35rem; max-width: 50ch; }
body.tv .home input {
  font-size: 1.5rem;
  width: 240px;
  padding: 14px 20px;
}
body.tv .home button { font-size: 1.25rem; padding: 14px 28px; }

body.tv .btn { font-size: 1.1rem; padding: 12px 24px; }

body.tv .podium { gap: 32px; }
body.tv .podium h2 { font-size: 2.5rem; }
body.tv .podium li { padding: 24px; gap: 12px; }
body.tv .podium .place { font-size: 2.5rem; }
body.tv .podium .name { font-size: 1.25rem; }
body.tv .podium .tokens { font-size: 1.1rem; }

/* Race header action buttons sit alongside the meta items */
.race-header .meta .btn {
  padding: 3px 10px;
  font-size: 0.8rem;
}
body.tv .race-header .meta .btn {
  padding: 4px 14px;
  font-size: 0.9rem;
}
.tv-toggle[aria-pressed="true"] {
  background: #2563eb;
  color: #fff;
}

/* ── Achievement ticker ─────────────────────────────── */

.achievement-ticker {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--ticker-h);
  overflow: hidden;
  z-index: 9999;
  background: linear-gradient(180deg, rgba(12, 12, 12, 0.94), rgba(20, 20, 20, 0.98));
  border-top: 2px solid var(--ticker-accent);
  box-shadow: 0 -4px 18px rgba(0, 0, 0, 0.45);
  white-space: nowrap;
  transition: opacity 0.3s ease;
}
.achievement-ticker.is-empty { opacity: 0; pointer-events: none; }

/* The track is wider than the viewport; ticker.ts translates it leftward. */
.achievement-ticker-track {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  height: 100%;
  width: max-content;
  will-change: transform;
}

.achievement-ticker-item {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: baseline;
  gap: 0.45rem;
  height: 100%;
  padding: 0 1.5rem 0 0;
  font-size: 1.05rem;
  line-height: var(--ticker-h);
  color: #fff;
}
.achievement-ticker-xp {
  font-weight: bold;
  color: var(--ticker-accent);
}
.achievement-ticker-horse { font-weight: bold; }
.achievement-ticker-name { color: var(--ticker-accent); }
.achievement-ticker-desc { color: #aaa; }

/* Bullet between achievements within a pass. */
.achievement-ticker-sep {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  height: 100%;
  padding: 0 1.5rem;
  color: var(--ticker-accent);
  opacity: 0.55;
}

/* Loop seam — a blank break marking the end of one pass and start of the next. */
.achievement-ticker-gap {
  flex: 0 0 auto;
  height: 100%;
}

/* Reserve room so the ticker sits below the race instead of over the last lane.
   Only while it's actually showing achievements. */
.race:has(.achievement-ticker:not(.is-empty)) { padding-bottom: var(--ticker-h); }

/* ── Org manager ("/org-manager": create/join orgs, schedule, webhook) ─── */

.muted { color: var(--muted); }

/* Signed-out state (render/login.ts). */
.org-login {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  padding: 32px;
  text-align: center;
}
.org-login h1 {
  margin: 0;
  color: var(--accent);
  font-size: clamp(2rem, 6vw, 3rem);
  letter-spacing: 0.08em;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4em;
}
.org-login-subtitle {
  text-transform: uppercase;
  letter-spacing: 0.15em;
  font-size: 0.9rem;
}
.org-login p { color: var(--muted); margin: 0; max-width: 42ch; }

/* Two equal-weight paths (Option B): a new visitor and an existing CLI racer.
   Stacks under the org-manager breakpoint used elsewhere in this file. */
.org-login-lanes {
  display: flex;
  gap: 40px;
  align-items: stretch;
  justify-content: center;
  flex-wrap: wrap;
  width: 100%;
  max-width: 640px;
}
.org-login-lane {
  flex: 1 1 220px;
  max-width: 280px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  padding: 20px 22px;
  background: var(--panel);
  border: 2px solid var(--border);
  border-radius: 6px;
}
/* Inline commands keep the surrounding pixel font and size — the browser's
   default monospace is both a different face and a larger one, which breaks
   the line mid-sentence. */
.org-login code {
  font-family: inherit;
  font-size: inherit;
  color: var(--accent);
  background: var(--bg);
  padding: 0 4px;
  border-radius: 3px;
  white-space: nowrap;
}
@media (max-width: 720px) {
  .org-login-lanes { flex-direction: column; align-items: center; gap: 24px; }
  .org-login-lane { max-width: 320px; width: 100%; }
}

/* Google's compliant "Sign in with Google" light button, built to Google's
   published spec exactly (height/radius/padding/colours/type). It must look
   identical in every theme, so it is deliberately literal-coloured and set in
   Roboto rather than the site's tokens and pixel font — resetting both here
   and on the label span so neither the global font nor a theme's glow can
   reach it. */
.org-login .google-signin,
.org-login .google-signin-label {
  font-family: Roboto, Arial, sans-serif;
  font-weight: 500;
  font-size: 14px;
  letter-spacing: 0.25px;
  text-shadow: none;
  text-decoration: none;
}
.org-login .google-signin {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: 40px;
  box-sizing: border-box;
  padding: 0 12px;
  background: #FFFFFF;
  border: 1px solid #747775;
  border-radius: 4px;
  color: #1F1F1F;
}
.org-login .google-signin:hover { background: #F8F9FA; }
.google-mark { flex-shrink: 0; }

/* Reads as an error, not a hint: .org-login-error on the signed-out screen,
   .org-auth-error on the signed-in shell. Scoped under .org-login so it outranks
   `.org-login p`, which would otherwise paint the text muted. */
.org-login .org-login-error,
.org-auth-error {
  color: var(--error);
  background: var(--panel);
  border: 2px solid var(--error);
  border-radius: 4px;
  padding: 10px 16px;
  font-size: 0.95rem;
  margin: 0;
}
.org-auth-error { margin: 16px 24px 0; }

.org-login .cmd,
.cmd {
  display: inline-block;
  background: var(--panel);
  border: 2px solid var(--border);
  color: var(--accent);
  padding: 10px 18px;
  font-size: 1.15rem;
  letter-spacing: 0.05em;
  border-radius: 4px;
}

/* ── CLI device approval ("/cli": render/cli-approve.ts) ─────────────────── */

.cli-approve {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 32px;
  text-align: center;
}
.cli-approve h1 {
  margin: 0;
  color: var(--accent);
  font-size: clamp(1.6rem, 5vw, 2.4rem);
  letter-spacing: 0.06em;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4em;
}
/* The one load-bearing sentence on this page — kept visually louder than the
   rest of the copy, the same way .org-login-error reads as urgent. */
.cli-approve-warning {
  max-width: 46ch;
  margin: 0;
  color: var(--error);
  font-weight: bold;
  font-size: 1rem;
}
.cli-approve-warning-detail {
  max-width: 46ch;
  margin: 0;
  font-size: 0.85rem;
}
.cli-approve-form {
  width: 100%;
  max-width: 360px;
  align-items: stretch;
  text-align: left;
}
.cli-approve-form input {
  font-family: inherit;
  text-transform: uppercase;
  letter-spacing: 0.15em;
  font-size: 1.2rem;
  text-align: center;
  color: var(--text);
  background: var(--bg);
  border: 2px solid var(--border);
  border-radius: 4px;
  padding: 10px;
  width: 100%;
}
.cli-approve-form input:focus { outline: none; border-color: var(--accent); }
/* Structural attribution for the attacker-controlled device label: framed as
   a quote FROM the requesting machine, never styled like the page's own
   voice, so a label that reads like our copy still cannot pass as ours. */
.cli-approve-label-block {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.cli-approve-label {
  margin: 0;
  padding: 10px 14px;
  background: var(--bg);
  border: 2px dashed var(--border);
  border-radius: 4px;
  color: var(--muted);
  font-style: italic;
  word-break: break-word;
  /* Defense in depth: the server now rejects bidi-override/format characters
     at intake, but this keeps the label's own text direction from leaking
     into the surrounding page if one ever reaches render by another route. */
  unicode-bidi: isolate;
}
.cli-approve-success {
  color: var(--accent);
  font-weight: bold;
  margin: 0;
}
.cli-approve-return-note {
  max-width: 42ch;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 10px 16px;
  font-size: 0.9rem;
}

/* ── Google account linking ("/link": render/link-google.ts) ─────────────── */

.link-google {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 32px;
  text-align: center;
}
.link-google h1 {
  margin: 0;
  color: var(--accent);
  font-size: clamp(1.6rem, 5vw, 2.4rem);
  letter-spacing: 0.06em;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4em;
}
.link-google-explain,
.link-google-status {
  max-width: 46ch;
  margin: 0;
  color: var(--muted);
}

/* Signed-in shell (org-manager/index.ts): sidebar + tabs + tab body. */
.org-manager {
  flex: 1;
  display: flex;
  align-items: stretch;
  min-height: 0;
}
.org-side { flex: 0 0 240px; }
.org-sidebar {
  height: 100%;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 20px 16px;
  background: var(--panel);
  border-right: 2px solid var(--border);
}
.org-side-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 0.75rem;
}
.org-logout {
  background: none;
  border: 1px solid var(--border);
  color: var(--accent);
  font: inherit;
  font-size: 0.75rem;
  letter-spacing: 0.03em;
  padding: 3px 10px;
  border-radius: 4px;
  cursor: pointer;
}
.org-logout:hover { border-color: var(--accent); }
.org-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  overflow-y: auto;
}
.org-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 10px 12px;
  background: var(--bg);
  /* Explicit, because the account row is a <button>: without this it takes the
     UA's buttontext, which is near-black on the black background. The org rows
     are divs and inherit --text, so they never showed the problem. */
  color: var(--text);
  border: 2px solid var(--border);
  border-radius: 4px;
  letter-spacing: 0.02em;
  cursor: pointer;
}
.org-row:hover { border-color: var(--accent); }
.org-row.selected { border-color: var(--accent); background: rgba(209, 213, 219, 0.08); color: var(--accent); }
.pill {
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 2px 8px;
  border-radius: 3px;
  background: var(--border);
  color: var(--muted);
}
.org-row.selected .pill { color: var(--bg); background: var(--accent); }

/* .org-actions is reused both as the stacked sidebar action buttons and the
   inline save/clear row inside a tab panel. */
.org-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.org-sidebar .org-actions { flex-direction: column; margin-top: auto; }
.org-create, .org-join, .org-link-google, .org-btn {
  background: var(--accent);
  color: var(--bg);
  border: none;
  padding: 9px 16px;
  font: inherit;
  font-size: 0.85rem;
  font-weight: bold;
  letter-spacing: 0.03em;
  border-radius: 4px;
  cursor: pointer;
}
.org-create:hover, .org-join:hover, .org-link-google:hover, .org-btn:hover { filter: brightness(1.1); }
.org-sidebar .org-create, .org-sidebar .org-join, .org-sidebar .org-link-google { width: 100%; }
.org-linked-email {
  text-align: center;
  font-size: 0.75rem;
  letter-spacing: 0.03em;
  padding: 9px 16px;
  border: 1px solid var(--border);
  border-radius: 4px;
}

.org-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  padding: 24px;
  gap: 16px;
}
.org-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  border-bottom: 2px solid var(--panel);
}
.org-tab {
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  padding: 10px 16px;
  color: var(--muted);
  font: inherit;
  font-size: 0.95rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  cursor: pointer;
}
.org-tab:hover { color: var(--text); }
.org-tab.on { color: var(--accent); border-bottom-color: var(--accent); }
.org-tabbody { flex: 1; }

.org-panel {
  display: flex;
  flex-direction: column;
  gap: 18px;
  max-width: 640px;
  padding: 20px 24px;
  background: var(--panel);
  border: 2px solid var(--border);
  border-radius: 6px;
}
.org-field { display: flex; flex-direction: column; gap: 6px; font-size: 1rem; }
.label {
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.72rem;
}
label.label {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  max-width: 260px;
}
label.label:has(input[type="checkbox"]) { flex-direction: row; align-items: center; max-width: none; }
label.label input, label.label select, label.label textarea {
  font-family: inherit;
  text-transform: none;
  letter-spacing: normal;
  font-size: 1rem;
  color: var(--text);
  background: var(--bg);
  border: 2px solid var(--border);
  border-radius: 4px;
  padding: 7px 10px;
  width: 100%;
}
label.label input[type="checkbox"] { width: auto; }
label.label textarea { resize: vertical; }
label.label input:focus, label.label select:focus, label.label textarea:focus { outline: none; border-color: var(--accent); }
label.label input:disabled, label.label select:disabled, label.label textarea:disabled { opacity: 0.5; }

.org-token {
  display: inline-block;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 5px 12px;
  margin-right: 8px;
  color: var(--accent);
  font-size: 1rem;
  letter-spacing: 0.04em;
}
.org-copy {
  background: none;
  border: 1px solid var(--border);
  color: var(--accent);
  padding: 4px 12px;
  font: inherit;
  font-size: 0.8rem;
  border-radius: 4px;
  cursor: pointer;
}
.org-copy:hover { border-color: var(--accent); background: rgba(209, 213, 219, 0.08); }
.warn { color: #fbbf24; font-size: 0.85rem; margin: 4px 0 0; }

.org-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--bg);
  border: 2px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
}
.org-table th {
  font-size: 0.72rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  text-align: left;
  font-weight: normal;
  padding: 9px 14px;
  border-bottom: 1px solid var(--border);
}
.org-table td { padding: 10px 14px; font-size: 0.95rem; }
.org-table tbody tr:nth-child(even) { background: rgba(255, 255, 255, 0.04); }

/* Members tab linkage columns (owner-only). Distinguishable by glyph as well
   as colour — tick/cross/dash reads the same even where colour doesn't. */
.org-tick { color: var(--live); text-align: center; }
.org-cross { color: var(--error); text-align: center; }
.org-na { color: var(--muted); text-align: center; }

.org-days { display: flex; flex-wrap: wrap; gap: 10px; }
.org-day {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  font-size: 0.85rem;
  cursor: pointer;
}
.org-day:has(input:checked) { border-color: var(--accent); color: var(--accent); }
.org-day input[type="checkbox"] { accent-color: var(--accent); }

/* Race settings tab (render/tabs/race-settings.ts). One collapsible section per
   scoring mechanic: the header carries the on/off switch, the tuning lives
   inside so the page stays quiet until someone wants to tune it. */
/* No border of its own — it sits inside the tab's .org-panel, and a box inside
   a box just doubles the rule. Sibling sections are separated instead. */
.org-accordion + .org-accordion { border-top: 1px solid var(--border); }
.org-accordion-head {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 4px 0;
  cursor: pointer;
  list-style: none;
}
.org-accordion-head::-webkit-details-marker { display: none; }
.org-accordion-heading { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
/* Disclosure caret, inline with the title so it reads as part of that line
   rather than floating beside a wrapped description. */
.org-accordion-title::before {
  content: '';
  display: inline-block;
  vertical-align: middle;
  margin-right: 8px;
  border-left: 5px solid var(--muted);
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform 120ms ease;
}
.org-accordion[open] .org-accordion-title::before { transform: rotate(90deg); }
.org-accordion-title { color: var(--text); font-size: 1rem; }
.org-accordion-sub { font-size: 0.85rem; }
.org-accordion-body {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 16px 0 6px;
}

/* On/off switch. A real button with role="switch" rather than a checkbox, so
   the header reads as a control rather than a form field. */
.org-switch {
  flex: none;
  position: relative;
  width: 40px; height: 22px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease;
}
.org-switch::after {
  content: '';
  position: absolute;
  top: 2px; left: 2px;
  width: 16px; height: 16px;
  border-radius: 50%;
  background: var(--muted);
  transition: transform 120ms ease, background 120ms ease;
}
.org-switch[aria-checked="true"] { background: var(--accent); border-color: var(--accent); }
.org-switch[aria-checked="true"]::after { transform: translateX(18px); background: var(--bg); }
.org-switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.org-switch:disabled { cursor: not-allowed; opacity: 0.5; }

.org-sliders { display: flex; flex-direction: column; gap: 14px; }
.org-slider-row { display: flex; flex-direction: column; gap: 4px; }
.org-slider-row input[type="range"] { width: 100%; accent-color: var(--accent); }
.org-slider-value { font-size: 0.85rem; color: var(--text); }
.org-readout { display: flex; flex-direction: column; gap: 4px; font-size: 0.9rem; }
.org-readout p { margin: 0; }
.org-readout strong { color: var(--accent); }

.org-secret {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px 14px;
  background: #0d0d0d;
  border: 1px dashed var(--accent);
  border-radius: 4px;
  color: var(--accent);
}
.org-secret code { font-size: 1rem; letter-spacing: 0.03em; word-break: break-all; }

/* Access tab (render/tabs/access.ts). Lays a checkbox and its label out on one
   row, against .org-panel's column default. */
.org-access-toggle { flex-direction: row !important; align-items: center; gap: 8px; max-width: none !important; }
/* A sub-section within the tab's own .org-panel, not a nested panel — a box
   inside a box would just double the border. A top rule is enough to set the
   rotation controls apart from the toggles above. */
.org-access-rotate {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}
.org-member-remove {
  background: none;
  border: 1px solid var(--error);
  color: var(--error);
  font: inherit;
  font-size: 0.8rem;
  padding: 4px 12px;
  border-radius: 4px;
  cursor: pointer;
}
.org-member-remove:hover { background: var(--error-wash); }

/* Account sidebar entry (render/sidebar.ts): styled like an org row so it
   reads as a peer entry, not a lesser one, even though it sits above the org
   list rather than inside it. */
.org-account-row {
  width: 100%;
  text-align: left;
  font: inherit;
  letter-spacing: 0.02em;
}

/* Account view (render/account.ts) — a sidebar-level view rendered straight
   into .org-main with no .org-tabs nav, since it is reachable with zero
   organisations and must not depend on one being selected. */
.org-account { display: flex; flex-direction: column; gap: 18px; }
.org-account-email-value { color: var(--text); }
.org-account-devices .org-table { max-width: 720px; }
/* The account-level credential the device table cannot list. Warning-coloured
   because it is the one thing on this view that revoking cannot remove. */
.org-account-legacy {
  max-width: 720px;
  margin: 12px 0 0;
  font-size: 0.85rem;
  color: var(--muted);
  border-left: 2px solid var(--error);
  padding-left: 10px;
}
.org-account-legacy strong { color: var(--error); }
/* The site has no global link colour; these two are the only anchors in the
   org manager's own copy, so they take the accent rather than browser blue. */
.org-account-devices a, .org-empty a { color: var(--accent); }
/* Attacker-influenced text, sanitised at intake — this keeps a label's own
   text direction from leaking into the surrounding row as defense in depth,
   the same treatment .cli-approve-label gives the device label at /cli. */
.org-device-label { unicode-bidi: isolate; }
.org-device-revoke {
  background: none;
  border: 1px solid var(--error);
  color: var(--error);
  font: inherit;
  font-size: 0.8rem;
  padding: 4px 12px;
  border-radius: 4px;
  cursor: pointer;
}
.org-device-revoke:hover { background: var(--error-wash); }

/* Preview-only scaffolding (site/src/preview-org-manager.ts) — not emitted by
   the render modules, just labels stacking the extra tab screenshots. */
.org-preview-section { padding: 8px 24px 24px; border-top: 2px solid var(--panel); }
.org-preview-heading {
  margin: 16px 0 12px;
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 0.9rem;
}

@media (max-width: 720px) {
  .org-manager { flex-direction: column; }
  .org-side { flex: 0 0 auto; }
  .org-sidebar { border-right: none; border-bottom: 2px solid var(--border); }
  .org-sidebar .org-actions { flex-direction: row; margin-top: 0; }
  .org-sidebar .org-create, .org-sidebar .org-join, .org-sidebar .org-link-google { width: auto; flex: 1; }
}

/* Live-order ticker rows — share the achievement-ticker container/track. */
.ticker-order {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4em;
  padding: 0 0.9rem;
  white-space: nowrap;
}
.ticker-order-pos { color: var(--muted); }
.ticker-order-name { color: var(--text); }
.ticker-order-val {
  color: #d98a8a;                     /* gap-to-leader */
  font-variant-numeric: tabular-nums;
}
.ticker-order-val--leader { color: #7bed9f; }  /* leader’s absolute tokens */

/* Projected league-standings rows — share the ticker container/track. */
.ticker-standing {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4em;
  padding: 0 0.9rem;
  white-space: nowrap;
}
.ticker-standing-pos { color: var(--muted); }
.ticker-standing-name { color: var(--text); }
.ticker-standing-total {
  color: var(--text);
  font-weight: bold;
  font-variant-numeric: tabular-nums;
}
.ticker-standing-total--leader { color: #ffd166; }  /* division leader’s projected points */
.ticker-standing-gain {
  color: #7bed9f;                     /* points this fixture would add */
  font-variant-numeric: tabular-nums;
}

.ticker-group-label {
  color: var(--bg);
  background: var(--accent);
  padding: 0 0.4rem;
  font-size: 0.85em;
  letter-spacing: 0.06em;
  margin-right: 0.2em;
}
.ticker-group-label.ticker-div-1 { background: #ffd166; color: #0b0b0f; }
.ticker-group-label.ticker-div-2 { background: #4db8ff; color: #0b0b0f; }
.ticker-group-label.ticker-div-3 { background: #a68bd8; color: #0b0b0f; }
.ticker-group-sep {
  color: #6b4f95;
  padding: 0 2.4rem;
}
.ticker-section-gap {
  display: inline-block;
  width: 150px;
}
/* League tickers space the order block out from the achievements (≥600px). */
.ticker-section-gap--wide {
  width: 600px;
}

/* End-screen detail-cycle nav: prev/next + page dots. */
.detail-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.9rem;
  /* Pinned bottom-centre, above the chart face — mirrors the dismiss button.
     A chart face is `position: fixed; z-index: 50` and covers the whole screen,
     so an in-flow nav gets painted over (invisible on the graph pages). Fixing
     it here keeps the pager reachable on every face. */
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: 24px;
  z-index: 100;
}
.detail-nav-btn {
  background: var(--panel);
  color: var(--text);
  border: 2px solid var(--border);
  font-family: inherit;
  font-size: 1.1rem;
  line-height: 1;
  padding: 0.15rem 0.7rem;
  cursor: pointer;
}
.detail-nav-btn:hover { border-color: var(--accent); }
.detail-nav-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.facedots {
  display: flex;
  align-items: center;
  gap: 0.45rem;
}
.facedot {
  width: 9px;
  height: 9px;
  padding: 0;
  border: none;
  background: var(--border);
  cursor: pointer;
}
.facedot.is-active { background: var(--accent); }
.facedot:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: no-preference) {
  .facedot { transition: background 0.2s ease; }
}

/* Racing tab — mode picker + per-division league editor */
.org-racing-modes { display: flex; gap: 18px; margin: 6px 0 12px; flex-wrap: wrap; }
.org-racing-mode { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
.org-racing-warn:not(:empty) { color: #ffcc55; margin: 6px 0; font-size: 0.85rem; }
.org-div-note { font-size: 0.8rem; margin: 2px 0 8px; }
.org-divisions { display: flex; flex-direction: column; gap: 4px; margin: 6px 0; }
.org-div-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border: 1px solid #3a3a4a; border-radius: 4px; }
.org-div-n { opacity: 0.6; width: 1.2rem; text-align: center; }
.org-div-name { flex: 1 1 auto; min-width: 100px; }
.org-div-cap input { width: 56px; }
.org-div-overflow { opacity: 0.6; font-size: 0.8rem; }
.org-div-remove { margin-left: auto; padding: 2px 8px; }
.org-div-swap { display: flex; align-items: center; gap: 8px; padding: 2px 8px 2px 30px; opacity: 0.9; font-size: 0.85rem; }
.org-div-swapn { width: 48px; }
.org-div-swaptext { opacity: 0.6; }
.org-div-summary:not(:empty) { margin: 10px 0; padding: 8px 12px; border: 1px dashed #3a3a4a; border-radius: 4px; }
.org-error:not(:empty) { color: #ff6b6b; margin: 8px 0; font-size: 0.85rem; }
@media (max-width: 600px) { .org-div-row { flex-wrap: wrap; } }

/* ---- League standings (public org page) ---- */
.org-standings { margin: 0 0 26px; }
.season-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.season-line { color: var(--muted); font-size: 1.05rem; }
.season-prog { flex-basis: 100%; height: 5px; background: #26262e; border: 1px solid var(--border); margin: 8px 0 4px; }
.season-prog > i { display: block; height: 100%; background: #ffd166; }
.org-eyebrow { text-transform: uppercase; letter-spacing: 0.22em; font-size: 0.78rem; color: #a68bd8; margin: 22px 0 10px; }

.div-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 12px; align-items: start; }
.div-card { border: 2px solid var(--border); background: var(--panel); }
.div-card-h { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.7rem; border-bottom: 2px solid #26262e; }
.div-card-name { font-size: 1.05rem; display: flex; align-items: center; gap: 0.45rem; }
.div-chip { font-size: 0.7rem; padding: 0 0.4rem; color: #0b0b0f; letter-spacing: 0.06em; }
.div-card.d1 .div-chip { background: #ffd166; }
.div-card.d2 .div-chip { background: #4db8ff; }
.div-card.d3 .div-chip { background: #a68bd8; }
.div-flight { font-size: 0.68rem; letter-spacing: 0.12em; color: var(--muted); }
.div-empty { color: var(--muted); padding: 0.6rem 0.7rem; font-size: 0.9rem; margin: 0; }

.org-standings table { width: 100%; border-collapse: collapse; font-size: 1.05rem; font-variant-numeric: tabular-nums; }
.org-standings td { padding: 0.26rem 0.7rem; }
.org-standings .rank { color: var(--muted); width: 2.1rem; }
.org-standings .nm small { color: var(--muted); font-size: 0.78rem; }
.org-standings .pts { text-align: right; width: 3.2rem; }
.org-standings tr.lead .rank { color: #ffd166; }
.org-standings tr.lead .nm { color: #fff; }
.org-standings tr.promote { background: rgba(74, 222, 128, 0.10); }
.org-standings tr.promote .rank { color: #4ade80; }
.org-standings tr.relegate { background: rgba(255, 107, 107, 0.09); }
.org-standings tr.relegate .rank { color: #ff6b6b; }
.org-standings .zone td { padding: 0.05rem 0.7rem; font-size: 0.6rem; letter-spacing: 0.18em; text-transform: uppercase; }
.org-standings .zone.up td { color: #4ade80; border-bottom: 1px dashed rgba(74, 222, 128, 0.45); }
.org-standings .zone.down td { color: #ff6b6b; border-top: 1px dashed rgba(255, 107, 107, 0.45); }

/* ── About ─────────────────────────────────────────── */
.about {
  flex: 1;
  width: 100%;
  max-width: 720px;
  margin: 0 auto;
  padding: 32px 24px 64px;
}
.about h1 {
  color: var(--accent);
  font-size: clamp(1.8rem, 5vw, 2.6rem);
  letter-spacing: 0.06em;
  display: flex;
  align-items: center;
  gap: 0.4em;
  margin: 0 0 8px;
}
.about-current { color: var(--muted); letter-spacing: 0.05em; margin: 0 0 28px; }
.about-current b { color: var(--text); }
.about-timeline { position: relative; padding-left: 26px; }
.about-timeline::before {
  content: "";
  position: absolute;
  left: 7px; top: 6px; bottom: 6px;
  width: 2px;
  background: var(--border);
}
.about-entry { position: relative; margin-bottom: 22px; }
.about-entry::before {
  content: "";
  position: absolute;
  left: -23px; top: 8px;
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--bg);
}
.about-row { display: flex; align-items: center; gap: 10px; }
.about-tag {
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  font-weight: bold;
  padding: 1px 7px;
  border-radius: 3px;
}
.about-tag.site { color: #7dd3fc; border: 1px solid #38607a; }
.about-tag.cli  { color: #4ade80; border: 1px solid #1f5a1f; }
.about-ver { color: var(--text); }
.about-date { color: var(--muted); font-size: 0.85rem; margin-left: auto; }
.about-changes { color: var(--muted); margin: 4px 0 0; padding-left: 1.1em; }
.about-changes li { margin: 2px 0; }
.about-home { color: var(--muted); text-decoration: none; }
.about-home:hover { color: var(--accent); }

/* ── Mid-race graph popup ─────────────────────────────────────────────── */
/* Sizing comes from .btn so it matches the Org/Home buttons beside it. */

.race-graphs { position: fixed; inset: 0; z-index: 200; }
.race-graphs-backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.72); }
.race-graphs-panel {
  position: absolute;
  inset: 6% 5%;
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 2px solid var(--border);
  padding: 0.8rem 1rem 1rem;
}
.race-graphs-top { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.race-graphs-tabs, .race-graphs-divisions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.race-graphs-divisions { margin-top: 0.7rem; }
.race-graphs-tab, .race-graphs-div, .race-graphs-close {
  background: transparent;
  color: var(--text);
  border: 2px solid var(--border);
  font-family: inherit;
  font-size: 0.95rem;
  padding: 0.2rem 0.7rem;
  cursor: pointer;
}
.race-graphs-tab:hover, .race-graphs-div:hover, .race-graphs-close:hover { border-color: var(--accent); }
.race-graphs-tab:focus-visible, .race-graphs-div:focus-visible, .race-graphs-close:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.race-graphs-tab[aria-selected='true'], .race-graphs-div[aria-selected='true'] {
  border-color: var(--accent);
  color: var(--accent);
}
.race-graphs-body { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; }
/* Override the end-screen sizing, which is set by unscoped `.chart-face` rules.
   buildFace's faces also carry `.detail-face` (shared with the end-screen
   cycler), but the end-screen hiding rules at lines 731 and 747 both require
   a `.detail-cycle` ancestor, which these popup faces never have — they live
   under `.race-graphs-body`, not `.detail-cycle`. Loosening either rule to
   drop that `.detail-cycle` qualifier would make it apply here too and blank
   the popup (opacity: 0, never given `.is-active`). */
.race-graphs-body .chart-face { width: 100%; gap: 10px; }
.race-graphs-body .chart-face .chart-svg { height: 52vh; }
.race-graphs-body .chart-face .chart-title { font-size: 1.05rem; }
.race-graphs-empty { color: #999; font-size: 1.1rem; text-align: center; }

/* ── Themes ─────────────────────────────────────────── */

/* "Derby" — the original palette — lives in :root above, so it is also the
   no-JS / pre-hydration default. Each theme below only overrides the tokens it
   changes; anything unlisted falls through to :root.

   --track-blend re-tints the opaque dirt sprite: `luminosity` keeps the pixel
   art's luminance and takes hue/saturation from --lane, so one PNG serves every
   theme. Derby stays `normal` (sprite untouched, byte-identical rendering). */

:root[data-theme="midnight"] {
  --bg: #05070f;
  --panel: #101627;
  --text: #dce6f5;
  --muted: #7b8aa5;
  --accent: #7dd3fc;
  --border: #26334d;
  --lane: #1b3a5c;
  --track-blend: luminosity;
  --track-shade: rgba(4, 10, 24, 0.55);
  --rail: #cfe6ff;
  --label-box: #121a2b;
  --live: #5eead4;
  --ticker-accent: #7dd3fc;
}

:root[data-theme="turf"] {
  --bg: #06120a;
  --panel: #102015;
  --text: #eaf7e2;
  --muted: #8aa88f;
  --accent: #86efac;
  --border: #22412c;
  --lane: #2f6b32;
  --track-blend: luminosity;
  --track-shade: rgba(3, 18, 8, 0.35);
  --rail: #f2fff2;
  --label-box: #12241a;
  --live: #bef264;
  --ticker-accent: #fde047;
}

:root[data-theme="photo"] {
  --bg: #0a0a0a;
  --panel: #191919;
  --text: #ededed;
  --muted: #8f8f8f;
  --accent: #ffffff;
  --border: #3d3d3d;
  --lane: #6b6b6b;   /* zero saturation → luminosity blend greys the dirt out */
  --track-blend: luminosity;
  --track-shade: rgba(0, 0, 0, 0.30);
  --rail: #ffffff;
  --label-box: #1c1c1c;
  --live: #d4d4d4;
  --ticker-accent: #e5e5e5;
}

/* ── London ─────────────────────────────────────────────
   A wet night on the Underground. The palette is the Tube map's own: Johnston
   blue in the lanes, roundel red as the accent, Circle yellow on the ticker,
   District green for LIVE — four strong hues that only work together because
   everything holding them is slate.

   Kept clear of Midnight, the other blue theme: Midnight is a muted steel blue
   under a cyan accent, this is a saturated indigo under red, so the two never
   read as variants in the picker. */
:root[data-theme="london"] {
  --bg: #0b0f14;
  --panel: #151c25;
  --text: #e6ecf2;
  --muted: #8794a5;
  /* Roundel red, lifted off #E32017 — the true red lands at 3.8:1 on --panel,
     and --accent is text on selected tabs, not just a border colour. */
  --accent: #ff4b3e;
  --border: #2a3441;
  /* Asphalt at night: near-neutral, only a breath of blue in it. The rest of
     the theme is slate, and a road that picks up the same slate reads as
     painted metal rather than tarmac.

     This is the one theme whose lanes are not the dirt sprite re-tinted (see
     the .lane-track override below), so --lane is the surface colour outright
     rather than a hue for `luminosity` to push into the sprite — which is also
     why --track-blend and --track-shade are absent here. */
  --lane: #24262b;
  --rail: #f2f6fa;
  --label-box: #121922;
  --live: #4ad991;
  --ticker-accent: #ffd300;
}

/* They are racing up a road, so the track is a road: the dirt sprite goes
   entirely — asphalt has no grain worth drawing at this size — and the lanes
   are divided by dashed markings instead of by the sprite's kerb bands. The
   square corners go with it; roads do not have rounded ends. */
:root[data-theme="london"] .lane-track {
  background: var(--lane);
  border-radius: 0;
}
/* A marking divides one lane from the one above it, so the first lane has none
   — there is nothing above it to be divided from. Long dash, longer gap, which
   is how road markings are actually spaced and what keeps them from reading as
   a dotted rule. */
:root[data-theme="london"] .lane + .lane .lane-track {
  background-image: repeating-linear-gradient(
    90deg,
    var(--rail) 0 34px,
    transparent 34px 62px
  );
  background-repeat: no-repeat;
  background-size: 100% 3px;
  background-position: left top;
}

/* Victorian lamps down both kerbs. Hung off the first and last lanes rather
   than off .track, which stretches to fill whatever height is left over and
   would have stood them well below the road.

   Both rows are absolutely positioned so they paint over their neighbours:
   z-index 2 clears the z-index 1 the horses carry and the z-index 0 stacking
   context the crowd strip sits in. `left` matches .lane-info's flex basis, so
   they start exactly where the road does and never cross the name plates. */
:root[data-theme="london"] .lane:last-child { position: relative; }

/* Far kerb: hung off the bottom of the crowd strip, which puts the bases on the
   boundary between the grandstand and the road. It cannot hang off the first
   lane instead — .track carries `overflow: auto` so it can scroll a long field,
   and anything reaching above the first lane is clipped at the road's edge.
   Being a positioned pseudo-element, it paints over the crowd sprites, which is
   right: this row stands in front of the grandstand, not among it.

   Shorter than the near row because it is further away, and offset half a span
   so the two rows never line up into columns. */
:root[data-theme="london"] .crowd::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 92px;
  pointer-events: none;
  background: url('img/london-lamp.svg') repeat-x 96px bottom / auto 92px;
}

/* Near kerb: bases below the last lane, so the posts rise across the lower
   lanes. A horse passing behind a post is the depth cue that sells the street. */
:root[data-theme="london"] .lane:last-child::after {
  content: '';
  position: absolute;
  left: var(--label-col);
  right: 0;
  bottom: -18px;
  height: 116px;
  z-index: 2;
  pointer-events: none;
  background: url('img/london-lamp.svg') repeat-x left bottom / auto 116px;
}

/* Fog, in front of the page rather than behind it — fog is the thing that comes
   between you and what you are looking at, so a backdrop layer (Matrix's trick)
   would be the wrong way round. That puts it over live text, so the banks peak
   at 0.10 alpha: enough to read as weather drifting past, not enough to move
   any foreground pair off its contrast ratio. z-index 2 matches the scanlines;
   no theme wears both.

   One element, not three: the banks are three gradients in one tile, so the
   whole thing is a single composited layer moved by `transform` — the same
   performance rule the Matrix backdrop is built on. Tiled at exactly 100vw and
   drifting exactly 100vw, so the loop has no seam. */
:root[data-theme="london"] body::before {
  content: '';
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  width: 200vw;
  z-index: 2;
  pointer-events: none;
  /* Every bank fades out well inside its own 100vw tile (centre ± 0.7 × the
     horizontal radius, the point the stop reaches zero alpha). A bank that
     overruns is clipped at the tile edge instead of bleeding into the next
     one, which puts a hard vertical seam down the page as the layer drifts.
     The ~2vw of clear air left either side of the tile is the seam, and clear
     air has no edge to see. Stops fade to an alpha-0 *grey* rather than
     `transparent`, which is alpha-0 black and greys the falloff. */
  background:
    radial-gradient(34vw 40vh at 25vw 78vh, rgba(197, 214, 232, 0.13), rgba(197, 214, 232, 0) 70%),
    radial-gradient(30vw 32vh at 58vw 46vh, rgba(186, 205, 226, 0.09), rgba(186, 205, 226, 0) 70%),
    radial-gradient(24vw 30vh at 82vw 92vh, rgba(205, 219, 234, 0.12), rgba(205, 219, 234, 0) 70%);
  background-size: 100vw 100%;
  background-repeat: repeat-x;
}
/* Slow enough that you never catch it moving — you just notice the page looks
   different than it did a minute ago. */
@media (prefers-reduced-motion: no-preference) {
  :root[data-theme="london"] body::before {
    animation: london-fog-drift 90s linear infinite;
  }
}
@keyframes london-fog-drift {
  from { transform: translateX(0); }
  to   { transform: translateX(-100vw); }
}
/* Same rule the crowd and the chromatic wash follow — the page stops moving
   once the race is over. The fog itself stays; only the drift halts. */
:root[data-theme="london"]:has(.race.finished) body::before {
  animation-play-state: paused;
}


/* Rain, in the same .rain-backdrop element Matrix uses — it is built for every
   theme in theme.ts and hidden in CSS by whichever ones do not want it, so this
   costs no new markup.

   In FRONT of the page (z-index 3, above the fog) rather than behind it, which
   is the opposite of Matrix. Glyph rain is a backdrop you read the page over;
   real rain is weather between you and the thing you are looking at, and a
   racecourse seen through it is the entire point of the theme.

   Everything that matters about the performance of this is inherited from the
   Matrix fix (PR #19): the fall is a `transform`, so each layer is rasterised
   once and every later frame is a matrix change on the compositor. Nothing here
   animates background-position, and nothing here is per-lane — this is one
   fixed layer for the whole page, so its cost does not grow with the field. */
:root[data-theme="london"] .rain-backdrop {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  overflow: hidden;
  /* Low, because this sheet crosses live text. Tuned against the horse names at
     the left of the track, which are the smallest thing it has to stay behind;
     at 0.5 the streaks started competing with them rather than sitting behind. */
  opacity: 0.32;
}
/* Each layer carries a tile of headroom on the two edges it travels away from —
   above, and to the right — so that after moving exactly one tile in each axis
   it still covers the screen and the pattern lands back on itself. The loop is
   invisible without the layer ever being redrawn. */
:root[data-theme="london"] .rain-backdrop > i {
  position: absolute;
  left: 0;
  bottom: 0;
  background-image: url('img/london-rain.svg');
  background-repeat: repeat;
}
/* One tile at three scales, each travelling its own size over the same
   duration — which is where the parallax comes from. The x-offsets stop the
   three landing on top of each other and showing the repeat. */
:root[data-theme="london"] .rain-backdrop > i:nth-child(1) {
  top: -480px;
  right: -120px;
  background-size: 120px 480px;
  background-position: 0 0;
}
:root[data-theme="london"] .rain-backdrop > i:nth-child(2) {
  top: -336px;
  right: -84px;
  background-size: 84px 336px;
  background-position: 40px 0;
}
:root[data-theme="london"] .rain-backdrop > i:nth-child(3) {
  top: -672px;
  right: -168px;
  background-size: 168px 672px;
  background-position: 70px 0;
}
/* Down and to the left, one tile of each per cycle. The x and y distances are
   the tile's own width and height, which is what keeps the loop seamless and
   the streaks travelling along their own slope. */
@keyframes london-rain-fall-1 { to { transform: translate3d(-120px, 480px, 0); } }
@keyframes london-rain-fall-2 { to { transform: translate3d(-84px, 336px, 0); } }
@keyframes london-rain-fall-3 { to { transform: translate3d(-168px, 672px, 0); } }

@media (prefers-reduced-motion: no-preference) {
  :root[data-theme="london"] .rain-backdrop > i:nth-child(1) { animation: london-rain-fall-1 0.9s linear infinite; }
  :root[data-theme="london"] .rain-backdrop > i:nth-child(2) { animation: london-rain-fall-2 0.9s linear infinite; }
  :root[data-theme="london"] .rain-backdrop > i:nth-child(3) { animation: london-rain-fall-3 0.9s linear infinite; }
}
/* Same rule the crowd, the bus and the chromatic wash follow — the page stops
   moving once the race is over. */
:root[data-theme="london"]:has(.race.finished) .rain-backdrop > i {
  animation-play-state: paused;
}

/* ── The city ───────────────────────────────────────────
   Two mountings for one piece of art, because the race view and everything
   else have opposite problems.

   On the race view it goes in a strip of sky opened up above the crowd, so it
   is behind the grandstand and ALWAYS on screen. The obvious mounting — a
   fixed layer at the foot of the page, the way Matrix hangs its rain — only
   works while the field is short enough to leave the bottom of the page empty;
   a full field covers it completely, which is exactly when a theme most needs
   to still look like itself. The strip costs the track about two lanes of
   height, and the track already scrolls when a field outgrows it.

   Everywhere else (home, the org pages) there is no crowd to sit behind, so
   the same art hangs along the bottom of the viewport instead. The two are
   mutually exclusive by construction: one requires a .crowd on the page, the
   other requires there not to be one.

   Both draw the landmarks ONCE, centred — the whole point of them is that they
   are Tower Bridge and Big Ben, and a tiled skyline would put two Big Bens on
   screen at laptop width — with the generic rooftops tiling out from behind
   them to both edges, so the city runs off the screen rather than ending in
   mid-air where the centrepiece stops. */

:root[data-theme="london"] .crowd {
  /* The sky strip. Sprites are pinned to the bottom of the taller box and given
     back their own height, since .crowd-person's `height: 100%` would otherwise
     stretch one 32px sprite over the whole strip. */
  height: calc(var(--sprite-scale) * 32px + var(--city-h));
  align-items: flex-end;
  /* z-index (not just position) so this is a stacking context of its own, which
     is what lets the bus below sit at z-index -1: inside a stacking context a
     negative-z child paints above the element's own background — the skyline —
     but below its in-flow children, the crowd sprites. Without it the bus would
     drop out to the page's stacking context and vanish behind the skyline. */
  position: relative;
  z-index: 0;
  /* Layers, front to back: the bridge you are standing on (its parapet, lamps
     and phone boxes are one tile — see gen-london-skyline.mjs for why), haze,
     the city downriver, generic rooftops.

     The haze sits BELOW the bridge and above the distant city, which is what
     separates the two: the bridge is where you are standing and stays crisp,
     the city is a mile downriver and goes soft.

     Everything shares one ground line, sat a little way INTO the crowd rather
     than resting on top of it, so the roadway disappears behind the grandstand
     instead of balancing on it. */
  --city-ground: calc(var(--sprite-scale) * 32px - 10px);
  /* Every layer is sized off this one number, so the narrow-screen rules below
     only have to change --city-h. */
  --city-h: 124px;
  /* The pavement line: the top of the parapet, which is also what the bus runs
     along. Anything standing on the bridge shares it. */
  --bridge-deck: calc(var(--city-ground) + var(--city-h) * 20 / 124);
  background-image:
    url('img/london-bridge.svg'),
    linear-gradient(180deg, rgba(150, 175, 205, 0) 45%, rgba(150, 175, 205, 0.12) 100%),
    url('img/london-skyline.svg'),
    url('img/london-rooftops.svg');
  background-repeat: repeat-x, no-repeat, no-repeat, repeat-x;
  background-position:
    center bottom var(--city-ground),
    center bottom var(--city-ground),
    center bottom var(--city-ground),
    center bottom var(--city-ground);
  background-size:
    auto var(--city-h),
    100% var(--city-h),
    auto var(--city-h),
    auto calc(var(--city-h) * 44 / 124);
}
/* A tall strip costs the track height, which a short screen has least of. */
@media (max-width: 760px) {
  :root[data-theme="london"] .crowd { --city-h: 88px; }
}
:root[data-theme="london"] .crowd-cap,
:root[data-theme="london"] .crowd-person {
  height: calc(var(--sprite-scale) * 32px);
}

/* Declared on body, a real element, rather than inside the body::after rule
   below where they started. A custom property set on a pseudo-element is
   visible to that pseudo-element and nothing else — and the bus is #app::after,
   a different element's pseudo. It could not see --bridge-deck, so its
   `bottom` was invalid at computed-value time and fell back to auto, which
   pinned the bus to the top of the viewport instead of the roadway. On body
   both pseudo-elements inherit them. */
:root[data-theme="london"]:not(:has(.crowd)) body {
  --city-h: 190px;
  --bridge-deck: calc(var(--ticker-h) + var(--city-h) * 20 / 124);
}
:root[data-theme="london"]:not(:has(.crowd)) #app { position: relative; z-index: 1; }
:root[data-theme="london"]:not(:has(.crowd)) body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* Same six layers as the crowd strip, one ticker-height up so the roadway
     runs along the top of the ticker rather than having its feet cut off. */
  background-image:
    url('img/london-bridge.svg'),
    linear-gradient(180deg, rgba(150, 175, 205, 0) 45%, rgba(150, 175, 205, 0.12) 100%),
    url('img/london-skyline.svg'),
    url('img/london-rooftops.svg');
  background-repeat: repeat-x, no-repeat, no-repeat, repeat-x;
  background-position:
    center bottom var(--ticker-h),
    center bottom var(--ticker-h),
    center bottom var(--ticker-h),
    center bottom var(--ticker-h);
  background-size:
    auto var(--city-h),
    100% var(--city-h),
    auto var(--city-h),
    auto calc(var(--city-h) * 44 / 124);
}

/* A Routemaster along the front, left to right — which is the direction the
   lane nearest the kerb runs in a country that drives on the left. Drawn well
   out of scale with the city behind it (a bus at Big Ben's scale would be six
   pixels long); the size difference is what makes it read as near traffic in
   front of a far skyline rather than a toy parked on the Embankment.

   In the crowd strip it is an absolutely positioned pseudo-element, which is
   out of flow and so never becomes a flex item of .crowd; .crowd's existing
   `overflow: hidden` is what clips it at both ends of its run. */
:root[data-theme="london"] .crowd::before,
:root[data-theme="london"]:not(:has(.crowd)) #app::after {
  content: '';
  /* One number per mounting; the height follows the sprite's own proportions.
     The two skylines are drawn at different heights, so the bus is sized
     against each rather than shared, or it would read as a different distance
     away on the home page than on the race view. */
  width: var(--bus-w);
  aspect-ratio: 58 / 28;
  pointer-events: none;
  background: url('img/london-bus.svg') no-repeat center / contain;
}
/* Behind the crowd, not over it — the road it is on runs along the back of the
   grandstand. Absolute positioning alone would paint it in front of them, since
   a positioned element outranks its non-positioned siblings whatever the source
   order; the negative z-index is what puts it back. Its wheels dip the same few
   pixels into the top of the crowd that the buildings' feet do, so bus and city
   stand on one ground line. */
:root[data-theme="london"] .crowd::before {
  --bus-w: 92px;
  position: absolute;
  left: 0;
  bottom: var(--bridge-deck);
  z-index: -1;
}
:root[data-theme="london"]:not(:has(.crowd)) #app::after {
  --bus-w: 140px;
  position: fixed;
  left: 0;
  bottom: var(--bridge-deck);
  z-index: -1;
}
@media (prefers-reduced-motion: no-preference) {
  :root[data-theme="london"] .crowd::before,
  :root[data-theme="london"]:not(:has(.crowd)) #app::after {
    animation: london-bus-pass 34s linear infinite;
  }
}
/* Starts and ends fully off-screen, so it arrives and leaves rather than
   popping. The long gap between passes is the point — a bus you see every half
   minute is scenery, a bus you see constantly is a distraction. */
@keyframes london-bus-pass {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100vw); }
}
/* Same rule the crowd and the chromatic wash follow — the page stops moving
   once the race is over. */
:root[data-theme="london"] .race.finished .crowd::before {
  animation-play-state: paused;
}

/* A neutral --lane + luminosity flattens the dirt to greyscale texture; the wash
   then paints hue/saturation back across the lane's width with `color` (which
   takes hue+sat from the gradient and luminosity from the backdrop), so the
   spectrum keeps every pixel of the sprite's shading and rails. */
:root[data-theme="chromatic"] {
  --bg: #06040f;
  --panel: #171029;
  --text: #f4ecff;
  --muted: #9d92c4;
  --accent: #ff7ae0;
  --border: #3a2a5e;
  --lane: #8a8a8a;
  --track-blend: luminosity;
  /* First and last stop are the same red, so one tile butts seamlessly against
     the next and the scroll below has no visible seam. Stops are positioned
     rather than evenly spaced so the magenta end of the spectrum stays a narrow
     band instead of taking a full even share alongside the red seam. */
  --track-wash: linear-gradient(
    90deg,
    #ff004d 0%, #ff7a00 15%, #ffe600 32%, #00d95f 50%,
    #00c2ff 68%, #6a5cff 85%, #ff3ea5 95%, #ff004d 100%
  );
  --wash-blend: color;
  --wash-tile: 480px;
  --rail: #ffffff;
  --label-box: #1a1230;
  --live: #7dffcf;
  --ticker-accent: #ffe600;
}

/* Size the wash to one spectrum tile (it repeats; the sprite layer keeps its own
   `auto 100%`), then drift it against the direction the horses run, so the colours
   rush backwards past them and read as speed. Shifting by exactly one tile width
   means the end state matches the start, so the loop is invisible. */
:root[data-theme="chromatic"] .lane-track {
  background-size: var(--wash-tile) 100%, auto 100%;
}
/* Stepped rather than linear, matching the crowd's steps(4) cheer — a smooth
   slide would read as the only non-pixel motion on the page. 24 steps over the
   480px tile lands each jump on a round 10 sprite-pixels (tile ÷ --sprite-scale
   ÷ 24), so the bands snap along the same grid the art is drawn on. */
@media (prefers-reduced-motion: no-preference) {
  :root[data-theme="chromatic"] .lane-track {
    animation: chromatic-scroll 3s steps(24) infinite;
  }
}
@keyframes chromatic-scroll {
  from { background-position: 0 0, center; }
  to   { background-position: calc(-1 * var(--wash-tile)) 0, center; }
}
/* Same rule the crowd follows — the spectacle stops once the race is over. */
.race.finished .lane-track { animation-play-state: paused; }

/* ── Matrix / Phosphor ──────────────────────────────────
   One phosphor-on-black CRT palette, worn two ways. Phosphor is the bare
   terminal: green on black, scanlines, glow, and no glyph rain anywhere. Matrix
   adds the rain, both as a page backdrop and running through the lanes.

   Both are deliberately pushed away from Turf, the other dark green theme: Turf
   is a desaturated grass palette on #06120a, these are a saturated #00ff41 on
   true black, so no two of the three read as variants in the picker.

   Phosphor being the still one does NOT make it a reduced-motion option — the
   crowd still cheers and the horses still run. OS-level prefers-reduced-motion
   is honoured separately, in the gates below. */
:root:is([data-theme="phosphor"], [data-theme="matrix"]) {
  --bg: #000000;
  --panel: #04140a;
  --text: #8affa8;
  /* Lighter than a dark theme's muted would normally go: these are the only
     themes with moving glyphs behind the text, and at the top of the rain's
     opacity swing a dimmer muted stopped being readable on the home page. */
  --muted: #4fbf6e;
  --accent: #00ff41;
  --border: #10401f;
  --lane: #0b6626;
  --track-blend: luminosity;
  /* Heavier than the other themes': the dirt sprite is pale tan, and luminosity
     keeps that luminance, so without a strong shade the lanes come out grass-
     bright — reading as Turf rather than as a phosphor track on black. */
  --track-shade: rgba(0, 10, 3, 0.62);
  --rail: #b9ffcb;
  --label-box: #04160b;
  --live: #00ff41;
  --ticker-accent: #00ff41;
}

/* Matrix's page backdrop — what fills the page around the track. A fixed
   layer at z-index 0, not a lane treatment: it shows through wherever the page
   is transparent (the header strip, the margins, everything below a short
   field) while every panel keeps its own opaque background. Raising #app above
   it is the only reason that rule exists, so both are scoped to Matrix —
   Phosphor has no backdrop to sit in front of.

   The fall is driven by `transform`, not `background-position`. That is a
   performance rule, not a style one: transforms are handed to the compositor,
   so a layer is rasterised once and then only moved, while an animated
   background-position repaints the element on the main thread every frame —
   here a full viewport of tiled SVG text, sixty times a second, which is what
   pinned people's CPUs.

   A transform moves the whole element, so the three parallax speeds need three
   boxes; that is why this is markup (see index.html) rather than the three
   background layers on one pseudo-element it used to be. */
:root[data-theme="matrix"] #app { position: relative; z-index: 1; }

.rain-backdrop { display: none; }
:root[data-theme="matrix"] .rain-backdrop {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  opacity: 0.2;
}
/* Each layer carries one tile-height of headroom above the viewport, so that
   after travelling exactly its own tile height it still covers the screen and
   the pattern lands back on itself — the loop is invisible without the layer
   ever being redrawn. */
:root[data-theme="matrix"] .rain-backdrop > i {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  background-image: url('img/matrix-rain.svg');
  background-repeat: repeat;
  /* The page-wide `pixelated` would crunch the rasterised glyphs — the tile is
     vector text, not sprite art, and the widest layer is scaled up 1.4x. */
  image-rendering: auto;
}
/* One tile at three scales and x-offsets. The tile repeats every 20 glyphs, so
   staggering the layers is what stops the eye locking onto that repeat. Each
   travels its own tile height over the same 6s, which is where the parallax
   comes from. */
:root[data-theme="matrix"] .rain-backdrop > i:nth-child(1) {
  top: -600px;
  background-size: 300px 600px;
  background-position: 0 0;
}
:root[data-theme="matrix"] .rain-backdrop > i:nth-child(2) {
  top: -420px;
  background-size: 210px 420px;
  background-position: 90px 0;
}
:root[data-theme="matrix"] .rain-backdrop > i:nth-child(3) {
  top: -840px;
  background-size: 420px 840px;
  background-position: 30px 0;
}
@keyframes matrix-rain-fall-1 { to { transform: translate3d(0, 600px, 0); } }
@keyframes matrix-rain-fall-2 { to { transform: translate3d(0, 420px, 0); } }
@keyframes matrix-rain-fall-3 { to { transform: translate3d(0, 840px, 0); } }
/* The "fading in and out" pass. Held well off any round multiple of the fall
   duration so the swell drifts against the rain instead of pulsing with it.
   It rides on the parent so it stays a plain opacity animation — also
   compositor-driven — rather than forcing the three layers to repaint. */
@keyframes matrix-rain-breathe {
  0%, 100% { opacity: 0.08; }
  50%      { opacity: 0.28; }
}
@media (prefers-reduced-motion: no-preference) {
  :root[data-theme="matrix"] .rain-backdrop {
    animation: matrix-rain-breathe 19s ease-in-out infinite;
  }
  :root[data-theme="matrix"] .rain-backdrop > i:nth-child(1) { animation: matrix-rain-fall-1 6s linear infinite; }
  :root[data-theme="matrix"] .rain-backdrop > i:nth-child(2) { animation: matrix-rain-fall-2 6s linear infinite; }
  :root[data-theme="matrix"] .rain-backdrop > i:nth-child(3) { animation: matrix-rain-fall-3 6s linear infinite; }
}
/* Same rule the crowd and the chromatic wash follow — the spectacle stops once
   the race is over. Reached with :has() because the backdrop sits outside the
   .race subtree those rules can match directly. */
:root[data-theme="matrix"]:has(.race.finished) :is(.rain-backdrop, .rain-backdrop > i) {
  animation-play-state: paused;
}

/* ── Matrix only: rain in the lanes ─────────────────────
   Its own element inside .lane-track, for the same compositor reason as the
   backdrop: as a fourth background layer the fall could only be animated
   through background-position, which repaints the lane every frame — and a
   lane's paint is three blended layers of dirt sprite and wash, so that was
   the more expensive of the two rains despite covering far less of the screen.
   As an element it repaints never, and the cost stops scaling with the size of
   the field.

   z-index -1 keeps the arrangement the background-layer version bought us: it
   paints above .lane-track's own background but below every child, so the
   horses and their shadows stay on top with nothing to juggle. That needs the
   track to be a stacking context, or the layer would drop behind the lane
   entirely — hence the z-index below.

   The layer is anchored to its own lane, so each lane runs its own rain rather
   than all of them sampling one page-wide sheet, and the rain travels with the
   lane — which is what makes it safe in TV mode, where .track is a scroll
   container.

   Anchoring per lane is why this uses its own tile: a lane is ~60px tall, so
   the 600px backdrop tile would park the same frozen slice in every lane. The
   lane tile is 180px — short enough to cycle through a lane, tall enough to
   keep glyphs legible — and 900px wide, because one layer has nothing to
   disguise its horizontal repeat the way the backdrop's three scales do. */
:root[data-theme="matrix"] .lane-track { z-index: 0; }

.lane-rain { display: none; }
:root[data-theme="matrix"] .lane-rain {
  display: block;
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  border-radius: 4px;
  pointer-events: none;
}
/* One tile height of headroom above the lane, then travel exactly that — same
   seamless-loop trick as the backdrop. 180px over 1.8s is 100px/s, the same
   fall speed as the backdrop's 600px over 6s, so the two rains agree where they
   meet at the edge of the track. */
:root[data-theme="matrix"] .lane-rain::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: -180px;
  bottom: 0;
  background: url('img/matrix-rain-lane.svg') repeat var(--rain-x, 0px) 0 / 900px 180px;
  image-rendering: auto;
}
@keyframes matrix-lane-fall { to { transform: translate3d(0, 180px, 0); } }
@media (prefers-reduced-motion: no-preference) {
  :root[data-theme="matrix"] .lane-rain::before {
    animation: matrix-lane-fall 1.8s linear infinite;
  }
}
/* Without this every lane would run the identical tile in lockstep, which reads
   as one sheet of wallpaper scrolling rather than as rain. Each lane gets a
   different slice of the tile and a different point in the cycle; the delays are
   negative so they start already offset instead of pausing first. Five variants,
   which shares no common factor with the field sizes that show up in practice,
   so the pattern doesn't line up into visible bands. */
:root[data-theme="matrix"] .lane:nth-child(5n+1) .lane-rain::before { --rain-x:    0px; animation-delay:  0s; }
:root[data-theme="matrix"] .lane:nth-child(5n+2) .lane-rain::before { --rain-x: -137px; animation-delay: -0.7s; }
:root[data-theme="matrix"] .lane:nth-child(5n+3) .lane-rain::before { --rain-x: -311px; animation-delay: -1.3s; }
:root[data-theme="matrix"] .lane:nth-child(5n+4) .lane-rain::before { --rain-x: -466px; animation-delay: -0.4s; }
:root[data-theme="matrix"] .lane:nth-child(5n+5) .lane-rain::before { --rain-x: -703px; animation-delay: -1.1s; }
/* The generic `.race.finished .lane-track` pause further up cannot reach this —
   different element — so the pause is repeated here. */
:root[data-theme="matrix"] .race.finished .lane-rain::before { animation-play-state: paused; }

/* Scanlines sit above the page content — a CRT's raster falls on the whole
   screen, not behind what it's drawing. z-index 2 is chosen to clear the
   z-index 1 that Matrix puts on #app; in Phosphor anything positive would do. */
:root:is([data-theme="phosphor"], [data-theme="matrix"]) body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: repeating-linear-gradient(
    180deg,
    rgba(0, 0, 0, 0.22) 0 1px,
    transparent 1px 3px
  );
}

/* The crowd sprites carry their own baked-in shirt colours, which no palette
   token can reach — left alone they're the one full-colour band on an otherwise
   monochrome page. Greyscale first so the filter works off luminance only, then
   sepia+hue-rotate tints the result green. The horses are deliberately NOT
   filtered: their colours are how you tell the racers apart. */
:root:is([data-theme="phosphor"], [data-theme="matrix"]) .crowd {
  filter: grayscale(1) sepia(1) hue-rotate(75deg) saturate(2.4) brightness(0.85);
}

:root:is([data-theme="phosphor"], [data-theme="matrix"]) :is(.home h1, .race-header h1, .race-status--live) {
  text-shadow: 0 0 8px rgba(0, 255, 65, 0.55);
}

/* ── Theme picker ───────────────────────────────────── */

.theme-picker {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--muted);
  cursor: pointer;
}
.theme-picker-caption {
  text-transform: uppercase;
  letter-spacing: 0.15em;
  font-size: 0.75rem;
}
.theme-select {
  background: var(--panel);
  color: var(--text);
  border: 2px solid var(--border);
  border-radius: 3px;
  padding: 4px 8px;
  font: inherit;
  font-size: 0.85rem;
  letter-spacing: 0.05em;
  cursor: pointer;
}
.theme-select:hover { border-color: var(--accent); }
.theme-select:focus-visible { outline: none; border-color: var(--accent); }

/* In the race header the picker sits among the meta items, so it matches the
   button scale there rather than the roomier home-page sizing. */
.race-header .meta .theme-picker { gap: 6px; }
.race-header .meta .theme-select { padding: 2px 6px; font-size: 0.8rem; }
body.tv .race-header .meta .theme-select { padding: 3px 10px; font-size: 0.9rem; }

/* ── One row, always ─────────────────────────────────────────────────────
   The title and the controls share a single line. Measured at 1280px with a
   real 42-character race name: the controls alone were 1044px and the title
   544px, so with the "Status:/Time left:/Join code:" labels shown the pair
   needed 1700px — impossible on any laptop. The labels are therefore off by
   default rather than at a breakpoint; the values read fine unintroduced.
   Everything below sheds further as width drops, so the row never wraps. */
.race-header .meta .meta-label { display: none; }
.race-header .meta .theme-picker-caption { display: none; }
.race-header .meta { gap: 12px; font-size: 0.9rem; }
.race-header .meta .btn { padding: 3px 8px; font-size: 0.75rem; }

@media (max-width: 1200px) {
  .race-header { padding: 6px 14px; }
  .race-header .meta { gap: 10px; }
  .race-header h1 { font-size: 1.05rem; }
}

@media (max-width: 900px) {
  /* Let the two variable-width controls give ground before anything is cut:
     the theme select (its longest option is "Chromatic Circuit") and the org
     button (org names run to 12 characters). */
  .race-header .meta .theme-select { max-width: 7rem; }
  .race-header .meta .org-btn { max-width: 7.5rem; }
}

@media (max-width: 820px) {
  /* The join code is already in the address bar — /race/WUEF98 — so it is the
     one readout that costs nothing to drop. */
  .race-header .meta .join-code { display: none; }
  .race-header .meta { gap: 8px; }
  .race-header { padding: 6px 10px; }
}

/* Past here the row can only stay on one line by giving things up. Dropped in
   order of what a racer can most afford to lose, and each is reachable
   elsewhere: the theme picker also lives on the home page, and a running
   countdown already tells you the race is live. */
@media (max-width: 660px) {
  .race-header .meta .theme-picker { display: none; }
  .race-header .meta { gap: 8px; font-size: 0.85rem; }
}

@media (max-width: 520px) {
  .race-header .meta .status-item { display: none; }
  .race-header .meta .org-btn { max-width: 6rem; overflow: hidden; text-overflow: ellipsis; }
}

@media (max-width: 440px) {
  /* Last thing to go. Home still gets you out, and the org page is one click
     from there — whereas nothing else here has a second route to it. */
  .race-header .meta .org-btn { display: none; }
  .race-header .meta .btn { padding: 3px 6px; }
}
