/*
 * 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 {
  /* --- 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;

  --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;

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

  --bg: var(--color-bg-page);
  /*
   * 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);
  --surface-sunken: var(--color-gray);
  /* Touch-down fill for rows and chips — one step deeper than sunken. */
  --surface-pressed: var(--color-gray-strong);
  --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);
  --text-inverse: var(--color-white);
  /* Channel forms, for alpha steps: the story viewer's over its dark card, and
     the Slip Builder's tooltips and touch-down darkening over any fill.
     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);

  /* 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 dark ground the story viewer paints its fullscreen card on. Awaiting
   * "Black 2"; --color-text-primary is the darkest confirmed value and reads
   * correctly behind white type.
   */
  --surface-inverse: var(--color-text-primary);

  /*
   * 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))
  );

  /* Spacing — 4px base */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-7: 32px;
  --space-8: 40px;

  /* Type scale */
  --text-2xs: 11px;
  --text-xs: 12px;
  --text-sm: 13px;
  --text-md: 15px;
  --text-base: 16px;
  --text-lg: 18px;
  --text-xl: 21px;
  /*
   * The display step, and the only thing on this scale a screen is allowed to
   * lead with. It exists for the fixture page's scoreline, which is the one
   * number in the app that has to be readable at arm's length rather than
   * scanned in a column — --text-xl is the list's own largest voice, and a
   * headline set at the same size as the row above it is not a headline.
   * Anything else reaching for this should be asking whether it is really the
   * single most important number on its screen.
   */
  --text-2xl: 32px;

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

  /* Radii */
  --radius-sm: 8px;
  --radius-md: 11px;
  --radius-lg: 16px;
  --radius-xl: 20px;
  --radius-full: 999px;

  /*
   * 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 4px 14px -4px rgb(22 24 29 / 0.07);
  --shadow-floating: 0 4px 16px rgb(0 0 0 / 0.08), 0 1px 3px rgb(0 0 0 / 0.05);
  --shadow-accent: 0 6px 18px -8px rgb(var(--accent-rgb) / 0.55);

  /* Layout */
  --app-max-width: 480px;
  --nav-height: 57px;
  /*
   * 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: 5px;
  /*
   * 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;

  /*
   * Telegram theme bridge. `theme.js` overwrites the --tg-* tokens from
   * themeParams at runtime; these fallbacks resolve to the palette above, so a
   * plain browser tab renders the design system's own colours.
   */
  --tg-surface: var(--surface);
  --tg-surface-rgb: var(--color-bg-card-rgb);
  --tg-text: var(--text);
  --tg-hint: var(--text-muted);
  --tg-border: var(--border);
  --tg-accent: var(--accent);
  --tg-accent-text: var(--accent-text);
  --tg-accent-rgb: var(--accent-rgb);
  /* The error red, now that the palette has one — distinct from --highlight's
     LIVE red, as the note on --color-accent asks. */
  --tg-destructive: var(--color-error);
  --tg-link: var(--color-primary);
  --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): the scale
 * knob, and a handful of colours only that screen has a use for. Everything
 * else it draws is a semantic token like any other component.
 */
:root {
  /*
   * One knob for the whole Slip Builder. Every length in slip-builder.css and
   * slip-controls.css — type, spacing, heights, radii, travel — is a value
   * multiplied by this, the same arrangement --fixture-scale gives the
   * fixtures list, so the screen scales as a unit. Hairlines (1–2px) and the
   * 44pt hit areas on steppers, slider thumbs and chip remove buttons are
   * deliberately not: the first go soft, the second go unusable. SlipControls.js
   * mirrors the value as SLIP_SCALE for the icon sizes Icon() pins inline;
   * keep the two in step.
   */
  --slip-scale: 0.75;

  /* 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] {
  --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);
}

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