/*
 * Design tokens — the single source of truth for the app's palette, scales and
 * motion. Nothing else in the project declares a colour value.
 *
 * Two layers, and the distinction matters:
 *
 *   --color-*   the raw palette, straight from Figma. Never referenced from a
 *               component; it has no opinion about what a colour is *for*.
 *   semantic    what a colour means here — surface, text, border, accent.
 *               Components use only these, so a palette change lands in one
 *               place instead of across thirty rules.
 *
 * Adding a component does not mean adding a colour. If a new need arises, map
 * it to the closest existing semantic token; if none fits, the palette below is
 * what gets extended, not the component.
 */

:root {
  /* Native controls, scrollbars and the canvas behind the page follow the
     theme; the dark block below says `dark`. */
  color-scheme: light;

  /* --- Palette: neutrals -------------------------------------------------- */

  /*
   * The neutral scale is cool — every step leans a few points blue — so the
   * greys read as one family with the primary rather than as unrelated
   * silvers.
   *
   * The values are the Slip Builder's, adopted app-wide on 8 Sep 2026: that
   * screen's spec sampled a full palette from its reference and it became the
   * house one rather than living beside it. The page is a warm off-white and
   * cards are white *on* it — the tonal layering the slip design is built on.
   * That is the opposite arrangement to the one this file once warned about
   * (page #f8f9fa under darker #f0f1f3 cards, which read as haze): lighter
   * cards on a slightly darker page lift, darker cards on a lighter page sink.
   * The hairline and the shallow --shadow-card still draw a card's edge, so a
   * card reads as an object on either ground. Below white the ladder is
   * sunken (chip fills, fields, accordion rows) → pressed (touch-down, one
   * step deeper), with the mid grey for disabled type, empty rings and the
   * slider rail's end ticks.
   */
  --color-white: #ffffff;
  --color-bg-page: #f7f8fa;
  --color-bg-card: #ffffff;
  --color-bg-card-rgb: 255 255 255;
  --color-border-subtle: #e7e9ef;
  --color-gray: #f1f2f6;
  --color-gray-strong: #e4e6ec;
  --color-gray-mid: #b6bbc6;
  --color-text-muted: #8a90a0;
  --color-text-secondary: #5e6675;
  --color-text-primary: #15171c;
  --color-text-primary-rgb: 21 23 28;

  /* --- Palette: brand ----------------------------------------------------- */

  /*
   * The app's primary. It owns every "this one is active" signal in the UI —
   * selection, CTAs, links, focus, the nav indicator, story rings, progress
   * bars, live badges — and no other blue is allowed into those roles.
   *
   * The Slip Builder's blue, adopted with the rest of its palette (see the
   * neutrals above); Figma's #0D4FF7 is what it replaced. 4.6:1 against
   * white, so it still carries small type. --color-primary-light is its
   * touch-down step — the value Figma's "Blue 4BB" slot was waiting for — and
   * --color-info-bg the soft tint behind a selected day pill or a tinted row.
   */
  --color-primary: #2a5cf6;
  --color-primary-light: #1f47d6;
  --color-info-bg: #edf2ff;

  /*
   * The standout signal. One sparing highlight at a time, and it owns exactly
   * one fact in this app: LIVE.
   *
   * Off the Figma export, which had this as lime (#CEFF00); it was azure for a
   * spell and is now red. The value is chosen for the job it actually does now,
   * which changed with it: this is no longer a fill with dark type on it, it is
   * the type — a running minute, set at --text-2xs and scaled down again by
   * --fixture-scale, so roughly 9px. That is small text, and small text needs
   * 4.5:1. Every brighter red fails it (#FF3B30 lands at 3.4, pure #FF0000 at
   * 3.8); this is the most saturated red that clears the bar, at 5.0:1 on the
   * white card a fixture row now sits in. Go lighter and the contrast goes
   * with it.
   *
   * If Figma's "lose" red is ever confirmed into --color-error below, it has to
   * stay clearly apart from this one, or a live match and a lost bet turn into
   * the same colour.
   */
  --color-accent: #e00000;

  /*
   * Gold — the one tier-status signal. It has no caller at the moment: the
   * avatar ring and status pill it was drawn for left the header in the navbar
   * rework, and it is kept for the screen that picks tier status back up.
   * Inactive status has no colour of its own; it falls back to the neutral
   * pill already used everywhere else (--surface-sunken / --text-muted below).
   */
  --color-gold: #c9971c;
  --color-gold-bg: #faf1d9;

  /*
   * Match results — win / loss, for the club page's Team Form row. No Figma
   * layer exists for this: the export's own "Green pasture" / "lose" pair
   * below is for predictions and payments, and the whole reason this pair
   * exists separately is to keep those two apart from an ordinary match
   * result (see --highlight's own comment on the same risk). Chosen rather
   * than sourced, so revisit if a real design ever ships for this row —
   * distinct enough from --highlight (#E00000) not to read as LIVE, and worth
   * a second look against --color-success / --color-error once either of
   * those is confirmed, so a result and a bet outcome stay visually apart.
   */
  --color-form-win: var(--color-success-strong);
  --color-form-win-rgb: 31 146 84;
  --color-form-loss: var(--color-error-strong);
  --color-form-loss-rgb: 196 41 28;

  /*
   * Player-rating tiers, for the Season Stats pitch card's rating pill — three
   * bands (high / mid / low) rather than --form-win / --form-loss's two, and
   * kept apart from that pair on purpose: a rating is a performance score, not
   * a match result, and the two ideas landing on the same green/red would
   * blur exactly the distinction --highlight's own comment warns about. No
   * Figma layer exists for this either; chosen, not sourced.
   */
  --color-rating-high: var(--color-success-strong);
  --color-rating-high-rgb: 31 146 84;
  --color-rating-mid: var(--color-warning-strong);
  --color-rating-mid-rgb: 185 137 26;
  --color-rating-low: var(--color-error-strong);
  --color-rating-low-rgb: 196 41 28;

  /*
   * Heatmap temperature, for the cloud a player's touches make (HeatCloud.js)
   * on the player page and the match-player sheet. Two stops, cool to hot,
   * and neither is a rating band: the cloud used to borrow --color-rating-*
   * and read as a verdict — red where the player "was worst" — when all it
   * says is where they were. Green for presence and a warm yellow for the
   * densest patch is the reading every heatmap in the sport already has, so
   * these are chosen to that convention and not sourced from a Figma layer.
   * The green is brighter than --color-success on purpose: it is laid down
   * translucent over a pale pitch, where --color-success-strong went muddy.
   */
  --color-heat-cool: #62dc92;
  --color-heat-cool-rgb: 98 220 146;
  --color-heat-hot: #f3de3a;
  --color-heat-hot-rgb: 243 222 58;

  /*
   * League-position zones, for the club page's Table tab — the coloured bar
   * beside each standings row. A third tri-band pair alongside --rating-* and
   * --form-*, kept apart from both for the same reason those two are kept
   * apart from each other: a promotion race and a player's rating are not the
   * same fact, even when both reach for "green means good." No Figma layer
   * exists for this either, and no real competition feed exists yet to read
   * exact rank cutoffs from — see the stub note on previewStandings
   * (state/selectors.js).
   */
  --color-zone-promotion: var(--color-success-strong);
  --color-zone-playoff: var(--color-warning-strong);
  --color-zone-relegation: var(--color-error-strong);

  /*
   * Injury status, for the Squad tab's player rows. Single-band, not a tier —
   * a player is either fit or not, so this has no "mid" the way --rating-*
   * does. Shares its hex with --color-rating-low / --color-form-loss rather
   * than inventing a fourth red: the three tri-bands above are already kept
   * apart from each other on the "different fact" rule despite two of them
   * landing on this same value, and an injury marker is a fourth fact by that
   * same rule, not a reuse of any of theirs.
   */
  --color-injury: var(--color-error-strong);

  /*
   * "Doubtful", for a player the Lineups panel lists as missing but not
   * ruled out. The amber counterpart to --color-injury above, and a separate
   * entry for the same reason that one is: it shares its hex with
   * --color-rating-mid and means something else entirely. Fit, doubtful and
   * out is the one place in the app where those three greens, ambers and reds
   * genuinely form a scale rather than three unrelated bands.
   */
  --color-doubtful: var(--color-warning-strong);

  /*
   * Substitutions — the arrow on a player who came off, and on the one who
   * replaced them. Its own pair for the same reason every band above is: a
   * substitution is not a verdict on anybody. The green and red are borrowed
   * hexes, as --color-injury borrows its red, because the *direction* is what
   * these encode and football already reads a red arrow as leaving and a
   * green one as arriving.
   */
  /*
   * Solid fills for the rating chip in the squad lists, where the chip is a
   * block of colour with white type on it rather than the pitch pill's tinted
   * wash with coloured type. Each is its --color-rating-* sibling taken down
   * far enough to clear 4.5:1 against white — the bright amber in particular
   * cannot carry white text at its own value, and the choice was a deeper fill
   * rather than dark text on one chip in a column of light-text ones.
   *
   * The middle band lands on a burnt orange rather than a darkened version of
   * its own amber. Taken straight down, --color-rating-mid goes olive before
   * it is dark enough for white type, which reads as a fourth colour that
   * means nothing; pushed towards red instead it stays on the green-to-red
   * run the three bands describe, and it is where a mid rating belongs on it.
   */
  --color-rating-high-solid: #1a7a46;
  --color-rating-mid-solid: #a85a15;
  --color-rating-low-solid: #b3251a;
  /* No mark yet — a starter in a confirmed eleven before the provider has
     scored him. The neutral grey the app already uses for a quiet figure,
     so the pill reads as "pending", not as the bottom of the run above. */
  --color-rating-none-solid: var(--color-text-muted);

  --color-sub-on: var(--color-success-strong);
  --color-sub-off: var(--color-error-strong);

  /*
   * Bookings, for the card drawn on a player node in a lineup. A fifth entry
   * in the run of "different fact" colours above, and the least negotiable of
   * them: a yellow card and a red card are physical objects with agreed
   * colours, so unlike --rating-* or --zone-* these are not a choice about
   * what a colour should mean. That also rules out folding the red into
   * --color-rating-low or --color-injury — those are this app's reds, and a
   * red card is football's.
   */
  --color-card-yellow: #f2c230;
  --color-card-red: #d92c20;
  --color-injury-rgb: 196 41 28;

  /*
   * Booking cards, for the fixture page's Commentary tab — the little
   * rectangle in the rail beside a yellow or a red. Not a signal band like the
   * tri-bands above: it is a *picture of an object*, and the object is
   * yellow-and-red because the Laws of the Game say so. That is why none of
   * the colours already here could take it. --gold is tier status and reads
   * brown at 9px; --highlight owns LIVE and nothing else, and a red rectangle
   * next to a running red minute would put two unrelated claims in one colour;
   * --color-form-loss / --color-injury are outcomes, and a booking is an
   * event, not a result. Chosen rather than sourced — no Figma layer exists —
   * and chosen as a matched pair, since the two are only ever read against
   * each other.
   *
   * Both are drawn as fills with no type on them, so the 4.5:1 floor that
   * pins --color-accent does not apply; what they have to clear is being
   * legible as a shape on --surface, which is why the yellow is taken well
   * below a pure #ffcc00.
   */
  --color-card-yellow: #e5a80c;
  --color-card-red: #d02516;

  /*
   * Model confidence, for the fixture page's Predictions tab — the pill on
   * each pick card. A fourth tri-band alongside --rating-*, --zone-* and
   * --form-*, kept apart from all three on the same "different fact" rule: how
   * sure a model is of a call is not a player's rating, a promotion race or a
   * result, even though all four reach for the same green.
   *
   * This is the one band the palette above has a named slot for — Figma's
   * "Green pasture" / "lose" pair is described there as belonging to
   * predictions and payments — but neither hex was readable from the export
   * (see Unresolved below), so these stand in until they are. If those land,
   * this band is the first thing to revisit: high/low should become them, and
   * this comment should go with the change.
   */
  --color-confidence-high: var(--color-success-strong);
  --color-confidence-high-rgb: 31 146 84;
  --color-confidence-medium: var(--color-warning-strong);
  --color-confidence-medium-rgb: 185 137 26;
  --color-confidence-low: #8b93a2;
  --color-confidence-low-rgb: 139 147 162;

  /*
   * Home / away, for the club page's Matches tab — its Calendar view's two
   * cell treatments (see the legend in match-calendar.css). A fixture is
   * played at home or away, which is neither a result nor a zone nor a
   * rating, so this is its own pair apart from --form-*, --rating-* and
   * --zone-* for the same "different fact" reason each of those is kept apart
   * from the others. No Figma layer specifies these two; chosen for a hue
   * none of red, green, gold or blue already owns elsewhere in the palette.
   */
  --color-side-home: #6d28d9;
  --color-side-home-rgb: 109 40 217;
  --color-side-away: #0e7490;
  --color-side-away-rgb: 14 116 148;

  /*
   * --- Status: success / warning / error ----------------------------------
   *
   * The slots this file long listed as unresolved from Figma ("Green
   * pasture", "lose", "YELLOW" and their light backgrounds). They landed with
   * the Slip Builder's palette rather than from Figma, so if the Figma values
   * are ever confirmed this trio is the first place to compare.
   *
   * Two weights each, and the split is load-bearing. The base colours are
   * fills: badge grounds, slider rings, a tinted cell — at ~2.4:1 against
   * white none of them can carry small type on a light surface. The `-strong`
   * steps are the same hues taken down to ~4.5:1 for exactly that: type on a
   * card, a bar that has to read at 9px. Every tri-band below (form, rating,
   * zone, confidence, injury, substitutions) is one of the strong steps, which
   * is what makes them one family with the badges rather than a second green
   * and a second red. The `-bg` tints are the flat light grounds a badge sits
   * on; the tri-bands keep their own alpha tints (`-soft`) because those
   * composite over the pitch and the dark card as well as over white.
   */
  --color-success: #2fbf71;
  --color-success-strong: #1f9254;
  --color-success-bg: #e1f5ea;
  --color-warning: #f2a93b;
  --color-warning-strong: #b9891a;
  --color-warning-bg: #fbeed3;
  --color-error: #f04141;
  --color-error-strong: #c4291c;
  --color-error-bg: #fde9e9;

  /*
   * --- Unresolved -------------------------------------------------------
   *
   *   --color-black-alt:                Figma "Black 2"       (in use)
   *
   * Still not readable from the export. --surface-inverse stands in with
   * --color-text-primary below. "Gray 2" (--color-bg-alt) is no longer
   * waiting: --color-gray-mid above is the value that slot was for.
   */

  --color-white-rgb: 255 255 255;

  /*
   * The neutrals that do not turn over with the theme (see the dark block at
   * the foot of this file). --color-media is the ground a photograph, a story
   * or a drawn night-pitch is shown on: a viewer that went pale in the light
   * theme would wash every picture out, so it is dark in both. --color-scrim
   * is what a sheet dims the page with, and a dimming layer has to darken
   * whatever is under it — a scrim in the text colour turns to a white fog the
   * moment the text goes light. In the light theme both equal
   * --color-text-primary, the value they borrowed before the dark theme needed
   * them apart; the dark theme takes the scrim to black.
   */
  --color-media: #15171c;
  --color-media-rgb: 21 23 28;
  --color-scrim: #15171c;
  /* Shadow ink: cool near-black under the light page, plain black under the dark. */
  --color-shadow-rgb: 22 24 29;

  /* --- Semantic: surfaces ------------------------------------------------- */

  --bg: var(--color-bg-page);
  /* The ground beyond the phone-width column, seen only in a desktop tab. A
     step darker than the page in either theme, so the column sits forward. */
  --canvas: var(--color-gray-strong);
  /*
   * An off-white page with white cards on it: the card is lighter than its
   * ground, and that lift — with the hairline and the shallow --shadow-card
   * drawing its edge — is what says "this is an object". The sunken fill one
   * step below the card carries the chips, fields and accordion rows that sit
   * *on* a card; on the bare page it is almost the page's own colour, so a
   * control that sits directly on the page needs a card or a border, not the
   * sunken fill alone. Every fullscreen page (fixture, club, league, player,
   * search) paints --bg, so the same layering holds wherever a card appears.
   */
  --surface: var(--color-bg-card);
  /* Channel form, for the frosted chrome (bottom nav, date strip, slip tray)
     that lays the card colour over the page at an alpha. */
  --surface-rgb: var(--color-bg-card-rgb);
  --surface-sunken: var(--color-gray);
  /* Touch-down fill for rows and chips — one step deeper than sunken. */
  --surface-pressed: var(--color-gray-strong);
  /*
   * A control's moving part laid on a sunken track — the segmented control's
   * sliding pill, a tab pill. The card colour in the light theme, where white
   * already sits above the grey track; the dark theme restates it a step
   * above the track, since there the card colour is *below* it and the pill
   * would read as a hole.
   */
  --surface-raised: var(--color-bg-card);
  /* A switch's knob, a slider's thumb: white in both themes, like the physical
     control — it has to read on the grey off-track and on the accent alike. */
  --knob: var(--color-white);
  --border: var(--color-border-subtle);
  /*
   * The mid grey: the read story ring, the Add Favourite circle's dashed edge,
   * an empty selection ring, a slider's end ticks — marks that have to hold
   * against a light surface without reading as type. This was --text-muted's
   * job while the palette waited on "Gray 2"; --color-gray-mid is that value.
   */
  --border-strong: var(--color-gray-mid);

  /* --- Semantic: text ----------------------------------------------------- */

  --text: var(--color-text-primary);
  --text-secondary: var(--color-text-secondary);
  --text-muted: var(--color-text-muted);
  /*
   * White type on a coloured fill — the accent, a status colour, a rating
   * chip, a club's colour, a photograph. It is white in both themes, because
   * the fills under it are the same colours in both. It is *not* the type on
   * --surface-inverse (that is --text-on-inverse, below): an inverse chip
   * turns light in the dark theme, and white type on it would vanish.
   */
  --text-inverse: var(--color-white);
  /* Channel forms, for alpha steps: the story viewer's over its dark card, a
     spinner's ring on a filled CTA, a tint over a selected chip. Components
     never reach past these to the palette. */
  --text-inverse-rgb: var(--color-white-rgb);
  --text-rgb: var(--color-text-primary-rgb);
  /* Type that cannot be acted on — a stepper at its bound, a label on a
     disabled CTA. One step lighter than muted so the two read apart. */
  --text-disabled: var(--color-gray-mid);

  /* --- Semantic: interactive ---------------------------------------------- */

  /*
   * Everything that means "active" resolves here, which is what keeps the rule
   * enforceable: a component asking for --accent cannot pick a different blue.
   */
  --accent: var(--color-primary);
  --accent-text: var(--color-white);
  --accent-rgb: 42 92 246;
  /* Soft tint of the primary for indicator fills and focus glows. Alpha over
     the channel form rather than a second blue, so it can never drift. */
  --accent-soft: rgb(var(--accent-rgb) / 0.1);
  /* The flat tint, where a fill has to be opaque — a selected day pill, a
     tinted row — and the touch-down step of any accent-filled surface. */
  --accent-tint: var(--color-info-bg);
  --accent-pressed: var(--color-primary-light);
  --ring: var(--color-primary);

  /*
   * The standout, semantically. It owns exactly one signal: LIVE, and since the
   * nav dot's removal it lives entirely inside a fixture row — the running
   * minute at one end, the scoreline at the other. Those are two marks but one
   * claim, which is the test anything else has to pass before it borrows this
   * red: not "is it important", but "is it the same fact as the minute".
   *
   * There is deliberately no --highlight-text companion. It existed for the era
   * when this was a light fill with dark type on it; as a red it is a
   * foreground colour, and a token inviting anyone to set type *on* it would be
   * inviting a contrast failure.
   */
  --highlight: var(--color-accent);

  /*
   * Status, semantically — see the palette comment on the trio. The base is
   * a fill or a ring, the strong step is type, the soft is the flat ground a
   * badge sits on. The Slip Builder is the first caller: its risk badge, its
   * confidence pills and its odds slider's two rings.
   */
  --success: var(--color-success);
  --success-strong: var(--color-success-strong);
  --success-soft: var(--color-success-bg);
  /* Channel form, for a tint at an alpha --success-soft does not offer —
     the match-player sheet's Highlights card, which sits a shade lighter. */
  --success-rgb: 47 191 113;
  --warning: var(--color-warning);
  --warning-strong: var(--color-warning-strong);
  --warning-soft: var(--color-warning-bg);
  --error: var(--color-error);
  --error-strong: var(--color-error-strong);
  --error-soft: var(--color-error-bg);

  /* Gold tier status, semantically — see the palette comment above. */
  --gold: var(--color-gold);
  --gold-bg: var(--color-gold-bg);

  /*
   * Win / loss, semantically — the Team Form row's own pair, apart from
   * --color-success / --color-error on purpose (see the palette comment).
   * Soft tints follow --accent-soft's own recipe: alpha over the channel form
   * rather than a second flat colour, so a light and a dark mode can share one
   * definition. Draw gets no entry here at all — it is the neutral scale's
   * job, and a form pill that draws borrows --surface-sunken / --text-secondary
   * rather than gaining a third invented hue for "neutral".
   */
  --form-win: var(--color-form-win);
  --form-win-soft: rgb(var(--color-form-win-rgb) / 0.12);
  --form-loss: var(--color-form-loss);
  --form-loss-soft: rgb(var(--color-form-loss-rgb) / 0.12);

  /* Rating tiers, semantically — the Season Stats pitch card's own three-band
     pair, apart from --form-win / --form-loss (see the palette comment). */
  --rating-high: var(--color-rating-high);
  --rating-high-soft: rgb(var(--color-rating-high-rgb) / 0.12);
  --rating-mid: var(--color-rating-mid);
  --rating-mid-soft: rgb(var(--color-rating-mid-rgb) / 0.12);
  --rating-low: var(--color-rating-low);
  --rating-low-soft: rgb(var(--color-rating-low-rgb) / 0.12);

  /*
   * The pitch, semantically — a drawn surface, not a material. It used to be
   * turf: its own pair of greens in the palette above, with white markings
   * painted on them by convention. The green is gone, so the pair no longer
   * needs a palette entry of its own and aliases the neutral ladder instead
   * — which is the point. Every pitch in the app (the club page's half, the
   * fixture page's full one, the shot map) now stands on the same greys the
   * cards around it are built from, and a player node reads identically on
   * all three without a per-pitch override.
   *
   * Still two steps rather than one, for the reason the green pair had two:
   * a flat fill reads as a rectangle, and the lighter-centre-to-deeper-edge
   * gradient both pitch graphics paint needs somewhere to fall away to.
   * --pitch is the ground, --pitch-deep the step below it — used for that
   * falloff in match-lineup.css and season-stats.css alike, and as the flat
   * ground in shot-map.css, where the sky above it is --surface-sunken and
   * the two would otherwise be the same grey.
   *
   * --pitch-line is --border-strong and not white: markings are white on
   * grass, but this is paint on a pale panel, where the club half-pitch's
   * hairline grey is the value that reads. There is no --pitch-text any more
   * — on a pale ground a player's name is just --text, which is what
   * season-stats.css already gives it.
   */
  --pitch: var(--surface-sunken);
  --pitch-deep: var(--surface-pressed);
  --pitch-line: var(--border-strong);

  /* Heatmap temperature, semantically — see the palette comment. The cloud's
     ground is --surface with --border markings, not --pitch: a heatmap is
     read for its colour, and a grey ground under a translucent green dulls
     it where white lets it show. */
  --heat-cool: var(--color-heat-cool);
  --heat-hot: var(--color-heat-hot);

  /* Bookings, semantically — see the palette comment. */
  --card-yellow: var(--color-card-yellow);
  --card-red: var(--color-card-red);

  /* Availability, semantically. --injury already carries "out" (it is the
     Squad tab's own token); this is the step short of it. */
  --doubtful: var(--color-doubtful);

  /* Solid rating chips, semantically — see the palette comment. */
  --rating-high-solid: var(--color-rating-high-solid);
  --rating-mid-solid: var(--color-rating-mid-solid);
  --rating-low-solid: var(--color-rating-low-solid);
  --rating-none-solid: var(--color-rating-none-solid);

  /* Substitution direction, semantically — see the palette comment. */
  --sub-on: var(--color-sub-on);
  --sub-off: var(--color-sub-off);

  /* Zone tiers, semantically — the Table tab's own three-band pair, apart
     from --rating-* / --form-* (see the palette comment). */
  --zone-promotion: var(--color-zone-promotion);
  --zone-playoff: var(--color-zone-playoff);
  --zone-relegation: var(--color-zone-relegation);

  /* Injury, semantically — the Squad tab's own single-band signal, apart
     from --rating-* / --form-* / --zone-* (see the palette comment). */
  --injury: var(--color-injury);
  --injury-soft: rgb(var(--color-injury-rgb) / 0.12);

  /* Booking cards, semantically — the Commentary tab's yellow and red. A
     drawn object rather than a signal band, and apart from every red above it
     for the reason the palette comment gives. */
  --card-yellow: var(--color-card-yellow);
  --card-red: var(--color-card-red);

  /*
   * Confidence tiers, semantically — the Predictions tab's own three-band
   * pair, apart from --rating-* / --zone-* / --form-* (see the palette
   * comment).
   *
   * The low band is grey, not the red every other tri-band here ends on, and
   * that is the whole point of it having its own entry: low confidence is the
   * model being unsure, not the user having lost anything. A red pill on a
   * pick card would say "this one went against you" — a claim nothing on this
   * page is in a position to make before the match has been played.
   */
  --confidence-high: var(--color-confidence-high);
  --confidence-high-soft: rgb(var(--color-confidence-high-rgb) / 0.12);
  --confidence-medium: var(--color-confidence-medium);
  --confidence-medium-soft: rgb(var(--color-confidence-medium-rgb) / 0.12);
  --confidence-low: var(--color-confidence-low);
  --confidence-low-soft: rgb(var(--color-confidence-low-rgb) / 0.12);

  /*
   * Tendency, semantically — where a number sits against the range its peers
   * occupy. The Preview tab's referee card is the caller: cards and fouls per
   * match, read as "Above average" / "Average" / "Below average".
   *
   * A fifth band alongside --rating-*, --zone-*, --form-* and --confidence-*,
   * kept apart from all four on the same "different fact" rule those are kept
   * apart from each other by: how strict an official is compared to his peers
   * is not a performance score, a promotion race, a result or a model's own
   * certainty. It reuses their hexes rather than adding a sixth green and a
   * fourth gold to the palette — what is new here is the meaning, not the
   * colour.
   *
   * The direction is deliberately not "good and bad": more cards than usual is
   * something to note, not something to disapprove of, which is why the middle
   * band is the neutral scale rather than a third hue.
   */
  --tendency-high: var(--color-rating-mid);
  --tendency-typical: var(--color-text-muted);
  --tendency-low: var(--color-rating-high);

  /*
   * Home / away, semantically — the Matches tab's Calendar view, apart from
   * every other pair above (see the palette comment).
   *
   * A second caller joined in the Statistics tab, and it is worth recording
   * why it was allowed to rather than given a pair of its own. Every other
   * "different fact" note above keeps two *meanings* apart. This is not that
   * case: the Calendar's cells mean "this fixture was played at home", the
   * statline's bars mean "this is the home side's figure", and those are the
   * same fact — which side of a fixture something belongs to — asked in two
   * places. Splitting them would have put two colours on one meaning, which is
   * the mirror image of the mistake the rule exists to stop, and would have
   * left the app with a fifth and sixth hue for nothing.
   *
   * The statline reaches these only as a *fallback*. Its bars are normally the
   * clubs' own crest colours, sampled at runtime (core/crest-colour.js), and
   * these stand in for a crest that will not sample — a dead URL, a badge with
   * no colour in it, a canvas the browser refuses to read back. That is the
   * common case rather than the rare one in this export, so the pair has to
   * hold up as a real answer and not only as an error state.
   *
   * The channel forms below are what made that possible without a new colour.
   * The statline mixes its own tint at a different alpha than `-soft`'s 12% —
   * a bar carrying meaning needs more presence than a background wash — and
   * `tokens.css`'s own recipe for that is alpha over a channel form. Exposing
   * the channels semantically, the way `--accent-rgb` and `--text-inverse-rgb`
   * already are, is what keeps a component from reaching past this layer to
   * `--color-side-home-rgb` to get them.
   */
  --side-home: var(--color-side-home);
  --side-home-rgb: var(--color-side-home-rgb);
  --side-home-soft: rgb(var(--side-home-rgb) / 0.12);
  --side-away: var(--color-side-away);
  --side-away-rgb: var(--color-side-away-rgb);
  --side-away-soft: rgb(var(--side-away-rgb) / 0.12);

  /*
   * The two clubs of the fixture on screen — the only colours in the app that
   * are not decided here.
   *
   * `core/team-colours.js` samples each crest at runtime and overrides the two
   * `-rgb` triples below on whichever element it is given, so a bar or a shot
   * marker wears the club's own colour rather than a house one. The pair above
   * is the fallback, and it is the common case rather than an error state:
   * plenty of fixtures in this export have no crest to sample, and a crest that
   * is black and white has no colour to find in it.
   *
   * They live here rather than in a component because two panels now draw a
   * fixture this way — the Statistics tab's bars, rings and shot markers, and
   * the Commentary tab's per-goal shot graphic. A second copy of these six
   * lines in a second stylesheet is a second place for the tint recipe to
   * drift.
   *
   * The soft step is deeper than the 12% every band above uses. Those are
   * background washes behind type; this one is a bar that has to hold its own
   * hue against the same colour at full strength beside it, and at 12% the
   * trailing side of a row reads as grey rather than as the club's colour.
   */
  --stat-home-rgb: var(--side-home-rgb);
  --stat-away-rgb: var(--side-away-rgb);

  /*
   * The one club of the *player* on screen — the same runtime override one level
   * simpler, for a page whose subject is a person rather than a fixture.
   * `core/team-colours.js`'s `applyClubColour` writes it; the player page's
   * identity band and its shot markers read it.
   *
   * Its own name rather than borrowing `--stat-home-rgb` because that one is
   * half of a pair: a page that set one half and not the other would put a
   * sampled colour next to a fallback and call them a comparison. The fallback
   * here is the accent, not a side colour — a player has no side, and the accent
   * is already what this app paints "the subject of this screen" in.
   */
  --club-rgb: var(--accent-rgb);
  /* The same fallback under a second name, for the player page's own
     `--club-rgb` rule (player-details.css): a rule that redeclares
     `--club-rgb` cannot fall back to `--accent-rgb` there, because that page
     also points `--accent-rgb` at `--club-rgb`, and the pair would be a cycle. */
  --club-fallback-rgb: var(--accent-rgb);

  /*
   * The inverse chip: a ground in the text colour with type in the card
   * colour — a VAR stamp, the kick-off pill, the secondary button, the Slip
   * Builder's tooltips and toasts. It turns over with the theme on purpose:
   * dark on the light page, light on the dark one, so it always stands out
   * from the page the same way. Still awaiting "Black 2" for the light value;
   * --color-text-primary is the darkest confirmed value.
   *
   * --text-on-inverse is the card colour, which is what "inverse" means here —
   * text and card swap places — and why it needs no dark override of its own.
   */
  --surface-inverse: var(--color-text-primary);
  --surface-inverse-rgb: var(--color-text-primary-rgb);
  --text-on-inverse: var(--color-bg-card);
  --text-on-inverse-rgb: var(--color-bg-card-rgb);

  /*
   * The grounds that stay dark in both themes (see the palette note on
   * --color-media). --surface-media is the story viewer's card and the
   * commentary's night-pitch replay; --scrim is what every sheet dims the
   * page with, faded in by the sheet's own opacity.
   */
  --surface-media: var(--color-media);
  --surface-media-rgb: var(--color-media-rgb);
  --scrim: var(--color-scrim);
  /* Channel form for a component's own drop shadow (a switch's thumb, a
     slider's knob) — never --text-rgb, which turns light in the dark theme. */
  --shadow-rgb: var(--color-shadow-rgb);

  /*
   * The device's top inset. Every fullscreen surface — the search overlay, the
   * story viewer, the club and fixture pages — pads by it so nothing sits under
   * a notch or a status bar. Defined here rather than per-component because a
   * missing definition makes every `calc()` that adds it invalid, which drops
   * the whole padding to zero instead of falling back to none of the inset.
   */
  --safe-top: max(
    env(safe-area-inset-top, 0px),
    calc(var(--tg-safe-area-inset-top, 0px) + var(--tg-content-safe-area-inset-top, 0px))
  );

  /* --- The universal scale ------------------------------------------------ */

  /*
   * One hierarchy for every screen. Each scale below is the complete set of
   * steps a component may use for that kind of length, and a component picks
   * a *step* — never a bare number, and never a step multiplied by a knob.
   *
   * That second rule is the one that matters. Until 19 Sep 2026 six screens
   * each carried their own density knob (--slip-scale 0.75, --player-scale
   * 0.915, --fixture-scale 0.84, --header-scale 0.95, --date-scale 0.9,
   * --overview-vscale 0.56), so `var(--text-sm)` rendered at 13px on one
   * screen, 11.9px on the next and 9.75px on a third — the same name, four
   * sizes, none of them on this scale. The knobs are gone; where a screen
   * wants to read denser it chooses a smaller step, which is the difference
   * between a hierarchy and a zoom. Retune the app's density here, once.
   *
   * tools/scale-lint.py fails the build on anything that steps off this
   * scale — a raw font-size, a `* var(--*-scale)`, an icon or crest sized by
   * a literal — so the rule holds on the next screen too.
   */

  /*
   * The app's density, as one number. Every ladder below is its design
   * value multiplied by this, so the whole app gets denser or airier
   * together and the steps keep their ratios. 0.8 from 19 Sep 2026, when
   * the unified scale first rendered and read a fifth too large; 0.88 since
   * 21 Sep, lifted a tenth from there. This is the only multiplier the
   * scale allows: it lives here, on every ladder at
   * once, and no component or screen ever reaches for it — that is what
   * the retired per-screen knobs were, and the lint refuses `var(--scale)`
   * anywhere but this file.
   *
   * It reaches everything drawn: the ladders below, --tap, the shadows,
   * the nav's height, and every box a component draws in literal pixels
   * (`calc(56px * var(--scale))` — the one form in which a component may
   * name the knob, and only for geometry that has no ladder). What it
   * leaves alone, on purpose: hairlines and nudges of 2px and under (they
   * go soft), the leading and tracking ratios, the safe-area insets and
   * --app-max-width (the phone's, not ours), and --tap-floor below.
   */
  --scale: 0.88;

  /*
   * Spacing — 4px base. The two half-steps are for hairline-adjacent gaps
   * (a 2px gap between a label and its unit, a 6px chip padding), the
   * distances the 4px ladder alone kept forcing into raw pixels. Read them as
   * 0.5 and 1.5.
   */
  --space-05: calc(2px * var(--scale));
  --space-1: calc(4px * var(--scale));
  --space-15: calc(6px * var(--scale));
  --space-2: calc(8px * var(--scale));
  --space-3: calc(12px * var(--scale));
  --space-4: calc(16px * var(--scale));
  --space-5: calc(20px * var(--scale));
  --space-6: calc(24px * var(--scale));
  --space-7: calc(32px * var(--scale));
  --space-8: calc(40px * var(--scale));

  /*
   * Type. Twelve steps, and the role each one plays — a component that
   * wants "a caption" reaches for the caption step, not for whichever pixel
   * value looks right in isolation:
   *
   *   3xs  10   micro: unit labels, ticks, the smallest badge. The floor —
   *             nothing renders smaller than this.
   *   2xs  11   caption: meta lines, timestamps, table headers
   *   xs   12   label: chips, tabs, secondary row text
   *   sm   13   body: the list's voice, and the app's default
   *   md   15   title: a row's or card's own title
   *   base 16   lead: inputs (iOS zooms any field set smaller) and lead copy
   *   lg   18   heading: a section's heading
   *   xl   21   screen heading: the largest voice a list may use
   *   2xl  24   display: a card's one big number (odds, a rating)
   *   3xl  28   display: a screen's one big number
   *   4xl  32   scoreline: the fixture page's score, readable at arm's length
   *   5xl  40   hero: the odds converter's result, and nothing else so far
   *
   * The display steps are what a screen is allowed to *lead* with, and each
   * screen leads with one of them at most — a headline set at the size of the
   * row above it is not a headline, and two headlines are none.
   */
  --text-3xs: calc(10px * var(--scale));
  --text-2xs: calc(11px * var(--scale));
  --text-xs: calc(12px * var(--scale));
  --text-sm: calc(13px * var(--scale));
  --text-md: calc(15px * var(--scale));
  --text-base: calc(16px * var(--scale));
  --text-lg: calc(18px * var(--scale));
  --text-xl: calc(21px * var(--scale));
  --text-2xl: calc(24px * var(--scale));
  --text-3xl: calc(28px * var(--scale));
  --text-4xl: calc(32px * var(--scale));
  --text-5xl: calc(40px * var(--scale));

  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;

  /*
   * Leading. Five steps: `none` for a number that must sit exactly in its
   * box (a score, a badge count), `tight` for headings and display figures,
   * `snug` for titles and single-line rows, `normal` for running text,
   * `relaxed` for the rare paragraph that has to breathe (an empty state's
   * explanation).
   */
  --leading-none: 1;
  --leading-tight: 1.2;
  --leading-snug: 1.3;
  --leading-normal: 1.4;
  --leading-relaxed: 1.5;

  /*
   * Tracking, in em so it follows the size it is set on. `tight` is for
   * display figures and headings, `snug` for titles, `wide` for small labels
   * that need air, `wider` and `caps` for uppercase eyebrows — `caps` is the
   * house eyebrow, `wider` the quieter one for uppercase that is not an
   * eyebrow (a tab's label, a column header).
   */
  --tracking-tight: -0.02em;
  --tracking-snug: -0.01em;
  --tracking-normal: 0;
  --tracking-wide: 0.02em;
  --tracking-wider: 0.06em;
  --tracking-caps: 0.08em;

  /*
   * Radii. `sm` for chips and small controls, `md` for buttons, fields and
   * list rows, `lg` for cards, `xl` for floating chrome, `2xl` for a sheet's
   * top corners. A card is a card on every screen, so nothing scales these.
   */
  --radius-sm: calc(8px * var(--scale));
  --radius-md: calc(11px * var(--scale));
  --radius-lg: calc(16px * var(--scale));
  --radius-xl: calc(20px * var(--scale));
  --radius-2xl: calc(24px * var(--scale));
  --radius-full: 999px;

  /*
   * Icons. Icon() pins its size inline, so js/core/sizes.js carries this
   * same ladder as ICON and every call site names a step from it; these
   * tokens are for the boxes a stylesheet draws around one. The steps track
   * the type steps they sit beside: an `xs` icon next to a caption, `md`
   * next to body, `xl` in a bar or a nav item.
   */
  --icon-2xs: calc(10px * var(--scale));
  --icon-xs: calc(12px * var(--scale));
  --icon-sm: calc(14px * var(--scale));
  --icon-md: calc(16px * var(--scale));
  --icon-lg: calc(18px * var(--scale));
  --icon-xl: calc(20px * var(--scale));
  --icon-2xl: calc(24px * var(--scale));
  /* An empty state's illustration — the one icon that is a picture. */
  --icon-hero: calc(96px * var(--scale));

  /*
   * Marks — crests, avatars, flags, portraits: the image boxes Logo() and
   * Avatar() write inline (mirrored as MARK in js/core/sizes.js). `2xs` is
   * the competition badge tucked on a crest's corner, `xs` a flag or a row's
   * competition mark, `sm`/`md` a row crest, `lg` a chip's or a story
   * circle's, `xl` a list avatar, `2xl` a page header's crest, `3xl` the
   * fixture page's, `hero` a profile portrait, `portrait` the profile
   * editor's.
   */
  --mark-2xs: calc(12px * var(--scale));
  --mark-xs: calc(16px * var(--scale));
  --mark-sm: calc(20px * var(--scale));
  --mark-md: calc(24px * var(--scale));
  --mark-lg: calc(32px * var(--scale));
  --mark-xl: calc(40px * var(--scale));
  --mark-2xl: calc(48px * var(--scale));
  --mark-3xl: calc(56px * var(--scale));
  --mark-hero: calc(96px * var(--scale));
  --mark-portrait: calc(128px * var(--scale));

  /*
   * Control heights: `xs` a chip, `sm` a compact button or segmented
   * switch, `md` a field or button, `lg` a primary button, `xl` a CTA or a
   * keypad key. --tap is the app's tap size — 44pt in the design frame,
   * scaled with everything else, since it is also the drawn size of every
   * icon button. --tap-floor is the unscaled 44pt for the few rules that
   * extend an invisible hit area rather than draw a box; at --scale 0.8
   * the drawn --tap is 35px, which is Tobi's call, made on 19 Sep 2026.
   */
  --control-xs: calc(32px * var(--scale));
  --control-sm: calc(36px * var(--scale));
  --control-md: calc(40px * var(--scale));
  --control-lg: calc(48px * var(--scale));
  --control-xl: calc(56px * var(--scale));
  --tap: calc(44px * var(--scale));
  --tap-floor: 44px;

  /*
   * Elevation. With the page white too, the card's fill separates nothing on
   * its own — the border draws the edge and this shadow gives it the shallow
   * lift that says "object on the page" without reading as floating chrome;
   * floating chrome gets the fuller --shadow-floating so it composes over
   * anything it retracts across; the accent glow belongs only under
   * filled-primary elements (selected day, CTAs).
   */
  --shadow-card: 0 1px 2px rgb(22 24 29 / 0.05), 0 calc(4px * var(--scale)) calc(14px * var(--scale)) calc(-4px * var(--scale)) rgb(22 24 29 / 0.07);
  --shadow-floating: 0 calc(4px * var(--scale)) calc(16px * var(--scale)) rgb(0 0 0 / 0.08), 0 1px calc(3px * var(--scale)) rgb(0 0 0 / 0.05);
  --shadow-accent: 0 calc(6px * var(--scale)) calc(18px * var(--scale)) calc(-8px * var(--scale)) rgb(var(--accent-rgb) / 0.55);

  /* Layout */
  --app-max-width: 480px;
  --nav-height: calc(57px * var(--scale));
  /*
   * The tab bar spans this share of the shell width. It is not centred on its
   * own any more: the account bubble (a --nav-height circle) sits --nav-gap
   * to its right, and the pair is centred as one.
   */
  --nav-width: 65%;
  --nav-gap: var(--space-2);
  /* Gap between the active indicator and the bar's inner edge. */
  --nav-indicator-inset: calc(5px * var(--scale));
  /*
   * Full stadium radius reads as a bubble now that BottomNav.js also insets
   * the indicator horizontally (INDICATOR_INSET_X) — there's clearance
   * either side of the label before the curve starts, so it no longer tapers
   * into the text.
   */
  --nav-indicator-radius: var(--radius-full);
  /*
   * Inside Telegram's iOS web view `env(safe-area-inset-bottom)` reads 0 even
   * though the home indicator overlays the bottom of the page, so the nav sat
   * under it. Bot API 8.0 clients publish the real insets themselves as
   * `--tg-safe-area-inset-*` (and, for fullscreen mode, the header's height as
   * `--tg-content-safe-area-inset-top`) on the root element; older clients
   * leave them undefined and the fallbacks make them 0. Both `--safe-*` take
   * the larger of the two sources, so a plain browser is unchanged.
   */
  --safe-bottom: max(env(safe-area-inset-bottom, 0px), var(--tg-safe-area-inset-bottom, 0px));

  /*
   * Motion. The whole scale lives here — see INTERACTIONS.md. Nothing in the
   * app animates longer than --duration-max.
   */
  --duration-press: 110ms; /* touch-down; must read within a frame */
  --duration: 180ms; /* fades, cross-fades, colour */
  --spring-duration: 320ms; /* springs: indicators, sheets, cards */
  --duration-slow: 320ms; /* alias, for non-spring work at the same length */
  --duration-max: 350ms; /* hard ceiling */

  --easing: cubic-bezier(0.2, 0, 0.2, 1);

  /* Every tappable element scales to this on touch-down. Never go below 0.95. */
  --press-scale: 0.96;

  /*
   * Spring: high stiffness, medium damping, slight overshoot. A real spring
   * needs linear(), which is not reliable across the WebView versions
   * Telegram ships on, so this is the equivalent overshoot bezier.
   */
  --spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --stagger-step: 40ms;

  /*
   * How much of the window Telegram's own chrome covers at the bottom, set by
   * `theme.js` from the viewport. Telegram's theme colours are deliberately
   * not bridged in: the app wears its own palette in every Telegram theme, and
   * paints Telegram's chrome to match it (theme.js, `paintAppChrome`).
   */
  --tg-chrome-inset: 0px;
}

/*
 * The derived half of the fixture-colour pair, and the reason it is not in
 * `:root` with the two triples above.
 *
 * A custom property is substituted where it is *declared*, not where it is
 * used. Put `--stat-home: rgb(var(--stat-home-rgb))` on `:root` and it resolves
 * once, against `:root`'s own fallback triple, and inherits down already
 * resolved — so `core/team-colours.js` writing a new triple onto a panel deeper
 * in the tree changes nothing at all. The colours silently stay the fallback
 * pair, which is the worst kind of wrong: it looks deliberate.
 *
 * So the derived values are declared on the same element the override is
 * written to. `data-team-colours` marks that element, and it is set in the
 * markup rather than by the script, so the fallback pair paints correctly from
 * the first frame instead of the panel having no colours at all until a crest
 * comes back over the network.
 */
/*
 * --- Slip Builder --------------------------------------------------------------
 *
 * What is left of the Quick Slip tab's own tokens now that its palette is the
 * app's (see the neutrals, the primary and the status trio above): a handful
 * of colours only that screen has a use for. Everything else it draws is a
 * semantic token like any other component, and it sizes itself from the
 * universal scale like any other screen — the --slip-scale knob that once
 * shrank it to 75% went with the other density knobs (see the scale above).
 */
:root {
  /* The Advanced Markets sheet's decorative stack layer — the card peeking
     out above the sheet, sampled from its reference. Not a surface anything
     sits on, so not a semantic token. */
  --slip-stack: #e9eaee;

  /* The odds gradient's midpoint, the matches dial's hot start, and the soft
     wash the per-pick confidence slider fills with. Not a status colour: it
     is the middle of a scale, never a verdict on its own. */
  --slip-amber: #ffd84d;
  --slip-amber-wash: rgb(255 216 77 / 0.4);
  /* The high band's start ring — the amber taken down far enough to read as
     a ring on the page. */
  --slip-ring-gold: #e5b93c;
  /* The counter badge's celebration patches; the third patch is the accent. */
  --slip-patch-purple: #7c3aed;
  --slip-patch-lime: #8be04a;
  --slip-odds-gradient: linear-gradient(90deg, var(--success) 0%, var(--slip-amber) 55%, var(--error) 100%);
  --slip-risk-hot: linear-gradient(90deg, var(--slip-amber) 0%, var(--error) 100%);
  --slip-confidence-gradient: linear-gradient(90deg, var(--error) 0%, var(--success) 100%);
}

/*
 * --- Lineup Builder --------------------------------------------------------
 *
 * The canvas the Lineup Builder draws (LineupCanvas.js) is a picture of a
 * pitch that gets rendered to an image and shared, and an image has to carry
 * its colours with it — so that file paints its greens as literals rather
 * than reading tokens, the one deliberate exception to the rule at the top
 * of this file. This is the single value of that palette the *UI* also
 * wears: the stat pill floating over the canvas takes the canvas's own ink,
 * so it reads as part of the picture rather than as chrome laid over it.
 * Mirrors `TURF.ink` there; keep the two in step.
 */
:root {
  --turf-ink: #0e3d23;
}

[data-team-colours] {
  /*
   * `core/team-colours.js` writes each sampled crest colour twice — as it
   * reads on the light page and as it reads on the dark one — and this is
   * where the theme picks. The dark theme's rule is below.
   */
  --stat-home-rgb: var(--stat-home-sample-rgb, var(--side-home-rgb));
  --stat-away-rgb: var(--stat-away-sample-rgb, var(--side-away-rgb));
  --stat-home: rgb(var(--stat-home-rgb));
  --stat-away: rgb(var(--stat-away-rgb));
  --stat-home-soft: rgb(var(--stat-home-rgb) / 0.28);
  --stat-away-soft: rgb(var(--stat-away-rgb) / 0.28);
}

/*
 * --- Dark theme --------------------------------------------------------------
 *
 * Set by `data-theme="dark"` on <html>, which the inline script in index.html
 * writes before first paint and `core/theme.js` keeps current: the person's
 * Appearance choice on the Account page, or with "System" the phone's (inside
 * Telegram, Telegram's own light or dark — used only to pick between these two
 * palettes, never to borrow Telegram's colours).
 *
 * The rule that makes this one block rather than a second stylesheet: it
 * overrides the *palette*, and every semantic token above follows. Nothing
 * here is new meaning; each line is the same job done on a dark ground.
 *
 *  - The neutral ladder keeps its cool lean and turns over. The page is the
 *    darkest step and cards are lifted *above* it, as white cards sit above
 *    the off-white page in the light theme — elevation is lighter in both.
 *    Sunken (chips, fields) and pressed step lighter again from the card, so a
 *    chip on a card still reads as a chip.
 *  - Type steps go light. Muted and secondary hold the same distance from the
 *    text they had on white (muted 5.6:1 on a card, secondary 8.9:1).
 *  - The primary lifts to #4c7dff. It is both link type (133 uses) and a fill
 *    under white type (88), and on a dark card those pull opposite ways; this
 *    value is where they meet — 4.7:1 as type on a card, 3.7:1 for white type
 *    on it, the same trade iOS's own dark-mode blue makes.
 *  - LIVE lifts to #ff453a: the light theme's #e00000 is 3.4:1 on the dark
 *    card, under the 4.5:1 its nine-pixel minute needs.
 *  - The `-strong` status steps (and every tri-band built on them) go light
 *    instead of dark — they are type, and type on a dark card has to be the
 *    light end. The base fills are unchanged; the `-bg` grounds become deep
 *    tints. The solid rating chips keep their values: they carry white type,
 *    which is the one thing that does not change.
 *  - Shadows go black and shallower-looking: on a dark page a card separates
 *    by tone, and a cool-grey shadow would read as a glow.
 */
:root[data-theme="dark"] {
  color-scheme: dark;

  --color-bg-page: #0e1014;
  --color-bg-card: #171a20;
  --color-bg-card-rgb: 23 26 32;
  --color-border-subtle: #2a2e37;
  --color-gray: #21252c;
  --color-gray-strong: #2b3038;
  --color-gray-mid: #535a67;
  --color-text-muted: #8b92a1;
  --color-text-secondary: #b3b9c5;
  --color-text-primary: #eef0f4;
  --color-text-primary-rgb: 238 240 244;

  --color-primary: #4c7dff;
  --color-primary-light: #3a68ea;
  --color-info-bg: #1b2542;
  --accent-rgb: 76 125 255;

  --color-accent: #ff453a;

  --color-gold: #dcae3c;
  --color-gold-bg: #2e2611;

  --color-success-strong: #3dc47d;
  --color-success-bg: #12291c;
  --color-warning-strong: #e0ad3e;
  --color-warning-bg: #2e2412;
  --color-error-strong: #ff6a5c;
  --color-error-bg: #351a1a;

  /* The tri-bands' channel forms, restated to match their strong steps. */
  --color-form-win-rgb: 61 196 125;
  --color-form-loss-rgb: 255 106 92;
  --color-rating-high-rgb: 61 196 125;
  --color-rating-mid-rgb: 224 173 62;
  --color-rating-low-rgb: 255 106 92;
  --color-injury-rgb: 255 106 92;
  --color-confidence-high-rgb: 61 196 125;
  --color-confidence-medium-rgb: 224 173 62;

  /* Home / away, lifted to hold 3:1 as a bar on the dark card while still
     carrying white type (the Compare page's leading figure sits on them). */
  --color-side-home: #9b6bff;
  --color-side-home-rgb: 155 107 255;
  --color-side-away: #1c92b2;
  --color-side-away-rgb: 28 146 178;

  --color-scrim: #000000;
  --color-shadow-rgb: 0 0 0;

  /*
   * The pitch's falloff runs lighter centre to deeper edge. It aliases the
   * sunken and pressed steps in the light theme, but here those step *up*
   * from the card, which would turn the falloff into a glowing rim — so the
   * deep step is restated to sit between the card and the sunken fill.
   */
  --pitch-deep: #1b1e24;

  /* A step above the sunken track (#21252c), so a sliding pill reads raised. */
  --surface-raised: #353a44;

  /* The desktop ground: darker than the page here, as it is in the light theme. */
  --canvas: #08090b;

  --shadow-card: 0 1px 2px rgb(0 0 0 / 0.3);
  --shadow-floating: 0 calc(4px * var(--scale)) calc(16px * var(--scale)) rgb(0 0 0 / 0.45), 0 1px calc(3px * var(--scale)) rgb(0 0 0 / 0.3);

  /* The Advanced Markets sheet's peeking stack layer: one step off the sheet. */
  --slip-stack: #262a32;
}

:root[data-theme="dark"] [data-team-colours] {
  --stat-home-rgb: var(--stat-home-sample-dark-rgb, var(--side-home-rgb));
  --stat-away-rgb: var(--stat-away-sample-dark-rgb, var(--side-away-rgb));
  /* 28% of a club colour over the dark card reads as a muddy grey, not as the
     club; the trailing bar needs a little more of it to keep its hue. */
  --stat-home-soft: rgb(var(--stat-home-rgb) / 0.4);
  --stat-away-soft: rgb(var(--stat-away-rgb) / 0.4);
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --duration-press: 0ms;
    --duration: 0ms;
    --spring-duration: 0ms;
    --duration-slow: 0ms;
    --stagger-step: 0ms;
  }
}
