@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700;800&family=Inter:wght@400;500;600&display=swap');

/* ---------------------------------------------------------------------------
   TAN PAGE, DARK CARDS. The default, and the default for everyone.

   NO prefers-color-scheme BLOCK EXISTS ANYWHERE IN THIS FILE, and there is no
   theme the device can select. City Connect is dark-ground, but a READER picks
   it from the settings screen - a phone that switches to dark at sunset still
   shows whichever theme was chosen, and a phone left in light still shows city
   if that is the choice.

   There WAS a dark palette here, and it was a different thing entirely: it
   followed the device automatically, nothing in the UI could turn it off, and
   it carried three documented sub-AA pairs. It was deleted. The presence of a
   dark-GROUND theme below is not that coming back, and check-contrast still
   asserts no prefers-color-scheme block exists.

   */
:root {
  /* THE SAME DECLARATION THE META IN THE HEAD MAKES, and not a duplicate of it.
     The meta is read before any CSS arrives and covers the window where the UA
     is deciding what to paint; this is the authoritative property afterwards,
     and the only one that shows up in computed style - measured, the meta alone
     leaves this reading "normal", so there was nothing to inspect.

     THIS IS TAN'S VALUE, NOT THE APP'S. It was one value for every theme while
     every theme was light-ground, and the comment here said a dark-ground
     theme would move it into the per-theme blocks. City did exactly that, so
     this is now the default's own declaration and each other block carries its
     own - which is also why the stamp in index.html rewrites the meta per
     theme rather than shipping one constant. */
  color-scheme: light;

  /* THE PAGE GROUND IS TAN, NOT WHITE. Measured against every token that
     sits on it, which is why nothing else moved: body text 9.36, dim text
     4.78, and a brown card 9.36 as a block against the page. Gold drops
     from 1.73 to 1.25 here, so it stays card-only and the allowlist that
     enforces that matters more, not less. */
  --bg: #E5DAC6;              /* the page ground */
  --surface: #3F2E1E;         /* cards - dark brown */
  --surface-raised: #54402C;  /* nested blocks inside cards */

  /* Text on the PAGE. */
  --text: #3F2E1E;            /* 12.96 on the page */
  --text-dim: #6B5A46;        /* 6.61 on the page */

  /* Text INSIDE a card. Same two roles, the other ground. */
  --text-on-surface: #F5EFE6; /* 11.33 on --surface */
  --dim-on-surface: #C9BCA8;  /* 6.94 on --surface */

  /* Two grounds means two hairlines as well - a dark overlay is invisible on
     a brown card and a light one is invisible on the page. */
  --border: rgba(63, 46, 30, 0.18);
  --border-on-surface: rgba(245, 239, 230, 0.20);
  --shadow: rgba(63, 46, 30, 0.10);

  --gold: #FEB902;            /* CARD-ONLY - 1.73 on the page */
  --gold-ink: #FEB902;        /* on a card the brand gold IS the text gold */
  --gold-soft: #FFD15C;       /* 5.71 on the gold tint */
  --on-gold: #3F2E1E;         /* 7.50 as ink on a gold fill */
  --gold-wash: rgba(254, 185, 2, 0.08);  /* 0.12 put gold at 4.44 on its own wash */
  --gold-edge: rgba(254, 185, 2, 0.32);

  --brick: #F5A79E;           /* card-only; 6.73 on surface, 5.08 on raised */
  --moss: #9ACB86;            /* card-only; 6.95 on surface, 5.25 on raised */
  --brick-tint: rgba(245, 167, 158, 0.14);
  --moss-tint: rgba(154, 203, 134, 0.18);
  --gold-tint: rgba(254, 185, 2, 0.20);
  --neutral-tint: rgba(245, 239, 230, 0.10);

  --skeleton-a: rgba(245, 239, 230, 0.06);
  --skeleton-b: rgba(245, 239, 230, 0.12);
  --live-ring: rgba(254, 185, 2, 0.45);
  --highlight-ring: rgba(254, 185, 2, 0.35);

  /* The playoff cut line, inside the standings card. */
  --cut: #F5EFE6;
  --cut-label: #C9BCA8;
  --in-tint: rgba(254, 185, 2, 0.14);
/* THE REPORTER AVATARS, which were five hex literals in app.js applied as an
     inline background. Invisible in tan - they were chosen against tan's ink -
     and wrong in any other theme, because a theme cannot override what a
     renderer hardcodes. Each is measured against --avatar-ink below. */
  --avatar-ink: #3F2E1E;
  --avatar-1: #FFC425;        /* 8.13 on the ink */
  --avatar-2: #F5A79E;        /* 6.73 */
  --avatar-3: #8FC17A;        /* 6.22 */
  --avatar-4: #C9BCA8;        /* 6.94 */
  --avatar-5: #E8BE5E;        /* 7.38 */

  /* THE TILE STRIP, THE HEADER BAND AND THE LAST PLAY BOX (2026-09-14).
     The six destinations are one light panel with hairline dividers and 11px
     labels, so the labels are the NORMAL tier - 4.5 - where the old tiles
     were 18.67px/700. The header band is a colour the lockup was drawn for:
     cream here, since the surface brown would put its letters at 1.21. */
  --strip-fill: #F5EFE6;      /* 1.21 on the page; its edge is --border */
  --on-strip: #3F2E1E;        /* 11.33 */
  --strip-divider: rgba(63, 46, 30, 0.18);  /* 1.39 on the fill */
  --strip-mark: #3F2E1E;      /* the active cell's bar - 11.33 */
  --header-band: #F5EFE6;     /* 1.21 on the page; edged by --border */
  --on-header-band: #3F2E1E;  /* 11.33 - the menu and its links */
  /* THE PALMS IN THE BAND (2026-09-15): white, the only direction that moves
     this band AWAY from the lockup's ink - every one of its 102,971 ink
     pixels is darker than the cream. 1.143:1 across itself (the ceiling is
     1.4), dE00 5.5 from the band - the band itself is 6.4 from the page.
     test-band-art judges every ink pixel on the worst ground: none loses. */
  --band-art: #FFFFFF;
  --band-art-alpha: 1;
  --band-art-display: block;
  --lastplay-fill: #655341;   /* 1.34 on the gameday fill; its text 6.40 */
  --lastplay-edge: transparent;
  /* THE ACTIVE CHIP, as a role rather than as gold. */
  --chip-on: #FEB902;         /* the accent fill, as before */
  --on-chip-on: #3F2E1E;      /* 7.50 */
  --chip-on-edge: transparent;
  /* THE TRANSACTION PILLS (2026-09-14): four meanings, each a fill with its
     own ink, on the day's card. check-contrast holds the ink on the fill at
     4.5, the fill on the card at 1.3, and every pair of fills at least 20
     apart in CIEDE2000, so they read as four colours rather than two. The
     word inside carries the meaning; the colour only speeds the scan. */
  --tx-added: #9ACB86;        /* ink 6.95 */
  --on-tx-added: #3F2E1E;
  --tx-removed: #F5A79E;      /* ink 6.73 */
  --on-tx-removed: #3F2E1E;
  --tx-minors: #9DC3E8;       /* ink 7.04 */
  --on-tx-minors: #3F2E1E;
  --tx-status: #FEB902;       /* ink 7.50 */
  --on-tx-status: #3F2E1E;
  /* THE INJURY STATUS DOTS (2026-09-15): two marks beside the words they
     decorate - a rehab assignment and a move to the 60-day list, both from
     MLB's transaction records. The Added and Status pill hues; held to 3:1
     on the card and 20 apart in check-contrast. */
  --inj-dot-rehab: #9ACB86;   /* 6.95 on the card */
  --inj-dot-moved: #FEB902;   /* 7.50 */

  /* A BACKING FOR TEXT THAT WOULD OTHERWISE SIT ON THE PAGE. Transparent
     here - these resolve to the page tokens and change nothing - and a solid
     surface in camo, whose page is a pattern with no contrast ratio. Every
     rule that used to put --text or --text-dim straight on the ground reads
     these instead, so one token block is the whole difference. */
  --backing: transparent;
  --on-backing: #3F2E1E;      /* = --text */
  --dim-on-backing: #6B5A46;  /* = --text-dim */
  --backing-border: rgba(63, 46, 30, 0.18);
  --backing-pad: 0;

  /* THE THIRD SEPARATION MECHANISM, after lift and hairline: a 2px keyline
     every card wears when the ground it sits on cannot be measured against.
     Zero width here; camo sets it. box-shadow so it follows border-radius
     everywhere and moves no layout. */
  --keyline: 0 0 0 0 transparent;

  /* CARDS MAY DIFFER FROM THE SURFACE, AND ALTERNATE. Beach paints story
     cards in two pastel tints while its chips, badges and tables stay white;
     the card scopes --surface and --surface-raised to its fill, so every
     rule inside it reads the fill without knowing. Even cards take the -alt
     set. Themes with one card colour repeat --surface here, and the
     alternation is a no-op. The edge is the card's border; the bar is the
     confirmation marker on its left; the pill pair is the Notable pill's
     background and ink, swapped per fill so it never vanishes into a card
     of its own family. Gameday and standings name their own fill and edge. */
  --card-fill: #3F2E1E;
  --card-fill-raised: #54402C;
  --card-fill-alt: #3F2E1E;
  --card-fill-alt-raised: #54402C;
  --card-edge: rgba(245, 239, 230, 0.20);   /* = --border-on-surface */
  --card-edge-alt: rgba(245, 239, 230, 0.20);
  --card-edge-width: 1px;
  --card-bar: #9ACB86;        /* = --moss, the confirmation bar */
  --card-bar-alt: #9ACB86;
  --gold-tint-alt: rgba(254, 185, 2, 0.20);
  --gold-soft-alt: #FFD15C;
  --gameday-fill: #54402C;    /* the raised brown, as before */
  --gameday-fill-raised: #54402C;
  --gameday-edge: #FEB902;
  --standings-fill: #3F2E1E;
  --standings-edge: rgba(245, 239, 230, 0.20);
  --standings-edge-width: 1px;

  /* INSIDE THE CARD: the 1px line just inside the border, the dividers
     between a card's parts, the ring on the avatar stack and on the subject
     headshot, and the width of the gameday's border. Defaults repeat the
     hairline and the surface, so three themes look exactly as they did;
     beach paints the pink here. */
  --card-inner: transparent;
  --card-rule: rgba(245, 239, 230, 0.20);   /* = --border-on-surface */
  --avatar-ring: #3F2E1E;                   /* = --surface */
  --subject-ring: rgba(245, 239, 230, 0.20);
  --gameday-edge-width: 3px;
}

/* ---------------------------------------------------------------------------
   BEACH - "OG City Connects". Sampled from the logo, and the inverse of tan
   in one important way: the page is near-white and nothing on it separates
   by luminance. The separation is the hairline - and on the story cards the
   hairline IS the brand border.

   EVERY CARD IS MINT, WITH A GOLD BORDER AND PINK INSIDE - the treatment the
   old reference tiles got, from Rick's screenshot of the feed. The cards
   alternated pink and mint for one build; nothing has a pink FILL now.
   --card-fill and --card-fill-alt are both the mint, so the alternation
   rule was removed and the suites derive one card palette for beach instead
   of two. The pink is INSIDE: a 1px line inside the 2px yellow border, the
   dividers between a card's parts, the avatar and headshot rings, the
   confirmation bar, the Notable pill's tint (with the dark ink), and the
   pink-ink link and source-box outline. Gameday and standings take the same
   treatment. The chips, badges and tables stay white: --surface is still
   white here, and the card scopes --surface to its fill for everything
   inside it.

   THE PINK IS A FLOOR. #FFCCE4 holds the ink at 10.42, the dim text at 4.63
   and the link at 5.08; one step more saturated (#FEAED3) fails the dim text
   at 3.76 and the link at 4.13. Mint (#AFFDF4) has headroom - 12.63 / 5.60 /
   6.21 - and if the two ever need matching in intensity, mint moves toward
   the pink, not the reverse.

   EVERY ON-CARD PAIR IS MEASURED ON THE MINT AND ON THE WHITE, in
   check-contrast and in the ground audit. Two values chosen while the cards
   were still half pink are kept, because they are better on mint too and a
   future pink fill would need them again: --in-tint is white at 0.45 (dim
   text 5.95 on mint; mint at 0.14 had measured 4.17 over pink) and --brick
   is #A0211A (6.67 on mint, 5.75 on its tint). The Notable pill is the pink
   as a tint with the DARK ink on it - 8.88 on mint - because the pink ink
   fails on any pink tint that can be seen.

   THE HUE / INK SPLIT IS THE WHOLE PALETTE. The three brand hues measure 3.46,
   2.29 and 1.50 against the ground - none of them can carry text at any size.
   They are for rules, bars, outlines, chip backgrounds that hold no words, and
   the underline under the masthead. Every letter and every icon uses --text,
   --text-dim, or one of the two inks, which were chosen to clear 7:1.

   Measured, so nobody has to rediscover it:
       white on --pink-ink    7.11  OK
       white on --mint-ink    7.17  OK
       --text on --mint       6.28  OK
       --text on --yellow     9.56  OK
       --text on --pink       4.15  FAILS  - pink never sits behind words
       white on --pink        3.52  FAILS  - nor does white

   Yellow is used the way the uniform uses it: an outline or an underline
   against a darker shape, never a fill behind text.
   --------------------------------------------------------------------------- */
:root[data-theme="beach"] {
  /* SPELLED OUT RATHER THAN INHERITED from :root. It would be correct either
     way - beach is light-ground - but this property now varies by theme, and a
     block that is silent about it reads as "not considered" rather than
     "light". The stamp and themes.js both name it for every theme, and
     test-themes fails if any of the three disagree. */
  color-scheme: light;

  --bg: #FFFDFB;              /* warm near-white ground */
  --surface: #FFFFFF;         /* chips, badges, tables - NOT the cards */
  --surface-raised: #FFF3F8;  /* nested blocks on those */

  /* Text on the PAGE. */
  --text: #0C2E2A;            /* 14.37 on the ground */
  --text-dim: #4A6360;        /* 6.38 on the ground */

  /* Text INSIDE a card. Both grounds are near-white here, so unlike tan these
     are the same two values rather than an inverted pair. */
  --text-on-surface: #0C2E2A; /* 14.58 on --surface */
  --dim-on-surface: #4A6360;  /* 6.47 on --surface */

  /* THE HAIRLINE IS LOAD-BEARING HERE, not decorative. It is the only thing
     separating a white card from a near-white page. */
  --border: rgba(12, 46, 42, 0.16);
  --border-on-surface: rgba(12, 46, 42, 0.20);  /* 0.14 measured 1.29 on the pink card and 1.30 on the mint - under the 1.3 hairline floor */
  --shadow: rgba(12, 46, 42, 0.08);

  /* The accent role tan fills with gold. Beach fills it with the pink INK, so
     that white sits on it at 7.11 - the hue itself could not hold a letter. */
  --gold: #AF0252;            /* pink-ink: the accent fill and rule colour */
  --gold-ink: #AF0252;        /* 7.11 on --surface */
  --gold-soft: #0C2E2A;       /* the ink, on the Notable pill's tint */
  --on-gold: #FFFFFF;         /* 7.11 as ink on the pink fill */
  --gold-wash: rgba(175, 2, 82, 0.05);
  --gold-edge: #AF0252;       /* the source-box outline: pink-ink, 6.16 on mint */

  /* Semantic, and dark enough to be text on a white card. */
  --brick: #A0211A;           /* 4.81 on its tint over the pink card */
  --moss: #036358;
  --brick-tint: rgba(179, 38, 30, 0.09);
  --moss-tint: rgba(3, 99, 88, 0.09);
  /* THE NOTABLE PILL, per fill: the OPPOSITE brand hue as a tint, with the
     dark ink on it. On pink cards mint (this); on mint cards pink (-alt). */
  --gold-tint: rgba(253, 47, 142, 0.25);  /* the pink, on a mint card */
  --neutral-tint: rgba(12, 46, 42, 0.05);

  --skeleton-a: rgba(12, 46, 42, 0.05);
  --skeleton-b: rgba(12, 46, 42, 0.10);
  /* DECORATION, and the one place the raw hues belong: rings hold no text. */
  --live-ring: rgba(253, 47, 142, 0.45);
  --highlight-ring: rgba(254, 203, 6, 0.55);

  /* The playoff cut line, inside the standings card. */
  --cut: #4A6360;
  --cut-label: #4A6360;
  /* WHITE, NOT MINT. A mint band over the pink card put the dim text at
     4.17; a lightening band lifts every ink on both fills: 5.39 / 5.95. */
  --in-tint: rgba(255, 255, 255, 0.45);

  /* NO RAW PINK HERE. #FD2F8E measures 4.15 against the ink and these discs
     carry initials, so it is the one brand hue the set cannot use. */
  --avatar-ink: #0C2E2A;
  /* Re-chosen against BOTH card fills: #FFC2DE and #A8EDE4 vanished into
     the pink and mint cards (1.07 / 1.14). The ink clears every disc; the
     disc-vs-fill numbers are printed by check-contrast as information, since
     the stack is read by its initials and its rings. */
  --avatar-1: #05BFAA;        /* 6.28 on the ink · 1.66 vs pink, 2.01 vs mint */
  --avatar-2: #FF9FCB;        /* 7.69 · 1.36 / 1.64 */
  --avatar-3: #FECB06;        /* 9.56 · 1.09 / 1.32 */
  --avatar-4: #FFFFFF;        /* 14.58 · 1.40 / 1.16 */
  --avatar-5: #FFE9A3;        /* 12.11 · 1.16 / 1.04 */

  /* THE THREE BRAND HUES. Decoration only - no rule in this sheet may use one
     as a color, and check-contrast asserts that for every theme. */
  --pink: #FD2F8E;
  --mint: #05BFAA;
  --yellow: #FECB06;

  /* THE TILE STRIP: a white panel with pink-ink labels, 7.11 at the normal
     tier. The header band is the surface white, which is the page's own
     colour (1.01), so it is found by its --border edge - beach separates by
     hairline everywhere. The last play box is white inside the mint card and
     edged the same way. The chips are 13px, which is why the active chip is
     the pink INK (#AF0252, white on it 7.11) and not the pink itself. */
  --strip-fill: #FFFFFF;
  --on-strip: #AF0252;        /* pink-ink - 7.11 */
  --strip-divider: rgba(12, 46, 42, 0.20);  /* 1.49 on the fill */
  --strip-mark: #AF0252;      /* 7.11 */
  --header-band: #FFFFFF;
  --on-header-band: #0C2E2A;  /* 14.58 */
  /* No palms: the band is already white and every ink pixel of this lockup is
     darker, so any art could only move the ground toward the ink. */
  --band-art: transparent;
  --band-art-alpha: 0;
  --band-art-display: none;
  --lastplay-fill: #FFFFFF;   /* found by its edge in the mint card */
  --lastplay-edge: rgba(12, 46, 42, 0.20);  /* 1.49 on the white */
  --chip-on: #AF0252;         /* pink-ink, the same fill --gold already is */
  --on-chip-on: #FFFFFF;      /* 7.11 */
  --chip-on-edge: transparent;
  /* The transaction pills: dark fills under white, since no brand hue here
     can carry text. Green and red are the theme's inks; navy and bronze are
     added for Minors and Status. */
  --tx-added: #036358;        /* white 7.17 */
  --on-tx-added: #FFFFFF;
  --tx-removed: #B3261E;      /* white 6.54 */
  --on-tx-removed: #FFFFFF;
  --tx-minors: #1D4E89;       /* white 8.39 */
  --on-tx-minors: #FFFFFF;
  --tx-status: #6B4A00;       /* white 8.06 */
  --on-tx-status: #FFFFFF;
  --inj-dot-rehab: #036358;   /* the injury status dots: 6.21 on mint */
  --inj-dot-moved: #6B4A00;   /* 6.98 */
  --backing: transparent;
  --on-backing: #0C2E2A;
  --dim-on-backing: #4A6360;
  --backing-border: rgba(12, 46, 42, 0.16);
  --backing-pad: 0;
  --keyline: 0 0 0 0 transparent;

  /* CARDS MAY DIFFER FROM THE SURFACE, AND ALTERNATE. Beach paints story
     cards in two pastel tints while its chips, badges and tables stay white;
     the card scopes --surface and --surface-raised to its fill, so every
     rule inside it reads the fill without knowing. Even cards take the -alt
     set. Themes with one card colour repeat --surface here, and the
     alternation is a no-op. The edge is the card's border; the bar is the
     confirmation marker on its left; the pill pair is the Notable pill's
     background and ink, swapped per fill so it never vanishes into a card
     of its own family. Gameday and standings name their own fill and edge. */
  /* MINT, BOTH SLOTS. --card-pink (#FFCCE4) stays a value in this file only
     as the pill's family; nothing has a pink fill. */
  --card-fill: #AFFDF4;       /* mint - ink 12.63, dim 5.60, link 6.16 */
  --card-fill-raised: #D6FEF9;   /* dim 5.98 · 1.07 vs the card */
  --card-fill-alt: #AFFDF4;
  --card-fill-alt-raised: #D6FEF9;
  /* THE GOLD BORDER, 2px: 1.32 on mint, 1.50 on the page - decorative, what
     was asked for, printed as info. */
  --card-edge: #FECB06;
  --card-edge-alt: #FECB06;
  --card-edge-width: 2px;
  /* THE PINK INSIDE. The 1px line inside the gold (3.04 on mint), the
     dividers, both rings, the bar. */
  --card-inner: #FD2F8E;
  --card-rule: #FD2F8E;
  --avatar-ring: #FD2F8E;
  --subject-ring: #FD2F8E;
  --card-bar: #FD2F8E;
  --card-bar-alt: #FD2F8E;
  --gold-tint-alt: rgba(253, 47, 142, 0.25);
  --gold-soft-alt: #0C2E2A;
  /* Gameday and standings: the same card. The pink 3px gameday edge is gone;
     it is 2px gold with the pink line inside, like every other card. */
  --gameday-fill: #AFFDF4;
  --gameday-fill-raised: #D6FEF9;
  --gameday-edge: #FECB06;
  --gameday-edge-width: 2px;
  --standings-fill: #AFFDF4;
  --standings-edge: #FECB06;
  --standings-edge-width: 2px;
}

/* ---------------------------------------------------------------------------
   CITY CONNECT. Obsidian page, obsidian cards, bone text.

   THIS IS NOT THE DARK MODE THAT WAS REMOVED, and the difference is the whole
   point. That was the app FOLLOWING THE DEVICE - a prefers-color-scheme switch
   that took the reader's theme away from them when their phone changed at
   sunset, and which nothing in the UI could turn off. This is a theme A READER
   PICKS from the settings screen, exactly as they pick tan or beach, and the
   device setting still changes nothing. If you are here because you think the
   removal was reverted: it was not. There is still no prefers-color-scheme
   block in this file and check-contrast still asserts there is none.

   What DID change is that color-scheme is no longer one value for the whole
   app. It used to be light everywhere because every theme was light-ground;
   the comment on :root said a dark-ground theme would move it into the
   per-theme blocks, and this is that theme, so it did. See the stamp in
   index.html, which sets the matching meta before any CSS arrives.

   SEPARATION IS THE HAIRLINE, NOT LUMINANCE - the same mechanism as beach and
   for a measured reason. The card is 1.22 against the page and the raised
   block 1.51, so neither clears 3:1, and the fix is not to lighten them: a
   card that clears 3:1 above obsidian lands around #1167C6, a bright royal
   blue that stops reading as obsidian at all and drags aqua down to 3.70. So
   the card is found by its edge, and --border-on-surface is load-bearing at
   3.02 rather than decorative. check-contrast holds this theme to the hairline
   rule instead of the card-on-page one, and the headshot disc measures 1.24 on
   the card - below the structural floor - which is the same fact again.

   THE FIRST THEME WHERE A BRAND HUE CARRIES TEXT. Tan splits gold into a fill
   and an ink; beach cannot use its hues for letters at all. Here aqua is 11.15
   on the page and 9.13 on a card, so the accent role is simply aqua - there is
   no derived ink, because deriving one would be inventing a colour to solve a
   problem the measurement says does not exist.

   The role tokens are named for tan, which is where they were coined. Read
   --gold as "the accent", not as the colour gold:
       --gold, --gold-ink   aqua      the fill, and the same aqua as text
       --gold-soft          gold      the secondary, on the accent tint
       --on-gold            obsidian  11.15 as ink on the aqua fill

   MARIGOLD (#FCA903) IS DELIBERATELY ABSENT. It was in the sampled set beside
   gold (#FEB902) and the two measure 1.12 against each other - the same colour
   to any eye. See the note in themes.js for why it was dropped rather than
   given a role.

   FIREBERRY IS DECORATION ONLY, for the same structural reason beach's pink
   is, and it is the only hue here that earns a prohibition:
       fireberry on the page       4.08  FAILS as text
       fireberry on a card         3.34  FAILS
       fireberry on a raised block 2.70  FAILS
       white on fireberry          4.10  FAILS as a fill
       bone on fireberry           3.51  FAILS as a fill
   It fails in both directions, so nothing can be written on it and it cannot
   be written with. check-contrast's hue scan governs it per theme.
   --------------------------------------------------------------------------- */
:root[data-theme="city"] {
  /* THE ONE THEME WHERE THIS IS NOT light. The UA paints scrollbars, form
     controls and the canvas behind the page from this, and none of them are
     set by any rule in this file - on an obsidian page a light-rendered
     scrollbar is a white stripe down the side. The pre-paint stamp writes the
     matching meta, and test-themes asserts the three copies agree. */
  color-scheme: dark;

  --bg: #051E3A;              /* obsidian - the page ground */
  --surface: #0E2E52;         /* cards - 1.22 on the page, see the hairline */
  --surface-raised: #183D66;  /* nested blocks inside cards */

  /* Text on the PAGE. */
  --text: #FCEBD0;            /* bone - 14.31 on the ground */
  --text-dim: #99A7BA;        /* 6.85 on the ground */

  /* Text INSIDE a card. Both grounds are obsidian here, so like beach and
     unlike tan these are the same two values rather than an inverted pair. */
  --text-on-surface: #FCEBD0; /* 11.71 on --surface, 9.45 on --raised */
  --dim-on-surface: #99A7BA;  /* 5.61 on --surface, 4.53 on --raised */

  /* THE HAIRLINE IS THE SEPARATION MECHANISM, not a decoration on top of it.
     A solid blue rather than an alpha overlay because it has to hold 3.02
     against the card whatever the card is composited over. */
  --border: rgba(252, 235, 208, 0.20);  /* 1.75 on the page */
  --border-on-surface: #4978B4;         /* 3.02 on --surface */
  --shadow: rgba(2, 10, 20, 0.45);

  /* THE ACCENT IS AQUA, undivided. */
  --gold: #00E9F5;            /* the fill, and the rule colour */
  --gold-ink: #00E9F5;        /* 9.13 on --surface, 7.36 on --raised */
  --gold-soft: #FEB902;       /* gold, the secondary - 5.65 on the tint */
  --on-gold: #051E3A;         /* obsidian, 11.15 as ink on the aqua fill */
  --gold-wash: rgba(0, 233, 245, 0.08);  /* aqua stays 7.55 on its own wash */
  --gold-edge: rgba(0, 233, 245, 0.32);

  /* Semantic, and light enough to be text on an obsidian card. */
  --brick: #F5A79E;           /* 7.12 on surface, 5.74 on raised */
  --moss: #7FE3A6;            /* 8.78 on surface, 7.08 on raised */
  --brick-tint: rgba(245, 167, 158, 0.14);
  --moss-tint: rgba(127, 227, 166, 0.14);
  --gold-tint: rgba(0, 233, 245, 0.14);
  --neutral-tint: rgba(252, 235, 208, 0.10);

  --skeleton-a: rgba(252, 235, 208, 0.06);
  --skeleton-b: rgba(252, 235, 208, 0.12);
  --live-ring: rgba(0, 233, 245, 0.45);
  --highlight-ring: rgba(0, 233, 245, 0.35);

  /* The playoff cut line, inside the standings card. */
  --cut: #FCEBD0;
  --cut-label: #99A7BA;
  /* 0.08, NOT beach's 0.14. The band lightens the card under every colour on
     the row, and the dim text has the least headroom: 0.14 puts it at 4.00 and
     0.10 at 4.41. Measured, then chosen - not copied from the theme above. */
  --in-tint: rgba(0, 233, 245, 0.08);

  /* Obsidian ink on light discs - the inverse of tan, whose discs are light
     with brown ink, and the same arrangement either way. --border-on-surface
     is NOT among them: the ink measures 3.69 on it, which is a hairline's
     number and not a disc's. */
  --avatar-ink: #051E3A;
  --avatar-1: #00E9F5;        /* 11.15 on the ink */
  --avatar-2: #FEB902;        /* 9.70 */
  --avatar-3: #7FE3A6;        /* 10.72 */
  --avatar-4: #F5A79E;        /* 8.70 */
  --avatar-5: #99A7BA;        /* 6.85 */

  /* THE DECORATION-ONLY HUES. Aqua and gold both carry text here and are
     reached through the accent roles above; these two are the ones no rule
     may use as a colour, and check-contrast requires each to fail as text on
     the ground before it will accept the prohibition. */
  --fireberry: #F5026E;       /* 4.08 on the page - fails */
  /* EMBER: the rose orange from the logo. Sampled, hue 24, and 2.21 from
     gold so it is a genuinely distinct token - marigold was 1.12 from gold
     and was dropped for exactly that. 4.39 on obsidian, so the edge reads
     against the page; 1.08 on fireberry, so it does NOT read against the
     fill - which is why it was piping on the OUTER edge of the old tiles
     and never a line on them. 4.39 is under 4.5: it earns its place on the list. */
  --ember: #DE5801;

  /* THE TILE STRIP is the card surface with bone labels, 11.71 at the normal
     tier - white on the old fireberry tiles was 4.10 and needed the large
     tier, which 11px labels do not have. The dividers are the card hairline
     (3.02) and the active cell's bar is aqua (9.13). The header band is the
     surface too: the lockup's white and aqua letters were drawn for it. The
     active chip is NOT a fireberry fill - chips are 13px - it is the card
     fill with a fireberry edge (3.34 on the card, 4.08 on the page) and bone
     letters. */
  --strip-fill: #0E2E52;      /* 1.22 on the page; its edge is --border */
  --on-strip: #FCEBD0;        /* 11.71 */
  --strip-divider: #4978B4;   /* 3.02 */
  --strip-mark: #00E9F5;      /* 9.13 */
  --header-band: #0E2E52;     /* 1.22 on the page; edged by --border */
  --on-header-band: #FCEBD0;  /* 11.71 */
  /* No palms: this lockup's ink sits on both sides of the band - 70.8% of it
     lighter, 29.2% darker - so art in either direction moves the ground
     toward some of it (dark palms cost the darker 29.2%, light ones 69%). */
  --band-art: transparent;
  --band-art-alpha: 0;
  --band-art-display: none;
  --lastplay-fill: #2F5075;   /* 1.34 on the gameday fill; its text 7.06 */
  --lastplay-edge: transparent;
  --chip-on: #0E2E52;         /* the card */
  --on-chip-on: #FCEBD0;      /* 11.71 */
  --chip-on-edge: #F5026E;    /* 3.34 on the card - a visible edge, not text */
  /* The transaction pills: light fills under obsidian. Minors is a light
     blue rather than the aqua accent. */
  --tx-added: #7FE3A6;        /* obsidian 10.72 */
  --on-tx-added: #051E3A;
  --tx-removed: #F5A79E;      /* 8.70 */
  --on-tx-removed: #051E3A;
  --tx-minors: #9CC8FF;       /* 9.67 */
  --on-tx-minors: #051E3A;
  --tx-status: #FEB902;       /* 9.70 */
  --on-tx-status: #051E3A;
  --inj-dot-rehab: #7FE3A6;   /* the injury status dots: 8.78 on the card */
  --inj-dot-moved: #FEB902;   /* 7.94 */
  --backing: transparent;
  --on-backing: #FCEBD0;
  --dim-on-backing: #99A7BA;
  --backing-border: rgba(252, 235, 208, 0.20);
  --backing-pad: 0;
  --keyline: 0 0 0 0 transparent;

  /* CARDS MAY DIFFER FROM THE SURFACE, AND ALTERNATE. Beach paints story
     cards in two pastel tints while its chips, badges and tables stay white;
     the card scopes --surface and --surface-raised to its fill, so every
     rule inside it reads the fill without knowing. Even cards take the -alt
     set. Themes with one card colour repeat --surface here, and the
     alternation is a no-op. The edge is the card's border; the bar is the
     confirmation marker on its left; the pill pair is the Notable pill's
     background and ink, swapped per fill so it never vanishes into a card
     of its own family. Gameday and standings name their own fill and edge. */
  --card-fill: #0E2E52;
  --card-fill-raised: #183D66;
  --card-fill-alt: #0E2E52;
  --card-fill-alt-raised: #183D66;
  --card-edge: #4978B4;
  --card-edge-alt: #4978B4;
  --card-edge-width: 1px;
  --card-bar: #7FE3A6;
  --card-bar-alt: #7FE3A6;
  --gold-tint-alt: rgba(0, 233, 245, 0.14);
  --gold-soft-alt: #FEB902;
  --gameday-fill: #183D66;
  --gameday-fill-raised: #183D66;
  --gameday-edge: #00E9F5;
  --standings-fill: #0E2E52;
  --standings-edge: #4978B4;
  --standings-edge-width: 1px;

  /* INSIDE THE CARD: the 1px line just inside the border, the dividers
     between a card's parts, the ring on the avatar stack and on the subject
     headshot, and the width of the gameday's border. Defaults repeat the
     hairline and the surface, so three themes look exactly as they did;
     beach paints the pink here. */
  --card-inner: transparent;
  --card-rule: #4978B4;
  --avatar-ring: #0E2E52;
  --subject-ring: #4978B4;
  --gameday-edge-width: 3px;
}

/* ---------------------------------------------------------------------------
   CAMO. Sand page, dark brown cards, lift mechanism - TAN'S STRUCTURE.

   This theme is tan's cousin: the same light-page / dark-card relationship,
   the same brown family, grounds 1.51 apart. Without the band it would be
   indistinguishable in KIND from tan. So its identity is a PATTERN, and a
   pattern has no contrast ratio: the letter ink measures 1.95 against the
   darkest camo patch and 6.30 against the lightest, which is to say it
   measures nothing useful at all. The palette is built so that never matters:

     - the page ground is FLAT sand. Every text-on-ground pair is a colour
       against a colour and every existing rule keeps working unchanged.
     - the camo appears in EXACTLY ONE PLACE: the page body, as a repeating
       tile (test-themes holds it there). The header band is flat
       --header-band, which is why it can carry the palms (--band-art).
     - NOTHING WITH TEXT SITS OVER THE BAND. That is a placement rule, not a
       colour pair, and no suite that measures colour against colour can
       express it. It is asserted by walking the rendered band's box against
       every text node's box - see scripts/audit-band.browser.js - and
       approximated statically in test-themes.

   THE OLIVE IS A CHOSEN COLOUR, NOT A SAMPLED ONE. The lockup contains zero
   pixels between hue 45 and 100; olive is in the description of the jersey,
   not in the artwork. "Sampled from the lockup" would be false here and this
   comment says so. It was handed over as #A3AE62 and it ships as #B0BB72, one
   step lighter, because of one number: #A3AE62 is 4.55 on --raised, and the
   accent wash that .w2w-label and a hovered source row paint over --raised
   lightens the ground under it. On its own wash the olive measured 4.13 at
   alpha 0.06, 4.41 at 0.02 - there is no alpha at which it both passes and is
   visible, and a wash that darkens instead passes at 4.68 while being
   invisible at 1.03 against the ground it is meant to mark. Lightening the
   olive is the only route on which every pair improves: 5.28 on --raised,
   7.22 on --surface, 4.57 on the wash, and 1.01 on the page, which keeps it
   card-only. Anything darker that reads as olive drab fails as text
   everywhere (#6B7A3F is 3.17 on the card), so the accent olive is the only
   olive this theme can hold.

   OLIVE IS CARD-ONLY, EXACTLY LIKE GOLD IN TAN (7.50 / 5.66 / 1.25 there,
   7.22 / 5.28 / 1.01 here). It sits behind the same GOLD_ALLOWED allowlist in
   check-contrast; there is no parallel mechanism for it, on purpose. Any rule
   that puts the accent on the page ground fails there whichever theme is
   active.

   The masthead's cream keyline #F9EEE2 doubling as --text-on-surface is
   deliberate: it is the one light colour the artwork contains.
   --------------------------------------------------------------------------- */
:root[data-theme="camo"] {
  color-scheme: light;

  --bg: #C9B08A;              /* flat sand - the page ground, and ONLY colour */
  --surface: #31251D;         /* cards - 7.12 as a block on the page */
  --surface-raised: #4A3A2C;  /* 1.37 on --surface, 5.21 as a block on the page */

  /* Text on the PAGE. The ink is the card colour, as in tan. */
  --text: #31251D;            /* 7.12 on the ground */
  --text-dim: #534335;        /* 4.53 on the ground */

  /* Text INSIDE a card. The cream is the lockup's keyline. */
  --text-on-surface: #F9EEE2; /* 12.99 on --surface, 9.50 on --raised */
  --dim-on-surface: #B8A48C;  /* 6.18 on --surface, 4.52 on --raised */

  --border: rgba(49, 37, 29, 0.22);           /* 1.45 on the page */
  --border-on-surface: rgba(249, 238, 226, 0.20);  /* 1.82 on --surface */
  --shadow: rgba(49, 37, 29, 0.12);

  /* THE ACCENT IS OLIVE, and it is card-only - see the allowlist. */
  --gold: #B0BB72;            /* CARD-ONLY - 1.01 on the page */
  --gold-ink: #B0BB72;        /* 7.22 on --surface, 5.28 on --raised */
  --gold-soft: #D2DC9A;       /* the lighter olive, on the accent tint */
  --on-gold: #31251D;         /* 7.22 as ink on an olive chip */
  --gold-wash: rgba(176, 187, 114, 0.06);  /* olive stays 4.57 on it over --raised */
  --gold-edge: rgba(176, 187, 114, 0.32);

  /* Semantic, tan's values: they were chosen for a dark brown card and this
     is a dark brown card. Measured again here rather than assumed. */
  --brick: #F5A79E;           /* 7.72 on surface, 5.64 on raised */
  --moss: #9ACB86;            /* 7.97 on surface, 5.83 on raised */
  --brick-tint: rgba(245, 167, 158, 0.14);
  --moss-tint: rgba(154, 203, 134, 0.18);
  --gold-tint: rgba(176, 187, 114, 0.20);
  --neutral-tint: rgba(249, 238, 226, 0.10);

  --skeleton-a: rgba(249, 238, 226, 0.06);
  --skeleton-b: rgba(249, 238, 226, 0.12);
  --live-ring: rgba(176, 187, 114, 0.45);
  --highlight-ring: rgba(176, 187, 114, 0.35);

  /* The playoff cut line, inside the standings card. */
  --cut: #F9EEE2;
  --cut-label: #B8A48C;
  --in-tint: rgba(176, 187, 114, 0.14);

  /* Card ink on light discs, tan's arrangement. */
  --avatar-ink: #31251D;
  --avatar-1: #B0BB72;        /* 7.22 on the ink */
  --avatar-2: #F5A79E;        /* 7.72 */
  --avatar-3: #9ACB86;        /* 7.97 */
  --avatar-4: #E8BE5E;        /* 8.46 */
  --avatar-5: #D9C9A6;        /* 9.10 */

  /* NO DECORATION-ONLY HUE. Nothing in this palette fails as text where it is
     used, so there is nothing for the prohibition scan to govern - the empty
     list in check-contrast is stated, not omitted. */

  /* THE TILES ARE CARDS, as in tan, and wear the keyline like every card. */
  /* THE TILE STRIP AND THE HEADER BAND are the cream: the lockup's dark
     letters were drawn for a light ground and are 1.05 on the surface brown.
     The strip wears the keyline on the pattern like every card. */
  --strip-fill: #F9EEE2;      /* 1.82 on the flat sand */
  --on-strip: #31251D;        /* 12.99 */
  --strip-divider: rgba(49, 37, 29, 0.22);  /* 1.53 on the fill */
  --strip-mark: #31251D;      /* 12.99 */
  --header-band: #F9EEE2;
  --on-header-band: #31251D;  /* 12.99 */
  /* The palms, white as in Classic: 1.144:1 across itself, dE00 7.2 from the
     band, and none of this lockup's 54,800 ink pixels loses contrast on it. */
  --band-art: #FFFFFF;
  --band-art-alpha: 1;
  --band-art-display: block;
  --lastplay-fill: #5C4E41;   /* 1.35 on the gameday fill; its text 7.02 */
  --lastplay-edge: transparent;
  --chip-on: #B0BB72;         /* the olive, card-only, as --gold */
  --on-chip-on: #31251D;      /* 7.22 */
  --chip-on-edge: transparent;
  /* The transaction pills: light fills under the card brown. Status is a
     gold rather than the olive accent, which stays the accent. */
  --tx-added: #9ACB86;        /* ink 7.97 */
  --on-tx-added: #31251D;
  --tx-removed: #F5A79E;      /* 7.72 */
  --on-tx-removed: #31251D;
  --tx-minors: #9DC3E8;       /* 8.07 */
  --on-tx-minors: #31251D;
  --tx-status: #F2C14E;       /* 8.86 */
  --on-tx-status: #31251D;
  --inj-dot-rehab: #9ACB86;   /* the injury status dots: 7.97 on the card */
  --inj-dot-moved: #F2C14E;   /* 8.86 */

  /* THE BACKING IS SOLID HERE, because the page is a pattern. It is the
     RAISED brown, not the card brown, so that a card sitting inside it -
     the injury expander pill, a reference table, a national-tab row - still
     lifts 1.37 off it rather than vanishing into it. Text on it takes the
     on-surface pair: 9.50 and 4.52. */
  --backing: #4A3A2C;
  --on-backing: #F9EEE2;      /* 9.50 on the backing */
  --dim-on-backing: #B8A48C;  /* 4.52 on the backing */
  --backing-border: rgba(249, 238, 226, 0.20);
  --backing-pad: 12px 16px;

  /* THE KEYLINE: 2px in the cream that is also --text-on-surface - the same
     halo the lockup's letters wear. Cards on a pattern have no measurable
     lift, so this is the theme's separation mechanism; check-contrast holds
     it to 3:1 against the card it edges (12.99) and against the backing
     (9.50), since the outside of the line cannot be measured at all. */
  --keyline: 0 0 0 2px #F9EEE2;

  /* CARDS MAY DIFFER FROM THE SURFACE, AND ALTERNATE. Beach paints story
     cards in two pastel tints while its chips, badges and tables stay white;
     the card scopes --surface and --surface-raised to its fill, so every
     rule inside it reads the fill without knowing. Even cards take the -alt
     set. Themes with one card colour repeat --surface here, and the
     alternation is a no-op. The edge is the card's border; the bar is the
     confirmation marker on its left; the pill pair is the Notable pill's
     background and ink, swapped per fill so it never vanishes into a card
     of its own family. Gameday and standings name their own fill and edge. */
  --card-fill: #31251D;
  --card-fill-raised: #4A3A2C;
  --card-fill-alt: #31251D;
  --card-fill-alt-raised: #4A3A2C;
  --card-edge: rgba(249, 238, 226, 0.20);
  --card-edge-alt: rgba(249, 238, 226, 0.20);
  --card-edge-width: 1px;
  --card-bar: #9ACB86;
  --card-bar-alt: #9ACB86;
  --gold-tint-alt: rgba(176, 187, 114, 0.20);
  --gold-soft-alt: #D2DC9A;
  --gameday-fill: #4A3A2C;
  --gameday-fill-raised: #4A3A2C;
  --gameday-edge: #B0BB72;
  --standings-fill: #31251D;
  --standings-edge: rgba(249, 238, 226, 0.20);
  --standings-edge-width: 1px;

  /* INSIDE THE CARD: the 1px line just inside the border, the dividers
     between a card's parts, the ring on the avatar stack and on the subject
     headshot, and the width of the gameday's border. Defaults repeat the
     hairline and the surface, so three themes look exactly as they did;
     beach paints the pink here. */
  --card-inner: transparent;
  --card-rule: rgba(249, 238, 226, 0.20);
  --avatar-ring: #31251D;
  --subject-ring: rgba(249, 238, 226, 0.20);
  --gameday-edge-width: 3px;
}

/* THE WHOLE PAGE GROUND IS THE PATTERN. A 640x640 SEAMLESS tile, generated
   procedurally from the five sampled colours rather than cropped from the
   artwork, which would seam. 320px so the cells match the source's scale at
   2x. NO background-attachment: fixed - WKWebView repaints it badly.

   --bg stays the flat sand behind it: it is what the status bar and the
   worker's offline canvas show, and what any rule reading var(--bg) gets.
   No text is measured against it - textOnGround: false in themes.js makes
   the ground audit fail any text with nothing painted under it. */
:root[data-theme="camo"] body {
  background-image: url("/icons/lfgsd-wire-camo-tile.png");
  background-size: 320px 320px;
  background-repeat: repeat;
}

/* GEOMETRY ONLY CAMO NEEDS. The backing's colour is a token, so the other
   three themes read transparent and lay out exactly as before; the padding
   and margins that turn a backed block into something that reads as a card
   would move their layout, so they are scoped here rather than tokenised. */
:root[data-theme="camo"] .inj-board,
:root[data-theme="camo"] .tx-feed,
:root[data-theme="camo"] .ref-page { padding: 14px 16px; }
/* THE ROSTER'S OWN, 8px at the sides: at 16 the table's name column was 89px
   inside camo's backing and broke "Cronenworth" (91px) mid-word - measured
   in the pane at 375. 8 leaves it 107. */
:root[data-theme="camo"] .ros-page { padding: 14px 8px; }
/* The player page backs like the others; its cards carry their own inset. */
:root[data-theme="camo"] .pl-page { padding: 6px 12px 14px; }
:root[data-theme="camo"] .about { margin-bottom: 12px; }
:root[data-theme="camo"] .site-footer { margin: 0 16px 12px; padding: 16px 20px; }
:root[data-theme="camo"] .filters {
  width: min(100% - 32px, 608px);
  margin: 0 auto 22px;
  padding: 10px 12px;
  border-radius: 12px;
}


/* THE BAND PAINTS NOTHING NOW, IN ANY THEME. It was where the pattern
   appeared when the pattern appeared in one place; the page ground is the
   pattern now, so the band is just more of it, and a cropped band asset at a
   different scale would have read as a box. The element stays: the lockup
   sits in it, and the rule that no text sits over it is asserted by walking
   it. On camo that walk covers every text node against the whole page. */


* { box-sizing: border-box; }

/* ---------------------------------------------------------------------------
   [hidden] MUST ACTUALLY HIDE.
   
   The UA rule is [hidden] { display: none }, and ANY explicit display
   declaration outranks it. .feed sets display: flex, so setting feed.hidden
   did nothing at all - the feed stayed on screen underneath every view route,
   and had done since the injuries view was added. It was only caught by
   looking at the rendered page rather than at the attribute, which reported
   hidden: true the whole time.

   !important because the whole point is to beat a later, more specific display
   on any container that grows one - which is how this happened once already.
--------------------------------------------------------------------------- */
[hidden] { display: none !important; }

/* ---------------------------------------------------------------------------
   WEB-ONLY IS NEVER PAINTED IN THE NATIVE APP (2026-09-17).

   Install, Home Screen and browser copy lives in data-only="web" elements, and
   platform.js stamps <html data-platform="native"> inside the native app before
   the body is parsed. This rule then hides every such element whatever a script
   sets - which is what put "Tap Share, then Add to Home Screen" in the TestFlight
   build: a PWA check unhid it. !important for the same reason as [hidden] above,
   and test-web-only-copy holds that nothing may out-rank it.
--------------------------------------------------------------------------- */
:root[data-platform="native"] [data-only="web"] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: 'Inter', sans-serif;
  -webkit-font-smoothing: antialiased;
}


/* THE HEADER IS A BAND (2026-09-14): full width, in --header-band, edged by
   the page hairline. The colour is per theme because the lockups are:
   Classic's and Camo's letters are dark brown, 1.21:1 and 1.05:1 on their
   surface colour, so their band is the cream they were drawn for; City's is
   its surface. Earlier the same day the header went from 279px to 195px at
   375 (no tagline, tighter padding); the lockup is still 161x128. The top
   padding is not the notch's clearance - the native shell insets the web
   view (contentInset: always in capacitor.config.json). */
.header {
  padding: 10px 16px 12px;
  background: var(--header-band);
  border-bottom: 1px solid var(--border);
  /* The palms (.band-art) draw under everything in the band: isolation makes
     z-index -1 mean "over the band's fill, under its content", and overflow
     keeps them inside it. Relative, never absolute - test-themes holds every
     header rule to normal flow. */
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

/* THE PALMS (2026-09-15). One inline SVG in the band (index.html), drawn
   under everything in it and over its fill - .header isolates, so z-index -1
   means exactly that - bottom-anchored, never wider than 560px so the groves
   flank the lockup on a wide screen too, and taking no taps. Colour and
   strength are the theme's (--band-art, --band-art-alpha); a theme whose band
   cannot carry them sets --band-art-display: none. test-band-art measures
   every ink pixel of every lockup on the worst ground the art makes. */
.band-art {
  position: absolute;
  left: 50%;
  bottom: 0;
  z-index: -1;
  display: var(--band-art-display);
  width: min(100%, 560px);
  height: 100%;
  transform: translateX(-50%);
  color: var(--band-art);
  opacity: var(--band-art-alpha);
  pointer-events: none;
}

/* THE WORDMARK GOES HOME (2026-09-15), as every reader expects a logo to: a
   link AROUND the band - nothing may be put inside it - holding no text of its
   own. goHome (app.js) does the rest. */
.wordmark-home {
  display: block;
  border-radius: 10px;
  color: inherit;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
.wordmark-home:focus-visible { outline: 2px solid var(--on-header-band); outline-offset: 2px; }

/* THE ROW: the menu, the centred stack, and a spacer the menu's width so the
   lockup stays centred. NO SEARCH: the app has none, and a magnifier that
   opened nothing would be a control that lies. Nothing that holds text is
   positioned - test-themes holds the header to normal flow; the one positioned
   element is the palm art, drawn BEHIND everything (.band-art). */
.header-row {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr) 44px;
  align-items: start;
  max-width: 640px;
  margin: 0 auto;
}
.header-mid { min-width: 0; }
.hdr-menu {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--on-header-band);
  cursor: pointer;
}
.hdr-menu svg { width: 24px; height: 24px; }

/* THE MENU'S PANEL, under the band and in flow: it pushes the page down
   rather than drawing over anything. Rows are 44px. */
.hdr-menu-panel {
  padding: 2px 16px 6px;
  background: var(--header-band);
  border-bottom: 1px solid var(--border);
}
.hdr-menu-panel a {
  display: flex;
  align-items: center;
  min-height: 44px;
  max-width: 640px;
  margin: 0 auto;
  color: var(--on-header-band);
  font-size: 15px;
  font-weight: 600;
  text-decoration: none;
}
.hdr-menu-panel a + a { border-top: 1px solid var(--strip-divider); }

/* THE STATUS PILL (2026-09-26): the old badge's box, exactly - same padding,
   border, text size and the 10px under it - in the old badge's slot above
   the lockup, on every page. The header's height was proven by that box
   (test-landing ran clean with it and flaked 2px without it), so the pill
   keeps it whatever the data says: with nothing to say it is
   visibility: hidden, never display: none. Its own opaque fill is the
   ground under its words. Text, not a control: no chevron, no pointer.
   NOT .status-pill: that is the story card's confirmation badge. */
.hdr-status {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: var(--surface);
  border: 1px solid var(--border-on-surface);
  border-radius: 999px;
  margin-bottom: 10px;
}
.hdr-status-text {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--dim-on-surface);
}
.hdr-status[data-state="none"] { visibility: hidden; }
/* The pulsing dot means live; delayed keeps its space and not its claim. */
.hdr-status[data-state="delayed"] .live-dot { visibility: hidden; }

.live-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--gold-ink);
  box-shadow: 0 0 0 0 var(--live-ring);
  animation: pulse 2s infinite;
}

@keyframes pulse {
  0% { box-shadow: 0 0 0 0 var(--live-ring); }
  70% { box-shadow: 0 0 0 6px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}



/* THE BAND WRAPS THE LOCKUP IN EVERY THEME, and paints nothing in any of
   them - camo's pattern moved to the page, and the palms are the header's
   (.band-art), behind it. The h1's old bottom margin lives here so the
   band is exactly the lockup's box plus whatever padding a theme adds, and
   the three themes without one lay out precisely as they did before it
   existed. NOTHING ELSE MAY BE PUT INSIDE IT: the element carries no text so
   that no text can ever sit over a pattern that has no contrast ratio.
   No bottom margin since the tagline went: nothing follows it in the header. */
.masthead-band {
  margin: 0;
}

.header h1 {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 32px;
  font-weight: 800;
  margin: 0;
  letter-spacing: -0.02em;
  color: var(--text);
}

/* .header h1 .accent is gone: "Wire" is part of the logotype image now, so
   there is no text beside it to colour - and gold text would not have been
   available on the page ground anyway. */

/* ---------------------------------------------------------------------------
   The masthead lockup.

   SIZED BY HEIGHT, NEVER BY WIDTH, so it scales with the h1 rather than being
   pinned to a pixel measurement that drifts the moment the heading size or the
   browser's default font changes. width: auto keeps the artwork's own ratio -
   WHICH IS PER THEME: 480x381 for tan, 480x341 beach, 480x218 city, 480x211
   camo. The width and height attributes are set by the masthead script from
   themes.js, where each pair is asserted against the PNG's own IHDR header,
   so the box the browser reserves is the box it ends up using. The markup used
   to carry tan's pair as a constant, which was wrong for every other theme.

   THIS LOCKUP IS TWO LINES WHERE THE OLD ONE WAS ONE, so height is a layout
   question rather than a typographic one. 4em is 161x128 at 375, and it was
   NOT shrunk when the header was cut on 2026-09-14 (279px to 195px, see
   .header): the saving came from the tagline and the padding around the
   lockup, so it still reads as the lockup rather than a small logo.
--------------------------------------------------------------------------- */
/* CENTRED. margin-inline: auto rather than text-align on the parent, because
   the image is a block - text-align would not move it. The live badge is
   centred with it so the masthead reads as one centred stack rather than a
   centred logo under left-aligned furniture; the menu sits beside that stack
   in its own column (.header-row). */
.wordmark {
  height: 4em;
  width: auto;
  display: block;
  max-width: 100%;
  margin-inline: auto;
}

.header { text-align: center; }
.hdr-status { margin-inline: auto; }

/* TWO ROWS WHEN IT MUST, NEVER A SIDEWAYS STRIP (2026-09-15). The theme
   picker is as wide as its longest option, so at 375 the row needs 45-53px
   more than it has, and as a scrolling strip what sat cut off at the edge
   was the picker - on every page, the only sign the app has themes. It
   wraps now, as the reference tiles and the Transactions chips do: a
   control a reader cannot see is one they will not use. The labels keep
   their 13px. The row sits below the gameday card and the tiles, so its
   second line moves nothing above the score. */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 6px 24px 22px;
  max-width: 640px;
  margin: 0 auto;
}
/* THE THEME SWITCHER ON A LINE OF ITS OWN (1.3), so the row is the same
   height whatever it holds and whenever it is filled. It was one line at
   first paint - Home, the Spotted chip still hidden, the <select> 47px wide
   with no options - and two once the feed unhid Spotted and settings.js (a
   module, after parse) gave the select its options and 163px: 41px more,
   about a quarter-second after a deep link had already scrolled its page to
   the title, which the whole page below then missed by 41px. Traced in
   test-landing with layout-shift records. The break (.filters-break, an
   empty element in the markup, so no rule reorders the row) is a flex line
   8px tall - the gap the rows had - so the two lines stand exactly where they
   did at 375 with Spotted shown; the row gap is 0 because the break is the
   gap. Nothing else wraps on the first line: two chips. */
.filters { row-gap: 0; }
.filters-break { flex-basis: 100%; height: 8px; }

.filter-chip {
  font-family: 'Inter', sans-serif;
  font-weight: 500;
  font-size: 13px;
  padding: 8px 15px;
  border-radius: 10px;
  border: 1px solid var(--border-on-surface);
  background: var(--surface);
  color: var(--dim-on-surface);
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
  transition: all 0.15s ease;
}

.filter-chip.active {
  /* A ROLE, NOT THE ACCENT. City marks the active chip with a fireberry edge
     on the card fill rather than a fireberry fill, because a 13px chip is not
     in the large tier and fireberry holds no normal text. The inset shadow
     doubles the 1px border to a visible 2px without moving the layout. */
  border-color: var(--chip-on-edge);
  box-shadow: inset 0 0 0 1px var(--chip-on-edge);
  background: var(--chip-on);
  color: var(--on-chip-on);
  font-weight: 700;
}

/* HOME, NOT A CATEGORY (2026-09-15). Someone who was not the developer could
   not find the way back to the feed: "Pads News" read as one filter of two.
   It is the destination now - a house and the word Home - and its two states
   sit further apart than a filter's: away from home it keeps full ink, full
   weight, a 2px ring and an outlined house, so it never dims into a peer; at
   home it takes the active chip's fill (.filter-chip.active outranks this)
   and its house fills too. */
.home-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 700;
  color: var(--text-on-surface);
  border-color: var(--text-on-surface);
  box-shadow: inset 0 0 0 1px var(--text-on-surface);
}
.home-icon { flex: none; width: 16px; height: 16px; }
.home-chip.active .home-icon path { fill: currentColor; }

/* LATEST PADRES NEWS - the feed's own heading. It sits on the page, so it
   wears the backing: transparent in three themes, a solid block on camo's
   pattern. Shown only with the All filter, like the card and the standings -
   under Spotted the feed is not Padres news. No "See All": the feed is
   directly below, and a link to where the reader already is goes nowhere.
   The width lines it up with the feed's cards (24px in, 640px at most). */
.feed-head {
  width: min(100% - 48px, 592px);
  margin: 2px auto 8px;
  padding: var(--backing-pad);
  background: var(--backing);
  border-radius: 12px;
  box-shadow: var(--keyline);
  color: var(--on-backing);
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.feed {
  padding: 4px 24px 40px;
  max-width: 640px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.card {
  /* THE CARD IS ITS OWN GROUND. --surface and --surface-raised are rebound
     here to the card fill, so every rule inside - avatars' rings, source
     rows, the sticky columns of a box score - reads the fill and never
     knows the theme paints cards differently from chips. */
  --surface: var(--card-fill);
  --surface-raised: var(--card-fill-raised);
  background: var(--surface);
  border: var(--card-edge-width) solid var(--card-edge);
  border-radius: 18px;
  padding: 18px 20px;
  /* The inner line first, then the keyline, then the lift. */
  box-shadow: inset 0 0 0 1px var(--card-inner), var(--keyline), 0 1px 2px var(--shadow);
}

.meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}

.urgency-pill {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 3px 9px;
  border-radius: 6px;
}

.urgency-breaking .urgency-pill { background: var(--brick-tint); color: var(--brick); }
.urgency-notable .urgency-pill { background: var(--gold-tint); color: var(--gold-soft); }
.urgency-minor .urgency-pill { background: var(--neutral-tint); color: var(--dim-on-surface); }

.meta .time {
  font-size: 12px;
  color: var(--dim-on-surface);
}

.card h3 {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 17.5px;
  font-weight: 700;
  color: var(--text-on-surface);
  line-height: 1.35;
  margin: 0 0 6px 0;
  letter-spacing: -0.01em;
}

.card p {
  font-size: 14px;
  color: var(--dim-on-surface);
  line-height: 1.55;
  margin: 0 0 16px 0;
}

/* A story published without a summary - the reproduction guard withheld it
   because restating the fact would have reproduced the source. renderCard omits
   the <p> entirely rather than emitting an empty one, so the 16px that .card p
   contributed below the text has to come from somewhere or the headline sits
   6px from the sources-row border and the card looks clipped. Moving it onto
   the headline reproduces the original spacing exactly, minus the body. */
.card.no-summary h3 {
  margin-bottom: 16px;
}

.sources-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  padding-top: 12px;
  border-top: 1px solid var(--card-rule);
}

.avatar-stack {
  display: flex;
  align-items: center;
}

.avatar {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 2px solid var(--avatar-ring);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: 600;
  /* ITS OWN INK, not the gold fill's. They were the same value in tan by
     coincidence; in a theme whose gold fill takes white ink, white initials on
     a pastel disc would be unreadable. */
  color: var(--avatar-ink);
  margin-left: -7px;
}

.avatar:first-child { margin-left: 0; }

.source-names {
  font-size: 12.5px;
  color: var(--dim-on-surface);
  margin-left: 8px;
}

.source-names strong { color: var(--text-on-surface); font-weight: 600; }

/**
 * THE SOURCE GROUP IS NOW THE CONTROL.
 *
 * The avatar stack and the count already read as one object, so it becomes the
 * button rather than growing a second control beside it. Cards sit on
 * --surface, so gold is legal here, and it is what makes this look pressable
 * next to a row that previously looked like a caption.
 *
 * 44px, per the iOS guideline, and it is the whole width of the row so the
 * target is generous rather than technically compliant.
 */
.src-toggle {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 44px;
  margin-top: 4px;
  padding: 6px 10px;
  background: transparent;
  border: 1px solid var(--gold-edge);
  border-radius: 10px;
  cursor: pointer;
  text-align: left;
  font: inherit;
  -webkit-tap-highlight-color: transparent;
}

.src-toggle:hover,
.src-toggle:focus-visible { border-color: var(--gold); background: var(--gold-wash); }

.src-toggle[aria-expanded="true"] {
  border-color: var(--gold);
  background: var(--gold-wash);
}

.src-count {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--gold-ink);
  white-space: nowrap;
}

/* Pushed to the far edge so the whole row reads as one control with an
   affordance at its end, rather than a label with a decoration after it. */
.src-chevron {
  margin-left: auto;
  width: 7px;
  height: 7px;
  flex: none;
  border-right: 2px solid var(--gold-ink);
  border-bottom: 2px solid var(--gold-ink);
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform 150ms ease;
}

.src-toggle[aria-expanded="true"] .src-chevron {
  transform: rotate(-135deg) translate(-1px, -1px);
}

@media (prefers-reduced-motion: reduce) {
  .src-chevron { transition: none; }
}

/* ---------------------------------------------------------------------------
   THE EXPANDED LIST. One row per source, each a real link.

   Rows are 44px minimum for the same reason the toggle is: this is the screen
   where a reader picks a reporter, and a 28px row is a dart board.
--------------------------------------------------------------------------- */
.src-panel {
  margin-top: 6px;
  border: 1px solid var(--border-on-surface);
  border-radius: 10px;
  overflow: hidden;
}

/* The claim the whole panel is making, in one line. */
.src-caption {
  margin: 0;
  padding: 8px 12px;
  font-size: 12px;
  color: var(--dim-on-surface);
  background: var(--surface-raised);
  border-bottom: 1px solid var(--card-rule);
}

.src-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.src-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  min-height: 44px;
  padding: 8px 12px;
  text-decoration: none;
  color: inherit;
  background: var(--surface-raised);
}

.src-list li + li .src-row { border-top: 1px solid var(--card-rule); }

.src-row:hover,
.src-row:focus-visible { background: var(--gold-wash); }

/* A source with no link is part of the record but is not a control - it must
   not offer a pointer to somewhere it cannot go. */
.src-row-dead { cursor: default; opacity: 0.75; }

.src-line { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; min-width: 0; }

.src-who { font-size: 13.5px; font-weight: 600; color: var(--text-on-surface); }
.src-outlet { font-size: 12px; color: var(--dim-on-surface); }
/* THE VALUE THIS FEATURE EXISTS FOR, so it is not the faintest thing on the
   row. An absolute clock time is what shows who was first; "2h ago" cannot. */
.src-when {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-on-surface);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* OUR pickup time, not a publication time - said in our own words and set back
   from the stated ones so the two cannot be mistaken for each other. */
.src-when-ours {
  font-weight: 400;
  font-style: italic;
  /* The stated time's ink (1.3): at 12.5px the list has one colour. The words
     "picked up" and the italic say whose clock it is. */
}

/* How long after the first report this one landed. */
.src-lag {
  font-size: 12px;
  /* The words say it; at 12px the list's ink is the dim one (1.3: it was gold
     beside the dim outlet and caption - two colours on one size). */
  color: var(--dim-on-surface);
  white-space: nowrap;
}

.src-tags { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }

.src-role {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--dim-on-surface);
  white-space: nowrap;
}

/* ONE COLOUR FOR EVERY ROLE (1.3). "First report", "Also reporting" and
   "Confirmed it" were gold, grey and green at one size; the words carry the
   meaning, and each size has one colour. */

/* The row the "Read at" button opens. Marked so the list and the button can
   never silently disagree about which source is the default. */
.src-primary {
  font-size: 10.5px;
  font-weight: 600;
  color: var(--dim-on-surface);
  border: 1px solid var(--border-on-surface);
  border-radius: 6px;
  padding: 2px 6px;
  white-space: nowrap;
}

.empty {
  color: var(--text-dim);
  font-size: 13px;
  padding: 20px 0;
}

/* --- Loading skeletons --- */
.card.skeleton { pointer-events: none; }

.sk-line {
  background: linear-gradient(90deg, var(--skeleton-a) 25%, var(--skeleton-b) 37%, var(--skeleton-a) 63%);
  background-size: 400% 100%;
  animation: shimmer 1.4s ease infinite;
  border-radius: 6px;
}

@keyframes shimmer {
  0% { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

.sk-pill { width: 72px; height: 18px; margin-bottom: 12px; }
.sk-title { width: 85%; height: 18px; margin-bottom: 10px; }
.sk-body { width: 100%; height: 12px; margin-bottom: 7px; }
.sk-body.short { width: 55%; }

/* --- Empty / error states --- */
/* The feed's own two states - "Couldn't load the feed" and "Nothing here
   yet" - on the same footing as the pages': never walked before either. */
.state {
  text-align: center;
  padding: 56px 24px;
  background: var(--backing);
  border-radius: 12px;
  box-shadow: var(--keyline);
}

.state-title {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 16px;
  font-weight: 700;
  color: var(--text);
  margin: 0 0 6px 0;
}

.state-sub {
  font-size: 14px;
  color: var(--text-dim);
  line-height: 1.5;
  margin: 0 auto;
  max-width: 300px;
}

.retry {
  margin-top: 18px;
  font-family: 'Inter', sans-serif;
  font-size: 13.5px;
  font-weight: 600;
  padding: 9px 20px;
  border-radius: 10px;
  border: none;
  background: var(--gold);
  color: var(--on-gold);
  cursor: pointer;
}

.card { transition: transform 0.12s ease, border-color 0.12s ease; }
.card:active { transform: scale(0.995); }

/* Deep-link target from a notification tap - brief gold pulse so the
   story you were notified about is obvious when the app opens. */
.card.highlight {
  border-color: var(--gold-ink);
  box-shadow: 0 0 0 3px var(--highlight-ring);
}

/* --- Attribution / link-out ---
   Visually prominent on purpose. This app states the fact and sends you
   to the reporter; if the link reads as a footnote, the product is
   pretending to be something it isn't. */
.read-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.read-source {
  font-size: 13px;
  font-weight: 600;
  color: var(--gold-ink);
}

.reported-by {
  font-size: 11.5px;
  color: var(--dim-on-surface);
}

/* THE MERCH LINE (dark; see merchLine in app.js): the card's small print,
   in the read row after the credit, the dim ink and nothing else - no fill,
   no gold, no image. It is a link and says so in its own words. */
.merch-link {
  flex-basis: 100%;
  font-size: 11.5px;
  color: var(--dim-on-surface);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--border-on-surface);
}

/* --- Confirmation status ---
   Deliberately quieter than the urgency pill. This is a qualifier on the
   headline, not a competing signal - if it shouted, every card would have
   two things demanding attention and neither would land. */
.status-pill {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  padding: 3px 8px;
  border-radius: 6px;
}

.status-pill.tentative {
  background: var(--neutral-tint);
  color: var(--dim-on-surface);
  border: 1px solid var(--border);
}

.status-pill.confirmed {
  background: var(--moss-tint);
  color: var(--moss);
}

/* A confirmed story gets a subtle green edge - scannable at a glance
   when you're thumbing the feed without reading pills. */
.card.is-confirmed {
  /* The confirmation bar. --card-bar is --moss in three themes; beach makes
     it the OTHER family's brand hue, so a confirmed card carries both. */
  border-left: 3px solid var(--card-bar);
}

/* NO ALTERNATION. For one build beach's even cards rebound --surface,
   --surface-raised, --card-edge, --card-bar, --gold-tint and --gold-soft to
   their -alt tokens by nth-child on the feed. Every card is one fill now,
   in every theme, so the rule is gone; the -alt tokens stay defined and
   equal to their base, and check-contrast and the ground audit still derive
   a second palette automatically for any theme whose --card-fill-alt ever
   differs from --card-fill. Restoring alternation is that rule back:
     .feed > .card:nth-child(even) { --surface: var(--card-fill-alt); ... } */

/* --- Fan pulse ---
   A quiet strip above the feed. It's ambient context, not news, so it
   reads lighter than the cards - if it competed with them visually,
   it would pull attention away from the thing people came for. */
.pulse {
  max-width: 640px;
  margin: 0 auto;
  padding: 0 24px 18px;
}

.pulse-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 9px;
}

.pulse-title {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.pulse-when { font-size: 11px; color: var(--text-dim); opacity: 0.7; }

.pulse-chips { display: flex; flex-wrap: wrap; gap: 7px; }

.pulse-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 12.5px;
  font-weight: 500;
  padding: 5px 11px;
  border-radius: 8px;
  background: var(--neutral-tint);
  border: 1px solid var(--border);
  color: var(--text);
}

/* Sentiment as a left edge - present but not shouting */
.pulse-chip.sent-positive { border-left: 2px solid var(--moss); }
.pulse-chip.sent-negative { border-left: 2px solid var(--brick); }
.pulse-chip.sent-mixed    { border-left: 2px solid var(--gold-ink); }

.pulse-delta { font-size: 10.5px; font-weight: 600; }
/* The pulse strip sits on the PAGE, so gold is not available here - it is
   1.73 on white. The delta keeps its meaning from the arrow and the sign. */
.pulse-delta.up { color: var(--text); }
.pulse-delta.down { color: var(--text-dim); }

/* ---------------------------------------------------------------------------
   Affiliation disclaimer.

   PROMINENCE IS THE POINT. The obvious move is 11px in --text-dim tucked under
   the feed, and that would be worth less than nothing: a disclaimer nobody can
   read does less work than one they can. So this uses body text size and the
   primary text colour, and the full wording sits open rather than behind an
   accordion.

   Measured against --bg #16110C: --text #F5F1EA is 16.7:1 and --text-dim
   #9C8D77 is 5.8:1. Both clear WCAG AA for normal text (4.5:1); --text is used
   anyway, because clearing the bar and being easy to read are not the same
   thing. Only the heading label and the rule above it use the dim tone.
--------------------------------------------------------------------------- */

.site-footer {
  padding: 24px 20px 8px;
  text-align: center;
}

.disclaimer-link {
  /* 15px rather than the 14px used inside cards: this is the summary a reader
     scanning for it should catch, not supporting detail. */
  font-size: 15px;
  line-height: 1.5;
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--text-dim);
  /* Comfortably past the 44px minimum tap target on its own line. */
  display: inline-block;
  padding: 10px 4px;
}

/* Footer is on the page ground; gold would be 1.73 here. */
/* The pressed state reads the backing pair too. The audit applies this rule
   unconditionally - pseudo-classes are matched by ignoring the condition -
   and on camo the page dim measured 1.15 on the backing while pressed. */
.disclaimer-link:active { color: var(--dim-on-backing); }

.about {
  max-width: 640px;
  margin: 0 auto;
  padding: 8px 20px 40px;
  /* So the footer link does not land the heading flush against the top edge. */
  scroll-margin-top: 16px;
}

.about h2 {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 0 0 14px 0;
  padding-top: 20px;
  border-top: 1px solid var(--border);
}

.about p {
  font-size: 15px;
  line-height: 1.65;
  color: var(--text);
  margin: 0 0 14px 0;
}

.about p:last-child { margin-bottom: 0; }

/* Native smooth scroll for the footer link - no JavaScript involved. */
html { scroll-behavior: smooth; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

/* ---------------------------------------------------------------------------
   Notification settings and diagnostics.

   The diagnostics are ALWAYS VISIBLE, not behind a debug flag. Testing is an
   iPhone driven from Windows: no Safari remote inspector, no console. Without
   these lines "no notification arrived" covers five separate failures that are
   indistinguishable from the outside.

   Same contrast discipline as the disclaimer: body copy at --text (16.7:1 on
   --bg), only labels at --text-dim (5.8:1). Both clear WCAG AA.
--------------------------------------------------------------------------- */

/* .about p sets the page text colour and is MORE SPECIFIC than a bare
   .push-hint, so this has to match the ancestor too - the hint paints its own
   --raised background and is a card, not page copy. */
.about .push-hint,
.push-hint {
  font-size: 15px;
  line-height: 1.6;
  /* It paints its own --raised background, so it is a card. */
  color: var(--text-on-surface);
  background: var(--surface-raised);
  border: 1px solid var(--border-on-surface);
  border-radius: 12px;
  padding: 12px 14px;
  margin: 0 0 16px 0;
}

.toggle-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
}

.toggle-text { display: flex; flex-direction: column; gap: 3px; }
.toggle-label { font-size: 15px; color: var(--text); }
.toggle-hint { font-size: 13px; color: var(--text-dim); line-height: 1.4; }

/* Native checkbox, scaled to a comfortable tap target rather than a custom
   control that would need its own accessibility work. */
.toggle-row input[type="checkbox"] {
  flex: none;
  width: 24px;
  height: 24px;
  accent-color: var(--gold-ink);
  cursor: pointer;
}

.push-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 18px 0 6px;
}

.push-btn {
  font-family: inherit;
  font-size: 15px;
  color: var(--on-gold);
  background: var(--gold);
  border: none;
  border-radius: 10px;
  padding: 12px 16px;   /* 44px+ tall */
  cursor: pointer;
}
.push-btn:disabled { opacity: 0.6; cursor: default; }

.push-btn-secondary {
  color: var(--text-on-surface);
  background: var(--surface-raised);
  border: 1px solid var(--border-on-surface);
}

.push-btn-quiet {
  /* Inside the diagnostics block, which is backed on camo: the page dim
     measured 1.15 there. The backing pair is the page pair everywhere else. */
  color: var(--dim-on-backing);
  background: transparent;
  border: 1px solid var(--backing-border);
  font-size: 14px;
  padding: 9px 13px;
  margin-top: 10px;
}

.diag-heading {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 22px 0 8px 0;
}

/* Monospace so a token tail and an HTTP status are readable at a glance, and
   so the columns line up when read aloud over the phone. */
.diag {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  line-height: 1.5;
  background: var(--surface);
  border: 1px solid var(--border-on-surface);
  border-radius: 12px;
  padding: 12px 14px;
}

.diag-line {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 3px 0;
  flex-wrap: wrap;
}

.diag-key { color: var(--dim-on-surface); }
.diag-val { color: var(--text-on-surface); text-align: right; word-break: break-word; }
.diag-ok { color: var(--moss); }
.diag-bad { color: var(--brick); }

.diag-note {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  color: var(--text-on-surface);
  line-height: 1.5;
}

/* ---------------------------------------------------------------------------
   Gameday card.

   Sits above the feed on game days. NO CLUB LOGOS OR MARKS - team names as
   text, same as everywhere else in the app.

   Contrast against --bg #16110C: --text is 16.7:1, --text-dim 5.8:1, --gold
   #FFC425 is 11.4:1. All clear WCAG AA for normal text.
--------------------------------------------------------------------------- */

#gameday:empty { display: none; }

/* ROOM UNDER THE BAND (2026-09-15): the card sat flush against it. The host
   takes the gap, and the reserved space lives inside the host, so the reserve
   and the card it stands for are spaced alike and nothing shifts. The menu's
   panel opens between the band and this, attached to the band. */
#gameday { margin-top: 14px; }
/* ...AND THE SAME ROOM WITHOUT THE CARD. Hidden (Spotted, a reference page)
   or empty (no game, or no answer), the host takes its gap with it, so the
   tiles - then the first thing under the band - take the same 14px. Only the
   reserve's script sits between the two (test-home). With the card showing
   this does not apply, and nothing moves. */
#gameday[hidden] ~ .reference-tiles, #gameday:empty ~ .reference-tiles { margin-top: 14px; }

/* THE RESERVED SPACE (index.html, after #gameday): the card's shape and fill,
   and nothing else - no edge, no text, no motion. It stands for "the card goes
   here", never for what the card said last time. Same margins as .gameday, so
   the space it takes is the space the card will take. */
.gd-reserve {
  margin: 0 16px 18px;
  border-radius: 18px;
  background: var(--gameday-fill);
}

/* PINNED, AND SAYING SO. Gold is 1.73 against the white page, so a hairline
   would simply not be there - this is decorative rather than a text pair, but
   decorative still has to be visible. 3px is what carries it at arm's length
   on a phone; at 1px the measured edge is indistinguishable from the page. */
.gameday {
  /* Its own fill and edge. Nested blocks read the fill's raised. */
  --surface-raised: var(--gameday-fill-raised);
  background: var(--gameday-fill);
  border: var(--gameday-edge-width) solid var(--gameday-edge);
  box-shadow: inset 0 0 0 1px var(--card-inner), var(--keyline);
  border-radius: 18px;
  padding: 16px 18px;
  margin: 0 16px 18px;
}

/* The off-day form is a LINE, not a card - it answers "when next" without
   competing with the news underneath it. */
.gameday-off {
  /* The off-day line sits on the page. Transparent in three themes; camo
     paints its backing under it and the line takes the backing ink. */
  background: var(--backing);
  padding: var(--backing-pad);
  border-radius: 12px;
  box-shadow: var(--keyline);
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  padding: 4px 2px 14px;
  margin: 0 16px 16px;
}

/**
 * THE STATE LABEL: one line, centred, above everything.
 *
 * It reads resolveState's output and has no state of its own - see
 * stateLabel() in app.js. Gold when live, because live is the one state the
 * reader should be able to spot without reading; dim on the card ground
 * otherwise, so FINAL and GAME DAY sit back and let the score lead.
 */
.gd-statelabel {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-align: center;
  color: var(--dim-on-surface);
  margin-bottom: 10px;
}

.gd-statelabel-live { color: var(--gold-ink); }

/* The schedule's live row. Gold, on the schedule table's card ground, and with
   a dot so it reads as live at a glance rather than as another result. */
.sch-live {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 700;
  color: var(--gold-ink);
  white-space: nowrap;
}

.sch-livedot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--gold-ink);
  flex: none;
}

.gd-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
}

.gd-matchup {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 16px;
  font-weight: 700;
  color: var(--text-on-surface);
  line-height: 1.3;
}

/* "Game 2 of 2" - so a doubleheader choice is never silent. */
.gd-tag {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--on-gold);
  background: var(--gold);
  border-radius: 999px;
  padding: 3px 8px;
}

.gd-when { font-size: 15px; color: var(--text-on-surface); margin-top: 6px; }
.gd-records { font-size: 13px; color: var(--dim-on-surface); margin-top: 3px; }

/* WHERE TO WATCH (broadcastLine). One line by construction: a fixed-height
   wrapping row that clips, so the TV names always show and a radio item that
   would wrap lands on a second row nobody sees. The names are the card's ink,
   the words around them the dim ink. */
.gd-tv {
  display: flex;
  flex-wrap: wrap;
  column-gap: 5px;
  height: 20px;
  overflow: hidden;
  margin-top: 4px;
  font-size: 14px;
  line-height: 20px;
  color: var(--text-on-surface);
}
.gd-tv-item { white-space: nowrap; }
.gd-tv-main { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
/* 13px (1.3): the key is the dim words, and on this card the dim ink is
   13px - one colour per size. At the line's 14px it was a second colour
   for the names' size. */
.gd-tv-k { color: var(--dim-on-surface); font-size: 13px; }
/* On an off day the line sits on the page backing, like .gd-next beside it. */
.gameday-off .gd-tv { color: var(--on-backing); }
.gameday-off .gd-tv-k { color: var(--dim-on-backing); }

/* ---------------------------------------------------------------------------
   THE GAMEDAY CARD, 2026-09-14 LAYOUT (renderGameday / renderUpcoming).

   The inner classes are the ones the card always had - gd-status, gd-runs,
   gd-diamond, gd-dot, gd-pitching, gd-atbat, gd-lastplay, gd-grid - so the
   audits and tests that know them still find them; these gd2 rules lay them
   out. Every colour is an existing on-surface token: team names are the
   app's type in the theme's ink, and nothing here is a club colour.
--------------------------------------------------------------------------- */
/* THE SCOREBOARD STAGE: the state row, the venue and the three columns. It
   carried a ballpark SVG behind them from 2026-09-14 to 2026-09-15; at the
   strength that kept it off the labels it read as noise rather than a
   ballpark, so the card is the theme's plain fill again. The stage stays
   positioned because the bases inside it are placed against it. */
.gameday .gd2-stage { position: relative; }

/* THE INNING ARROW, gold, above the inning: up in the top half, down in the
   bottom (liveInning in app.js). A mark, held to 3:1 against the worst pixel
   under it. */
.gameday .gd2-arrow { display: block; width: 12px; height: 7px; color: var(--gold-ink); }

.gd2-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 24px; }
.gameday .gd2-top .gd-statelabel {
  flex: none;
  margin: 0;
  padding: 2px 9px;
  border: 1px solid currentColor;
  border-radius: 999px;
  line-height: 18px;
  text-align: left;
}
.gd2-top .gd-tag { flex: none; }
.gameday .gd2-top .gd-tv { flex: 1 1 auto; min-width: 0; margin-top: 0; justify-content: flex-end; }
.gd2-venue {
  margin-top: 6px;
  text-align: center;
  font-size: 13px;
  font-variant: all-small-caps;
  letter-spacing: 0.06em;
  color: var(--dim-on-surface);
}
.gameday .gd2-weather { margin-top: 2px; text-align: center; }
/* THE ROUND (1.3): "Wild Card Series · Game 1 of 3", postseason only, under
   the venue and in the venue's type exactly - 13px in the same dim ink - so
   the card gains no size and no second colour for one. */
.gd2-round {
  margin-top: 2px;
  text-align: center;
  font-size: 13px;
  font-variant: all-small-caps;
  letter-spacing: 0.06em;
  color: var(--dim-on-surface);
}

.gameday .gd2-board {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 8px;
  margin-top: 10px;
}
.gd2-side { display: flex; flex-direction: column; align-items: center; text-align: center; min-width: 0; }
.gd2-city { font-size: 13px; line-height: 1.25; color: var(--dim-on-surface); }
.gd2-nick {
  max-width: 100%;
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 19px;
  font-weight: 800;
  line-height: 1.2;
  color: var(--text-on-surface);
  overflow-wrap: anywhere;
}
/* THE SCORE IS THE LARGEST TYPE ON THE CARD (test-gameday-card holds it so). */
.gameday .gd2-side .gd-runs { margin-top: 2px; font-size: 52px; font-weight: 800; line-height: 1.05; letter-spacing: -0.01em; text-align: center; }
.gd2-rec { margin-top: 1px; font-size: 13px; color: var(--dim-on-surface); font-variant-numeric: tabular-nums; }
/* THE FINAL: the winner by WEIGHT, never by colour (2026-09-26). Both scores
   and both nicknames keep the one ink their size has; the loser's are lighter. */
.gd2-final .gd2-side .gd-runs { font-weight: 500; }
.gd2-final .gd2-side .gd2-nick { font-weight: 600; }
.gd2-final .gd2-won .gd-runs, .gd2-final .gd2-won .gd2-nick { font-weight: 800; }
/* The verdict: a separate, smaller label under the score, in the card's ink. */
.gd-recaplist .w2w-head { font-size: 13px; } /* the recap's wire at the dim 13px, not beside the 14px names in ink */
.gd-recapverdict {
  margin: 8px 0 2px;
  text-align: center;
  font-size: 14px;
  font-weight: 700;
  color: var(--text-on-surface);
}

.gd2-mid { display: flex; flex-direction: column; align-items: center; gap: 4px; min-width: 0; text-align: center; }
.gameday .gd2-mid .gd-status { margin-top: 0; font-size: 15px; font-weight: 800; }
.gameday .gd2-mid .gd-when { margin-top: 0; font-weight: 700; font-size: 18px; white-space: nowrap; }
.gd2-outs { font-size: 13px; color: var(--dim-on-surface); }
.gd2-at { font-size: 13px; font-style: italic; color: var(--dim-on-surface); }
/* THE BASES ABOVE THE COUNT (2026-09-14), not beside it: one column, the
   diamond and then three labelled rows - three balls, two strikes, two outs. */
.gd2-field { display: flex; flex-direction: column; align-items: center; gap: 8px; margin-top: 4px; }
.gd2-field .gd-diamond { width: 46px; height: 34px; }
.gd2-field .w2w-base { width: 14px; height: 14px; }
.gd2-field .b2 { left: 16px; top: 3px; }
.gd2-field .b1 { left: 29px; right: auto; top: 17px; }
.gd2-field .b3 { left: 3px; top: 17px; }
.gd2-field .gd-countbox { gap: 5px; }
.gd2-field .gd-dot { width: 10px; height: 10px; }
.gd2-field .gd-dotlabel { width: 10px; font-size: 10px; }

.gd2-rule { margin: 12px 0; border: 0; border-top: 1px solid var(--card-rule); }

/* The live block is no longer a wrapping row with the diamond in it - the
   diamond moved to the middle column - so it lays out as a column. */
.gameday .gd-livedetail.gd2-live { display: block; margin-top: 0; padding-top: 0; border-top: 0; }

.gameday .gd2-duo { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 0; }
.gameday .gd2-cell { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; min-width: 0; font-size: 14px; }
.gd2-person { display: flex; align-items: center; gap: 8px; min-width: 0; }
.gd2-person .gd-shot { margin-right: 0; flex: none; }
.gd2-name { font-weight: 700; font-size: 14px; line-height: 1.25; color: var(--text-on-surface); overflow-wrap: anywhere; }
.gd2-line { font-size: 12px; color: var(--dim-on-surface); font-variant-numeric: tabular-nums; }
.gameday .gd2-cell .gd-pitchline { margin-left: 0; font-size: 12px; }
/* Workload over results, each on its own line and never broken inside - see
   pitchingLines in app.js for the widths. */
.gd2-pitchgroup { display: block; white-space: nowrap; }
/* The At Bat line the same way: today's line over the season average - see
   liveDuo in app.js for the widths. */
.gd2-linegroup { display: block; white-space: nowrap; }
.gd2-ondeck { margin-top: 10px; }

/* LAST PLAY IS AN INSET BOX (2026-09-14) in its own fill, a little lighter
   than the card's: 1.34 on it in Classic and City, 1.35 in Camo; beach's
   white box is found by its hairline edge. Its label takes the full text
   colour - the dim one is 4.53 and 4.52 on City's and Camo's card and would
   drop under 4.5 on anything lighter. check-contrast holds every pair. */
.gd2-lastplay {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  margin-top: 12px;
  padding: 10px 12px;
  border: 1px solid var(--lastplay-edge);
  border-radius: 12px;
  background: var(--lastplay-fill);
  color: var(--text-on-surface);
  text-decoration: none;
}
.gd2-lastplay .gd-label { flex: none; margin: 0; color: var(--text-on-surface); }
.gd2-playtext { flex: 1; min-width: 0; font-size: 14px; color: var(--text-on-surface); }
.gd2-chev {
  flex: none;
  width: 8px;
  height: 8px;
  margin-right: 4px;
  border-top: 2px solid currentColor;
  border-right: 2px solid currentColor;
  transform: rotate(45deg);
}

.gd2-actions { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 8px; margin-top: 14px; }
.gd2-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 6px;
  border: 1px solid var(--border-on-surface);
  border-radius: 10px;
  background: var(--surface-raised);
  color: var(--text-on-surface);
  font-family: 'Plus Jakarta Sans', sans-serif;
  /* 14px, the card's size for text in its full ink (2026-09-26): at 13px
     these sat beside the dim city and record, two colours for one size. */
  font-size: 14px;
  font-weight: 700;
  text-align: center;
  text-decoration: none;
}

.gd-probs { margin-top: 10px; display: flex; flex-direction: column; gap: 4px; }
/* gap, NOT a space in the markup. This became a flex container when the
   headshot went in, and flex turns a bare text node into an anonymous item and
   eats the whitespace around it - which is how "San Diego PadresMichael King"
   happened with the space still present in the source. */
.gd-prob {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 14px;
  color: var(--text-on-surface);
}
.gd-prob-name { font-weight: 600; color: var(--text-on-surface); }
.gd-prob-team { color: var(--dim-on-surface); }
.gd-era { color: var(--dim-on-surface); }

.gd-meta { font-size: 13px; color: var(--dim-on-surface); margin-top: 8px; }

.gd-score {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 2px 12px;
  margin-top: 10px;
  align-items: baseline;
}
.gd-team { font-size: 15px; color: var(--text-on-surface); }
.gd-runs {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 22px;
  font-weight: 700;
  color: var(--text-on-surface);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.gd-status {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--dim-on-surface);
  margin-top: 8px;
}
.gd-live { color: var(--gold-ink); }
.gd-off { color: var(--brick); }

.gd-lineup { margin-top: 10px; }
/* 15px (1.3): gold says it opens, and 15px is a size nothing else on the
   card uses. At 13px it was a second colour for the venue's dim size. */
.gd-lineup summary {
  font-size: 15px;
  color: var(--gold-ink);
  cursor: pointer;
}
.gd-lineup ol {
  margin: 8px 0 0 0;
  padding-left: 22px;
  font-size: 14px;
  color: var(--text-on-surface);
  line-height: 1.6;
}
.gd-pos { color: var(--dim-on-surface); font-size: 12px; }

/* ONE ELEMENT, TWO GROUNDS - which no single token can serve.

   .gd-next appears inside the card on final and postponed, and on the
   transparent .gameday-off line on an off day. It was --text-on-surface, which
   measured 1.14 on the white page; switching it to --text just moved the
   failure to 1.32 inside the card. Both readings came from the forced-state
   audit, and neither state was reachable from the other, which is precisely
   why one token looked correct for as long as only one state was ever walked.

   So it takes the card scale by default and the page scale where the card is
   not there. .gameday-off carries both classes, so this needs the extra
   specificity rather than source order. */
.gd-next { font-size: 14px; color: var(--text-on-surface); }
.gameday-off .gd-next { color: var(--on-backing); }

/* Only ever rendered when the data is NOT fresh, so its presence is the
   signal. A stale score shown as current is the failure this prevents. */
.gd-stale {
  display: block;
  margin-top: 10px;
  font-size: 12px;
  color: var(--brick);
}
/* ON THE CARD, THE 12px INK (2026-09-26): 12px there is the digits', and a
   size has one colour. The words - "as of 20 min ago · MLB unreachable" -
   carry the warning; brick beside the digits carried it at the rule's cost. */
.gameday:not(.gameday-off) .gd-stale { color: var(--text-on-surface); }

/* ---------------------------------------------------------------------------
   What to watch, and the live detail.

   The watch block gets a faint gold wash and a gold label because it is the
   lead, not a footnote - it is the one thing here that no other app can build.
   Everything else on the card is public data anyone can print.
--------------------------------------------------------------------------- */

.gd-label {
  display: block;
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  /* Most .gd-labels are inside a card - the gameday, the standings, the
     recap - so this is the card scale. */
  color: var(--dim-on-surface);
  margin-bottom: 4px;
}
/* THE SAME LABEL ON THE OTHER GROUND. The injury board and the transaction
   feed paint no background of their own, so their headings sit on bare page
   and need the page scale - on the card scale they measured 1.87.

   AFTER .gd-label, not before. Both are single-class selectors, so specificity
   ties and source order decides; the first version of this rule was written
   above and silently lost. */
.gd-label-page { color: var(--dim-on-backing); }


.w2w {
  margin-top: 14px;
  padding: 12px 14px;
  border-radius: 12px;
  /* Faint wash rather than a solid fill - it should read as lifted, not as a
     separate card fighting the one it sits inside. */
  background: var(--gold-wash);
  border: 1px solid var(--gold-edge);
}

.w2w-label { color: var(--gold-ink); }

.w2w-item + .w2w-item {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.w2w-who {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 14px;
  font-weight: 700;
  color: var(--text-on-surface);
}

/* The generated one-liner - the thing Yahoo cannot print. */
.w2w-line {
  font-size: 15px;
  line-height: 1.45;
  color: var(--text);
  margin-top: 3px;
}

/* ---------------------------------------------------------------------------
   Tonight's line, directly under the storyline that made him worth watching.

   Deliberately styled as a live readout and not as more prose: it sits on the
   gold wash with a pulsing dot, so the eye reads "this is happening now"
   against the past-tense thread above it. That adjacency IS the feature - the
   reporting and the game in one glance.
--------------------------------------------------------------------------- */

.w2w-live {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-top: 6px;
  padding: 5px 9px;
  border-radius: 8px;
  background: var(--gold-wash);
  border: 1px solid var(--gold-edge);
  /* Only as wide as the numbers - a full-width bar would read as a banner. */
  align-self: flex-start;
  width: fit-content;
}

/* --gold-soft, NOT --gold-ink. This band paints --gold-wash over the RAISED
   ground rather than over --surface, and brand gold on that composite measured
   4.17 - passing on the card it was checked against and failing on the one it
   actually renders on. --gold-soft exists for exactly this ground. */
.w2w-liveline {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 13px;
  font-weight: 700;
  color: var(--gold-soft);
  font-variant-numeric: tabular-nums;
}

/* Same dot and the same keyframe as the header's, so "live" looks like one
   idea in both places rather than two that happen to resemble each other. */
.gd-livedot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--gold-ink);
  box-shadow: 0 0 0 0 var(--live-ring);
  flex: none;
  animation: pulse 2s infinite;
}

/* A pulsing dot is decoration; nobody should have to sit through it. */
@media (prefers-reduced-motion: reduce) {
  .gd-livedot, .live-dot { animation: none; }
}

.w2w-steps {
  list-style: none;
  margin: 8px 0 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.w2w-steps li {
  display: grid;
  grid-template-columns: 52px 1fr;
  gap: 8px;
  font-size: 13px;
  line-height: 1.4;
}

/* A DATE IS ONE TOKEN to a reader. "Aug 31" broke across two lines on the
   first From the Wire row and not the second, because that row's headline was
   long enough to squeeze it - so two identical columns disagreed. */
.w2w-date {
  color: var(--dim-on-surface);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.w2w-head { color: var(--dim-on-surface); }
/* The newest step is where the thread has got to, so it reads at full strength. */
.w2w-steps li:last-child .w2w-head { color: var(--text); }

.w2w-single { font-size: 14px; color: var(--text-on-surface); margin-top: 4px; }

/* --- live detail: diamond, count, at-bat --- */

.gd-livedetail {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--card-rule);
  flex-wrap: wrap;
}

/* Three squares rotated into a diamond. No images anywhere in this app. */
.gd-diamond {
  position: relative;
  width: 42px;
  height: 42px;
  flex: none;
}
.w2w-base {
  position: absolute;
  width: 13px;
  height: 13px;
  transform: rotate(45deg);
  border: 1px solid var(--dim-on-surface);
  background: transparent;
}
.w2w-base.on { background: var(--gold-ink); border-color: var(--gold-ink); }
.b1 { right: 0; top: 14px; }
.b2 { left: 14px; top: 0; }
.b3 { left: 0; top: 14px; }

.gd-countbox { display: flex; flex-direction: column; gap: 4px; justify-content: center; }
.gd-count {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 20px;
  font-weight: 700;
  color: var(--text-on-surface);
  font-variant-numeric: tabular-nums;
}
.gd-outs { font-size: 12px; color: var(--dim-on-surface); }

/* ---------------------------------------------------------------------------
   Balls, strikes and outs as dots.

   Three rows of filled and hollow dots read faster than "3-2 · 2 outs",
   because the count has a SHAPE - two of three, one of two - and a shape is
   taken in without being read. Empty slots stay visible as outlines so the
   denominator is always there: two filled dots mean nothing if you cannot see
   that there were three.

   Colour is not doing the work alone. Balls, strikes and outs sit on their own
   rows in a fixed order, so they are told apart by position as well as hue.
--------------------------------------------------------------------------- */

.gd-dots { display: flex; gap: 4px; align-items: center; }

/* One character, so the rows are never guessed at. */
.gd-dotlabel {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 9px;
  font-weight: 700;
  color: var(--dim-on-surface);
  width: 8px;
  margin-right: 1px;
}
.gd-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  border: 1.5px solid var(--dim-on-surface);
  background: transparent;
  flex: none;
}
.gd-dot.on { border-color: transparent; }
.gd-ball.on { background: var(--moss); }
.gd-strike.on { background: var(--gold-ink); }
.gd-out.on { background: var(--brick); }

/* Between innings there is no count to draw. Said in words rather than left
   as three rows of empty circles, which would read as 0-0 with nobody out. */
.gd-between {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim-on-surface);
}

.gd-atbat { font-size: 14px; color: var(--text-on-surface); }

/* --- the live pitching line, the last play, and who is due up --- */

.gd-pitching, .gd-lastplay, .gd-due {
  flex-basis: 100%;
  font-size: 14px;
  color: var(--text);
}

/* NO COLOUR AT ALL was declared here, so it inherited body's page ink onto a
   card: #3F2E1E on #54402C, measured 1.32. This is the "Walker Buehler"
   line, and the absence of a declaration is exactly why reading the token
   block could never have found it. */
.gd-pitchername { font-weight: 600; color: var(--text-on-surface); }
.gd-pitchline {
  margin-left: 8px;
  font-size: 13px;
  color: var(--dim-on-surface);
  font-variant-numeric: tabular-nums;
}

.gd-lastplay { color: var(--text-on-surface); }

.gd-duelist {
  list-style: none;
  margin: 4px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.gd-duelist li { display: flex; align-items: baseline; gap: 8px; font-size: 13px; }
.gd-duename { color: var(--text-on-surface); font-weight: 600; }
.gd-duetoday { color: var(--dim-on-surface); font-variant-numeric: tabular-nums; }
/* The season average is the last thing on the line and the least urgent. */
.gd-dueavg { margin-left: auto; color: var(--dim-on-surface); font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------------------
   The inning grid.

   Scrolls inside itself rather than pushing the card wide - extra innings can
   run to fifteen columns and the card must not grow a horizontal scrollbar.
--------------------------------------------------------------------------- */

/* flex-basis for the live card, where this IS a flex item; plain block in the
   recap. min-width: 0 so that a flex ancestor cannot be forced wider by the
   table's min-content width - which is how a 19-column extra-innings grid
   would otherwise stretch the whole page sideways instead of scrolling here. */
.gd-gridwrap { flex-basis: 100%; min-width: 0; max-width: 100%; overflow-x: auto; }

.gd-grid {
  border-collapse: collapse;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  min-width: 100%;
}
/* ONE INK FOR EVERY DIGIT (2026-09-26): innings, totals, headers and team
   abbreviations are all the card's text colour. What used to be told by
   colour - the innings dimmer than R H E, our row in gold - is told by weight
   and by the rule before R. */
.gd-grid th, .gd-grid td {
  padding: 3px 6px;
  text-align: center;
  color: var(--text-on-surface);
  font-weight: 400;
  border-bottom: 1px solid var(--card-rule);
  white-space: nowrap;
}
.gd-grid thead th {
  font-family: 'Plus Jakarta Sans', sans-serif;
  /* 12px, the body digits' own size and ink: 10px is the dim labels' and
     11px the state label's, and a size has one colour on the card. */
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.04em;
}
.gd-grid tbody th { text-align: left; color: var(--text-on-surface); font-weight: 600; }
.gd-grid tbody tr:last-child th, .gd-grid tbody tr:last-child td { border-bottom: none; }
/* R, H and E are the answer; the innings are the working - by weight, and
   set apart by a rule. */
.gd-grid .gd-tot { font-weight: 700; }
.gd-grid .gd-tot-first { border-left: 1px solid var(--card-rule); padding-left: 8px; }
/* Us, by weight - never by colour. */
.gd-gridus th, .gd-gridus .gd-tot { font-weight: 800; }

/* --- the wire, beside the score --- */

.gd-latest {
  display: block;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  text-decoration: none;
  color: inherit;
}
.gd-latest-head { display: block; font-size: 14px; line-height: 1.4; color: var(--text-on-surface); }
.gd-latest-src { display: block; font-size: 12px; color: var(--dim-on-surface); margin-top: 3px; }

/* ---------------------------------------------------------------------------
   NL West standings.

   Collapsed by default with the Padres line as the summary: five rows above
   the feed is a real chunk of a phone screen, and the feed is the product.
   Tabular numerals so the columns line up. Text only, no marks.
--------------------------------------------------------------------------- */

.standings {
  margin: 0 16px 18px;
  padding: 12px 14px;
  /* Its own fill and edge; --surface is rebound so the cut label's mask
     (.st-cut span) paints the same colour as the card behind it. */
  --surface: var(--standings-fill);
  background: var(--standings-fill);
  border: var(--standings-edge-width) solid var(--standings-edge);
  box-shadow: inset 0 0 0 1px var(--card-inner), var(--keyline);
  border-radius: 14px;
}

.standings summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.standings summary::-webkit-details-marker { display: none; }

.st-summary { font-size: 15px; color: var(--text-on-surface); }
/* THE BRACKET CARD (1.3): the standings card's fill, label and 15px line,
   stacked as the standings summary stacks them. */
.bracket-card { display: flex; flex-direction: column; gap: 2px; }

.st-table {
  width: 100%;
  margin-top: 12px;
  border-collapse: collapse;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

.st-table th {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim-on-surface);
  text-align: right;
  padding: 0 0 6px 0;
  border-bottom: 1px solid var(--card-rule);
}
.st-table th:nth-child(1), .st-table th:nth-child(2) { text-align: left; }

.st-table td {
  padding: 6px 0;
  color: var(--dim-on-surface);
  text-align: right;
  border-bottom: 1px solid var(--card-rule);
}
.st-table tr:last-child td { border-bottom: none; }

.st-rank { text-align: left; width: 18px; }
.st-team { text-align: left; color: var(--text-on-surface); font-weight: 600; }
.st-rec { color: var(--text-on-surface); }

/* Us, in gold. The only row that needs to be findable at a glance. */
.st-us td { color: var(--gold-ink); }
.st-us .st-team { color: var(--gold-ink); font-weight: 700; }

/* ---------------------------------------------------------------------------
   Division / wild card toggle.

   One panel, two tables. See renderStandings for why this is a toggle and not
   a second collapsible.
--------------------------------------------------------------------------- */

.st-tabs {
  display: flex;
  gap: 6px;
  margin-top: 12px;
}

.st-tab {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 6px 11px;
  border-radius: 999px;
  border: 1px solid var(--border-on-surface);
  background: transparent;
  color: var(--dim-on-surface);
  cursor: pointer;
}

/* The active tab is a gold FILL with dark ink, never gold text - gold text on
   this background is the 1.8:1 case the light palette exists to avoid. */
.st-tab-on {
  background: var(--gold);
  border-color: var(--gold);
  color: var(--on-gold);
}

.st-panel[hidden] { display: none; }

.st-empty {
  margin-top: 12px;
  font-size: 13px;
  color: var(--dim-on-surface);
}

/* ---------------------------------------------------------------------------
   THE PLAYOFF CUT LINE.

   The one piece of this table that carries the meaning. Three things make it
   unmistakable, and it needs all three, because any one alone can be missed:

   1. A heavy rule, 3px against the 1px hairlines everywhere else in the table.
   2. The words "Playoff line" sitting in the rule, so it does not depend on the
      reader already knowing what third place means.
   3. The rows below it fade. Being out is visible from the row itself, without
      having to count back up to the line.

   Colour is deliberately NOT carrying this on its own. The rule is ink-coloured
   and the weight, the label and the fade all survive a reader who cannot
   distinguish it by hue.
--------------------------------------------------------------------------- */

.st-cut td {
  padding: 0;
  border-bottom: none;
  position: relative;
  height: 20px;
}

.st-cut td::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 3px;
  margin-top: -1.5px;
  background: var(--cut);
  border-radius: 2px;
}

.st-cut span {
  position: relative;
  display: inline-block;
  /* Sits in the rule rather than beside it, so the two read as one object. */
  margin-left: 50%;
  transform: translateX(-50%);
  padding: 0 8px;
  background: var(--surface);
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--cut-label);
}

/* The teams currently in, banded together above the line.
   NOT DONE BY FADING THE ROWS BELOW, which was the first attempt: at any
   opacity low enough to read as "out", the text stopped being readable, and
   being out of the race is information the reader wants, not something to
   hide. Tinting the qualifying rows says the same thing and costs no
   legibility - every row keeps its full contrast. */
.st-in td { background: var(--in-tint); }

.st-spots-note {
  margin-top: 8px;
  font-size: 11px;
  color: var(--dim-on-surface);
}

/* Seven columns instead of six. Tightened so a phone still fits them without
   a horizontal scroll. */
.st-wc-table { font-size: 12px; }
.st-wc-table .st-rd, .st-wc-table .st-l10 { font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------------------
   Injury board.

   A second VIEW, not a filtered feed. Header lines are facts - position, name,
   which list, the date - and everything narrative under them is our own
   reporting. Headshots since the 2026-09-15 rebuild; no logos, and no
   "expected return" unless our own story states one.
--------------------------------------------------------------------------- */

.injuries { padding: 0 16px 40px; }
.injuries[hidden] { display: none; }

/* THE INJURIES PAGE (rebuilt 2026-09-15): a title, a summary line and the
   time the board was fetched, chips with counts, then one card of players.
   The card rebinds --surface to the theme's card fill as .card does, so every
   colour inside it is an on-card token; the page's own text takes the backing
   pair, set with the other backing rules further down. The two status dots
   are the only new colours - tokens per theme, measured in check-contrast. */
.inj-top { margin: 2px 0 10px; }
/* THE FETCH TIME RIDES ON THE TITLE'S LINE (2026-09-15). On a line of its
   own it was a third header row, and from the Injuries tile the first player
   row started at 559 of 667. Same sizes as before - nothing shrank. */
.inj-titlerow { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 2px 12px; }
.inj-title {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 22px;
  font-weight: 800;
  letter-spacing: -0.01em;
  line-height: 1.2;
  margin: 0;
}
.inj-sub { margin: 2px 0 0; font-size: 13px; line-height: 1.4; }
.inj-updated-at { font-size: 12px; line-height: 1.4; white-space: nowrap; }

/* The Transactions chips' shape and tokens, wrapping rather than scrolling:
   a chip past the edge is a filter nobody finds. */
.inj-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
.inj-chip {
  font-family: 'Inter', sans-serif;
  font-weight: 500;
  font-size: 13px;
  padding: 8px 15px;
  border-radius: 10px;
  border: 1px solid var(--border-on-surface);
  background: var(--surface);
  color: var(--dim-on-surface);
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
}
.inj-chip.inj-chip-on {
  border-color: var(--chip-on-edge);
  box-shadow: inset 0 0 0 1px var(--chip-on-edge);
  background: var(--chip-on);
  color: var(--on-chip-on);
  font-weight: 700;
}
.inj-chip-n { font-variant-numeric: tabular-nums; }

/* THE PLAYERS' CARD. */
.inj-list {
  --surface: var(--card-fill);
  --surface-raised: var(--card-fill-raised);
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--surface);
  border: var(--card-edge-width) solid var(--card-edge);
  border-radius: 16px;
  box-shadow: inset 0 0 0 1px var(--card-inner), var(--keyline), 0 1px 2px var(--shadow);
  overflow: hidden;
}
.inj-item { padding: 14px; }
.inj-item[hidden] { display: none; }
/* Between SHOWN rows only, so a filtered card never opens on a stray rule. */
.inj-item:not([hidden]) ~ .inj-item:not([hidden]) { border-top: 1px solid var(--card-rule); }

.inj-card-row { display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 12px; align-items: start; }
/* The disc is reserved at 44px whether or not a picture arrives; the ring
   finds it on a card of its own colour, as on the story's subject. */
.inj-shot { width: 44px; height: 44px; background: var(--surface-raised); box-shadow: 0 0 0 1px var(--subject-ring); }
.inj-shot .shot-initials { color: var(--dim-on-surface); font-size: 12px; }
.inj-shot .shot-img { width: 44px; height: 44px; }

.inj-body { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.inj-who { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 6px; }
.inj-pos {
  padding: 0 5px;
  border: 1px solid var(--border-on-surface);
  border-radius: 5px;
  background: var(--surface-raised);
  color: var(--text-on-surface);
  font-size: 11px;
  font-weight: 700;
  line-height: 16px;
}
.inj-name {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 16px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--text-on-surface);
}
/* Ours, from a guarded story - never MLB's note. */
.inj-type-line { font-size: 13px; line-height: 1.4; color: var(--text-on-surface); }
.inj-il { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px; font-size: 13px; line-height: 1.4; }
.inj-il-pill {
  padding: 0 6px;
  border: 1px solid var(--border-on-surface);
  border-radius: 5px;
  background: var(--surface-raised);
  color: var(--text-on-surface);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.05em;
  line-height: 17px;
  text-transform: uppercase;
}
.inj-placed { color: var(--dim-on-surface); }

/* THE STATUS LINE: a dot, the words, the date - only where MLB's transaction
   records carry one. The dot is decoration in a theme token; the words carry
   the meaning. */
.inj-state { display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 8px; font-size: 13px; line-height: 1.4; }
.inj-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; align-self: center; }
.inj-dot-rehab { background: var(--inj-dot-rehab); }
.inj-dot-moved { background: var(--inj-dot-moved); }
.inj-state-text { color: var(--text-on-surface); font-weight: 600; }
.inj-state-when { color: var(--dim-on-surface); }
.inj-return { font-size: 13px; line-height: 1.4; color: var(--text-on-surface); }
.inj-latest { font-size: 13px; line-height: 1.4; color: var(--dim-on-surface); }
.inj-latest-label { font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--dim-on-surface); }
.inj-latest-head { color: var(--text-on-surface); }
.inj-latest-when { color: var(--dim-on-surface); white-space: nowrap; }

/* The expander, and the timeline it opens, sit under the words, not the face. */
.inj-item .inj-reports { margin: 10px 0 0 56px; }
.inj-steps {
  list-style: none;
  margin: 10px 0 0 56px;
  padding: 0 0 0 12px;
  border-left: 2px solid var(--card-rule);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.inj-steps li {
  display: grid;
  grid-template-columns: 52px 1fr;
  gap: 8px;
  font-size: 13px;
  line-height: 1.4;
}
.inj-step-date { color: var(--dim-on-surface); font-variant-numeric: tabular-nums; white-space: nowrap; }
.inj-step-head { color: var(--text-on-surface); }

/* EVERY LOADING, ERROR AND EMPTY LINE A PAGE PAINTS goes through this class,
   and it had no ground of its own: on camo the lines sat straight on the
   pattern. The browser band audit caught "Could not load this page." there;
   the ground audit could not, because it only ever walked pages that had
   loaded (2026-09-15 - it walks the failure states now). The backing is
   transparent in three themes and camo's block on camo, so only camo changes;
   the side padding is for that block. */
.inj-empty {
  padding: 24px 16px;
  font-size: 15px;
  color: var(--text-dim);
  text-align: center;
  background: var(--backing);
  border-radius: 12px;
  box-shadow: var(--keyline);
}

/* ---------------------------------------------------------------------------
   The recap, between the last out and the reader's own midnight.

   The card used to jump from the ninth inning straight to tomorrow's probable
   pitcher, which is the one thing nobody wants at 10pm.
--------------------------------------------------------------------------- */


/* Won or lost, said in a word. A reader should not have to compare two numbers
   to find out how the night went. */
.gd-result { font-weight: 700; margin-bottom: 12px; }
.gd-win { color: var(--moss); }
.gd-loss { color: var(--brick); }
.gd-tie { color: var(--dim-on-surface); }

.gd-recapwire { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--card-rule); }

.gd-recaplist {
  list-style: none;
  margin: 6px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.gd-recaplist li { display: flex; gap: 10px; align-items: baseline; font-size: 14px; }

/* The score toggles are a different decision from the news toggles, so they
   get a heading rather than being appended to the same undifferentiated list. */
.toggle-group {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 18px 0 2px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

/* The recap's inning grid, separated exactly as the wire block below it is -
   the same 12px and the same hairline, so the three sections read as one
   stack rather than as a grid that wandered in. */
.gd-recapgrid { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--card-rule); }

/* X - the home team led and never had to bat. Dimmed because it is the
   absence of a half-inning rather than a score, and must not be mistaken for
   a number at a glance. */
.gd-notneeded { color: var(--text-dim); }

.inj-steps[hidden] { display: none; }

/* ---------------------------------------------------------------------------
   THE TRANSACTIONS PAGE (rebuilt 2026-09-14): a title, the filter chips, and
   one card per announced day holding one row per player.

   The words are the server's line, composed from MLB's structured fields and
   never its description; this page only arranges them. The day's card is a
   card: it rebinds --surface to the theme's card fill as .card does, so every
   colour inside it is an on-card token. The four pills are the only new
   colours, and they are tokens per theme (--tx-*), measured in check-contrast.
   The page's own text - title, subtitle, day and count - takes the backing
   pair, set with the other backing rules further down.
--------------------------------------------------------------------------- */

/* LOADING HOLDS A SCREEN. "Loading…" was one line tall, so the about section
   and the footer sat in view under it and were thrown down when the rows
   arrived - 0.84 CLS on a cold deep link. A screen's height keeps them below
   the fold until the rows are there; nothing is drawn in it but the word. */
.tx-loading { min-height: 100vh; }
/* ...AND FROM THE FIRST PAINT, NOT FROM WHEN THE ROUTER RUNS. On a deep link
   the page can paint before app.js has shown the section, with the footer on
   screen at 414px, and whatever then fills the section throws it down. With
   app.js held 1s and the feed 1.5s the old page measured 0.85 and this one,
   with .tx-loading alone, 0.35; a :target version of this rule measured the
   same 0.35. The pre-paint stamp in index.html names the route before the
   first frame, [hidden] keeps this to the moment before the router shows the
   section (.tx-loading holds the screen from then), and the router removes
   the stamp so no later visit to another page inherits the margin. */
html[data-prepaint-route="transactions"] #view-transactions[hidden] ~ .site-footer { margin-top: 100vh; }
html[data-prepaint-route="player"] #view-player[hidden] ~ .site-footer { margin-top: 100vh; }

.tx-head { margin: 2px 0 12px; }
.tx-title {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 22px;
  font-weight: 800;
  letter-spacing: -0.01em;
  line-height: 1.2;
  margin: 0;
}
.tx-sub { margin: 4px 0 0; font-size: 13px; line-height: 1.4; }

/* The home filter chips' shape and tokens. A class of its own, because the
   home wiring reads every .filter-chip in the document. */
/* TWO ROWS, NOT A SIDEWAYS STRIP (2026-09-15). Five chips need about 392px
   and the row has 343 at 375, so as a scrolling strip "Status" sat cut off
   at the edge. The reference tiles made the same call for the same reason: a
   destination a reader cannot see is one they will not visit. The labels
   keep their 13px - shrinking them to force one row is not the fix. */
.tx-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 4px; }
.tx-chip {
  font-family: 'Inter', sans-serif;
  font-weight: 500;
  font-size: 13px;
  padding: 8px 15px;
  border-radius: 10px;
  border: 1px solid var(--border-on-surface);
  background: var(--surface);
  color: var(--dim-on-surface);
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
}
.tx-chip.tx-chip-on {
  border-color: var(--chip-on-edge);
  box-shadow: inset 0 0 0 1px var(--chip-on-edge);
  background: var(--chip-on);
  color: var(--on-chip-on);
  font-weight: 700;
}

.tx-day { margin-top: 16px; }
.tx-day[hidden] { display: none; }
.tx-date {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin: 0 2px 8px;
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.tx-count { font-variant-numeric: tabular-nums; letter-spacing: 0.04em; }

/* THE DAY'S CARD. */
.tx-list {
  --surface: var(--card-fill);
  --surface-raised: var(--card-fill-raised);
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--surface);
  border: var(--card-edge-width) solid var(--card-edge);
  border-radius: 16px;
  box-shadow: inset 0 0 0 1px var(--card-inner), var(--keyline), 0 1px 2px var(--shadow);
  overflow: hidden;
}
.tx-item[hidden] { display: none; }
/* Between SHOWN rows only: under a filter, the first row left in a card must
   not carry a rule from a hidden one above it. */
.tx-item:not([hidden]) ~ .tx-item:not([hidden]) { border-top: 1px solid var(--card-rule); }

.tx-row {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr);
  align-items: center;
  gap: 12px;
  min-height: 64px;
  padding: 12px 14px;
  color: var(--text-on-surface);
  text-decoration: none;
}

/* The disc is reserved at 40px whether or not a picture arrives; the ring is
   what finds it on a card of its own colour, as on the story's subject. */
.tx-shot { width: 40px; height: 40px; background: var(--surface-raised); box-shadow: 0 0 0 1px var(--subject-ring); }
.tx-shot .shot-initials { color: var(--dim-on-surface); font-size: 12px; }
.tx-shot .shot-img { width: 40px; height: 40px; }

.tx-body { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.tx-who { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 6px; }
.tx-pos {
  padding: 0 5px;
  border: 1px solid var(--border-on-surface);
  border-radius: 5px;
  background: var(--surface-raised);
  color: var(--text-on-surface);
  font-size: 11px;
  font-weight: 700;
  line-height: 16px;
}
.tx-name {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--text-on-surface);
}
.tx-what { font-size: 13.5px; line-height: 1.45; color: var(--dim-on-surface); }
.tx-text { color: var(--dim-on-surface); }
/* A backdated move says so, rather than sitting under the day it was
   announced with no word about the gap. */
.tx-retro { color: var(--dim-on-surface); font-size: 12.5px; }

.tx-pill {
  display: inline-block;
  margin-right: 6px;
  padding: 0 6px;
  border-radius: 5px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.05em;
  line-height: 17px;
  text-transform: uppercase;
  vertical-align: 1px;
}
.tx-pill-added { background: var(--tx-added); color: var(--on-tx-added); }
.tx-pill-removed { background: var(--tx-removed); color: var(--on-tx-removed); }
.tx-pill-minors { background: var(--tx-minors); color: var(--on-tx-minors); }
.tx-pill-status { background: var(--tx-status); color: var(--on-tx-status); }

/* ---------------------------------------------------------------------------
   The Roster tab: injuries, then transactions.
--------------------------------------------------------------------------- */

.roster-half + .roster-half { margin-top: 28px; }

/**
 * THE THREAD EXPANDER: A CONTROL THAT LOOKS LIKE ONE.
 *
 * This was "7 reports" in dim grey inside a row that was itself a giant
 * <button>. Neither part announced itself - the row gave no visual sign it was
 * pressable, and the count read as a statistic. It is now a bordered pill with
 * a chevron, so it reads as pressable BEFORE it is pressed.
 *
 * ON THE PLAYERS' CARD since the 2026-09-15 rebuild (.inj-list), so gold sits
 * on the card ground it needs. The pill still fills itself with --surface -
 * the card's own fill there - so it holds that ground wherever it is used.
 * (Before the rebuild the board had no card, its first painted ancestor was
 * <body>, and gold on a transparent pill would have been 1.73: the
 * .inj-thread bug a second time.)
 *
 * 44px MINIMUM, per the iOS guideline. The pill is the whole target now that
 * the row is no longer a button, so it has to earn that on its own rather than
 * inherit it from a large ancestor.
 */
.inj-reports {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  margin-top: 8px;
  padding: 0 16px;
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 13px;
  font-weight: 700;
  color: var(--gold);
  background: var(--surface);
  border: 1px solid var(--gold-edge);
  border-radius: 22px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.inj-reports:hover,
.inj-reports:focus-visible {
  border-color: var(--gold);
}

/* Expanded reads as filled, so the open row is identifiable at a glance in a
   board where several may be open at once. */
.inj-reports[aria-expanded="true"] {
  background: var(--gold);
  color: var(--on-gold);
  border-color: var(--gold);
}

/**
 * A chevron built from a rotated border corner rather than an SVG or a glyph:
 * it inherits currentColor, so it flips with the expanded state for free, and
 * there is no asset to load.
 */
.inj-chevron {
  width: 7px;
  height: 7px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  /* Pointing down when collapsed - "there is more below". */
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform 150ms ease;
}

.inj-reports[aria-expanded="true"] .inj-chevron {
  /* Up when open: pressing again closes it. */
  transform: rotate(-135deg) translate(-1px, -1px);
}

@media (prefers-reduced-motion: reduce) {
  .inj-chevron { transition: none; }
}

/* ---------------------------------------------------------------------------
   TILES ARE DESTINATIONS; CHIPS ARE FILTERS.

   The six destinations are ONE STRIP (2026-09-14): a light panel the width of
   the content, six columns, hairline dividers, an icon over a small label and
   no fill behind any one cell. The chips stay pills below it, so shape still
   says which row goes somewhere and which narrows the feed.

   THE LABELS LEFT THE LARGE-TEXT TIER. The tiles were 18.67px/700 so that two
   themes' inks (4.10 and 4.15) could clear 3.0. At six across the labels are
   11px, so every one is held to 4.5 - the strip inks are 11.33, 7.11, 11.71
   and 12.99 - and "tile" is no longer in scripts/fixtures/large-text.json.

   MEASURED AT 375, not guessed: the content width is 343px, 57.2px a cell.
   "Transactions" was 60.3px at 10px and 66.4px at 11px, so that cell reads
   "Moves" and the page it opens keeps its Transactions heading. The widest
   label left is 50.2px at 11px/600.
--------------------------------------------------------------------------- */
.reference-tiles {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  width: min(100% - 32px, 608px);
  margin: 0 auto 14px;
  background: var(--strip-fill);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: var(--keyline);
  overflow: hidden;
}

.tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  /* Without this the longest label sets the track width and the page scrolls
     sideways - measured at 450px on a 375px screen, with the old tiles. */
  min-width: 0;
  min-height: 58px;
  padding: 8px 2px 7px;
  color: var(--on-strip);
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
}
/* Hairlines between the columns, none at the ends. */
.tile + .tile { border-left: 1px solid var(--strip-divider); }
.tile-icon { width: 22px; height: 22px; flex: none; }

/* The page you are already on: a bar along the cell's foot in the strip's
   mark colour - no fill, like every other cell, and the same ink, so the
   label is measured once at 4.5 like the rest. */
.tile-on { box-shadow: inset 0 -3px 0 var(--strip-mark); }

/* ---------------------------------------------------------------------------
   The reference tables.
--------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   THE ROSTER PAGE (rebuilt 2026-09-15) - app.js renderRoster. A title, the
   group chips, the search and two pickers, and one card holding the table.

   EVERY COLUMN FITS AT 375. The table is fixed-layout: six narrow columns and
   the name taking what is left, wrapping onto a second line rather than the
   table scrolling sideways, and no label shrunk to make it fit. Every colour
   is an existing token; the card is the Injuries card.
--------------------------------------------------------------------------- */
.ros-top { margin: 2px 0 10px; }
.ros-titlerow { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 2px 12px; }
.ros-title {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 22px;
  font-weight: 800;
  letter-spacing: -0.01em;
  line-height: 1.2;
  margin: 0;
}
.ros-updated { font-size: 12px; line-height: 1.4; white-space: nowrap; }

/* The Transactions chips' shape and tokens, with a count, wrapping. */
.ros-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 10px; }
.ros-chip {
  font-family: 'Inter', sans-serif;
  font-weight: 500;
  font-size: 13px;
  padding: 8px 15px;
  border-radius: 10px;
  border: 1px solid var(--border-on-surface);
  background: var(--surface);
  color: var(--dim-on-surface);
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
}
.ros-chip.ros-chip-on {
  border-color: var(--chip-on-edge);
  box-shadow: inset 0 0 0 1px var(--chip-on-edge);
  background: var(--chip-on);
  color: var(--on-chip-on);
  font-weight: 700;
}
.ros-chip-n { font-variant-numeric: tabular-nums; }

/* THE SEARCH AND THE PICKERS, wrapping. The fill is on the field's label and
   on each picker's wrapper, not the control, so the magnifier or the chevron
   and the text beside it sit on one measured ground. 16px in the field: iOS
   zooms the page into a smaller input when it takes focus. */
.ros-tools { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
.ros-search {
  flex: 1 1 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 12px;
  border: 1px solid var(--border-on-surface);
  border-radius: 10px;
  background: var(--surface);
  color: var(--text-on-surface);
}
.ros-search:focus-within { outline: 2px solid var(--gold-ink); outline-offset: 2px; }
.ros-search-icon { flex: none; width: 16px; height: 16px; color: var(--dim-on-surface); }
.ros-q {
  flex: 1 1 auto;
  min-width: 0;
  height: 42px;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font-family: 'Inter', sans-serif;
  font-size: 16px;
  outline: none;
}
.ros-q::placeholder { color: var(--dim-on-surface); opacity: 1; }
.ros-picks { display: flex; flex-wrap: wrap; gap: 8px; }
.ros-pick {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  border: 1px solid var(--border-on-surface);
  border-radius: 10px;
  background: var(--surface);
  color: var(--text-on-surface);
}
.ros-pick:focus-within { outline: 2px solid var(--gold-ink); outline-offset: 2px; }
.ros-select {
  appearance: none;
  -webkit-appearance: none;
  min-height: 40px;
  padding: 8px 32px 8px 14px;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: inherit;
  font-family: 'Inter', sans-serif;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  outline: none;
}
.ros-chev { position: absolute; right: 13px; top: 50%; width: 10px; height: 10px; margin-top: -5px; pointer-events: none; }

/* THE TABLE, on the card. */
.ros-card, .pl-card {
  --surface: var(--card-fill);
  --surface-raised: var(--card-fill-raised);
  background: var(--surface);
  border: var(--card-edge-width) solid var(--card-edge);
  border-radius: 16px;
  box-shadow: inset 0 0 0 1px var(--card-inner), var(--keyline), 0 1px 2px var(--shadow);
  padding: 2px 6px;
}
.ros-table {
  width: 100%;
  table-layout: fixed;
  border-collapse: collapse;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--dim-on-surface);
}
.ros-table th {
  padding: 10px 3px 8px;
  text-align: left;
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim-on-surface);
  border-bottom: 1px solid var(--border-on-surface);
}
/* 34 + 38 + 32 + 44 + 32 + 28 = 208px of fixed columns; the name has the
   rest - 123px at 375 in Classic, 91 inside camo's backing, which still holds
   the longest surname on the roster, Cronenworth, on one line. Height takes
   44 so 5'10" never runs into the weight. Measured in the pane, not reasoned. */
.ros-h0 { width: 34px; }
.ros-h2 { width: 38px; }
.ros-h3 { width: 32px; }
.ros-h4 { width: 44px; }
.ros-h5 { width: 32px; }
.ros-h6 { width: 28px; }
.ros-table td { padding: 9px 3px; vertical-align: middle; border-bottom: 1px solid var(--border-on-surface); }
.ros-table tbody tr:last-child td { border-bottom: 0; }
.ros-name {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 14px;
  font-weight: 700;
  line-height: 1.25;
  color: var(--text-on-surface);
  overflow-wrap: break-word;
}
.ros-num, .ros-pos {
  display: inline-block;
  min-width: 26px;
  padding: 0 4px;
  border: 1px solid var(--border-on-surface);
  border-radius: 5px;
  background: var(--surface-raised);
  color: var(--text-on-surface);
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}
.ros-none td { padding: 14px 3px; color: var(--text-on-surface); }

/* ---------------------------------------------------------------------------
   THE PLAYER PAGE (#player/<id>, 2026-09-22). One player, opened from a
   Roster row.

   EVERY COLOUR IS AN EXISTING TOKEN, AND EVERY SURFACE AN EXISTING ONE: the
   page is backed like the Roster page (the backing group, below), its cards
   are the Roster card (.ros-card, .pl-card), its season table is the Stats
   page's (.ref-table), its IL block is the Injuries row's classes and its
   moves the Moves row's. Text on the backing takes the backing pair; text on
   a card, the surface pair - the two grounds the audits measure it on.
--------------------------------------------------------------------------- */
.pl-page { display: flex; flex-direction: column; gap: 16px; }
.pl-bar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0 12px; }
/* A tap target of its own height, not the line's. */
.pl-back {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: 14px;
  font-weight: 700;
  color: var(--on-backing);
  text-decoration: none;
}
.pl-updated { font-size: 12px; line-height: 1.4; color: var(--dim-on-backing); }
/* A deep link paints before the payload arrives; a screen's height keeps
   the footer below the fold until it does (as .tx-loading). */
.pl-loading { min-height: 100vh; }

.pl-card { padding: 14px; }
.pl-top { display: flex; align-items: flex-start; gap: 14px; }
.pl-id { flex: 1; min-width: 0; }
.pl-name {
  margin: 0;
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 22px;
  font-weight: 800;
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--text-on-surface);
  overflow-wrap: anywhere;
}
.pl-tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 0; }
.pl-facts { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 12px 0 0; }
.pl-fact dt {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim-on-surface);
}
.pl-fact dd { margin: 0; font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text-on-surface); }

.pl-sec-title, .pl-group {
  margin: 0 2px 8px;
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim-on-backing);
}
.pl-group { margin-top: 12px; }
.pl-il { display: flex; flex-direction: column; gap: 6px; }
.pl-status { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.4; color: var(--text-on-surface); }
.pl-total td { color: var(--text-on-surface); font-weight: 700; }

/* THE CARD'S INSET SIDEWAYS, THE ROWS' OWN DOWN: a list resets padding, and
   a list that is also a .pl-card must not lose the card's. */
.pl-list { list-style: none; margin: 0; padding: 0 14px; }
.pl-list > li + li { border-top: 1px solid var(--card-rule); }
.pl-move {
  display: grid;
  grid-template-columns: 56px minmax(0, 1fr);
  align-items: baseline;
  gap: 10px;
  padding: 12px 0;
}
.pl-when { font-size: 12.5px; line-height: 1.4; font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--dim-on-surface); }
.pl-story {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 10px;
  min-height: 44px;
  padding: 10px 0;
  color: var(--text-on-surface);
  text-decoration: none;
}
.pl-story-head { font-size: 14px; font-weight: 600; line-height: 1.4; color: var(--text-on-surface); }

/* THE ROWS THAT OPEN IT - Roster, Injuries, Stats: the name reads as a link,
   in the colour its row already gives the name, and the row shows it can be
   tapped. */
.row-link { color: inherit; text-decoration: underline; text-decoration-color: var(--border-on-surface); text-underline-offset: 3px; }
[data-player] { cursor: pointer; }

.ref-page { padding: 0 16px; }
.ref-second { margin-top: 24px; }

.ref-note {
  font-size: 12px;
  color: var(--text-dim);
  margin: 8px 0 0;
}

/* min-width: 0 IS THE LOAD-BEARING DECLARATION. Without it the table's
   min-content width pushes an ancestor wider and the whole PAGE scrolls
   sideways on a phone - the one thing a layout must never do. Verified at
   375px rather than reasoned about. */
.ref-scroll {
  overflow-x: auto;
  min-width: 0;
  max-width: 100%;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
}

.ref-table {
  border-collapse: separate;
  border-spacing: 0;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--dim-on-surface);
  white-space: nowrap;
}

.ref-table th, .ref-table td {
  padding: 7px 10px;
  text-align: right;
  border-bottom: 1px solid var(--border-on-surface);
}
.ref-table tr:last-child td { border-bottom: none; }

.ref-table thead th {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim-on-surface);
  /* NOT STICKY TO THE TOP. .ref-scroll scrolls sideways only, so a header
     stuck to its top edge never had anything to stick against - and on
     iOS the box score's header row painted OUTSIDE its container, up by
     the chip row, once the table was long enough to matter. The header
     needs the same treatment as the name column: the corner cell stays put
     sideways (.ref-name, sticky left) and nothing sticks vertically. */
  background: var(--surface);
}

/* THE NAME COLUMN STAYS PUT while the numbers scroll past it - a row of
   figures with the name scrolled off is unattributable. It needs its own
   background or the scrolling cells show through underneath. */
.ref-name {
  position: sticky;
  left: 0;
  text-align: left !important;
  background: var(--surface);
  color: var(--text-on-surface) !important;
  font-weight: 600;
  /* Above the scrolling cells, below the sticky header's own corner. */
  z-index: 1;
}
.ref-table thead .ref-name { z-index: 2; }

/* The schedule reads as a list rather than a stat grid, so its first column is
   the date and nothing is sticky-wide. */
.sch-date { text-align: left !important; }
.sch-opp { text-align: left !important; color: var(--text-on-surface); }
/* A PLAYOFF ROW SAYS WHICH ROUND AND GAME (2026-09-23), on a line of its own
   under the opponent, in the table's secondary ink. */
.sch-round { display: block; font-size: 11px; line-height: 1.3; color: var(--dim-on-surface); white-space: normal; }
.sch-result { font-weight: 700; }
.sch-win { color: var(--moss); }
.sch-loss { color: var(--brick); }
.sch-tie, .sch-ppd { color: var(--dim-on-surface); }
.sch-time { color: var(--dim-on-surface); }

/* The next game to be played, so the eye lands on it after the scroll.
   ON --raised RATHER THAN THE GOLD BAND: a completed game can occupy this row
   when today's game has already finished, and a loss measured 4.43 on the gold
   tint. --raised is a ground every one of these colours is already proven
   against, and the emphasis comes from a gold rule on the leading edge. */
.sch-next td { background: var(--surface-raised); }
.sch-next td:first-child { box-shadow: inset 3px 0 0 var(--gold); }

/* ---------------------------------------------------------------------------
   THE SUBJECT'S HEADSHOT, at the head of the avatar stack.

   Same 24px circle as the initials beside it, so the stack reads as one row of
   people rather than a photo with decorations. It leads because the story is
   about the player and only reported by the others.

   object-fit: cover because MLB's crop is 67-tall and squaring it by scaling
   would squash the face. The border matches .avatar so a card with a headshot
   and a card without have identically shaped stacks - the fallback is the
   common path and must not look like a degraded version of anything.
--------------------------------------------------------------------------- */
/* ---------------------------------------------------------------------------
   THE SUBJECT, BESIDE THE HEADLINE.

   52px against the sources row's 24px. The gap is the whole point: one form
   meant two things - who the story is about and who wrote it - at a size where
   neither read as a photograph. Size and position carry the distinction now.

   SEPARATION FROM THE BROWN. A dark photo on a dark card reads as a hole in
   the card rather than as a picture, so it gets a ring and a lighter disc
   behind it. The disc is also what fills the box while the image is in flight,
   which is why it matches the raised ground rather than being transparent.

   No object-position: the URL crops square and face-centred at source, so
   nudging it here would fight a per-image decision with a fixed guess.
--------------------------------------------------------------------------- */
.card-lede {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}

/* The headline takes the rest of the row. min-width: 0 is load-bearing on a
   flex child: without it a long unbroken word sets the min-content width and
   pushes the card wider than the page - the same rule the wide tables needed. */
.card-lede h3 { flex: 1; min-width: 0; }

.subject-shot {
  flex: none;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface-raised);
  border: 2px solid var(--subject-ring);
}
/* The disc with his initials (1.3); the photo fills it over them. */
.subject-shot .shot-initials { color: var(--dim-on-surface); font-size: 16px; }
.subject-shot .shot-img { width: 100%; height: 100%; }
/* NO DISC WITHOUT INITIALS (1.3, personShot): a photo with nothing under it
   is invisible - its box held - until it has loaded, and gone if it fails. */
.shot-bare { background: none; }
.shot-bare:not(.shot-in) { visibility: hidden; }

/* No object-position: see above. */
.avatar-shot {
  object-fit: cover;
  background: var(--surface-raised);
}

/* ---------------------------------------------------------------------------
   AROUND THE LEAGUE.

   Rows, not cards. These items never went through the classifier, the guards
   or a summary, so they must not wear the furniture of something that did -
   a Padres card's pills and badges are claims, and these have earned none of
   them. A link, an outlet and a time is the whole of what this tab knows.

   44px minimum, like every other row a thumb has to hit.
--------------------------------------------------------------------------- */
.nat-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--border-on-surface);
  border-radius: 12px;
  overflow: hidden;
}

.nat-item + .nat-item .nat-link { border-top: 1px solid var(--border-on-surface); }

.nat-link {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-height: 44px;
  padding: 12px 14px;
  text-decoration: none;
  background: var(--surface);
}

.nat-link:hover,
.nat-link:focus-visible { background: var(--surface-raised); }

.nat-head {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 14.5px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--text-on-surface);
}

.nat-meta { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.nat-outlet { font-size: 12px; font-weight: 600; color: var(--gold-ink); }
.nat-when { font-size: 12px; color: var(--dim-on-surface); }

/* Only ever shown when the list is actually old - signal, not decoration. */
.nat-stale {
  font-size: 12px;
  color: var(--text-dim);
  margin-bottom: 8px;
}

/* ---------------------------------------------------------------------------
   HEADSHOTS, AND THE TWO GROUNDS THEY LAND ON.

   The image is the same everywhere; the INITIALS UNDERNEATH are not. They show
   when a headshot 404s, so they are the visible state roughly as often as the
   picture is, and they have to be legible on whichever surface they fall on.

   This is the mistake that cost .gd-pitchername (page ink on a brown card at
   1.32) and .gd-next (right on one state, wrong on the other). The gameday
   card is --surface, so its initials take the ON-SURFACE tokens, not the page
   ones - stated here rather than inherited from whatever the parent happened
   to set.
--------------------------------------------------------------------------- */
.shot {
  position: relative;
  display: inline-block;
  flex: none;
  border-radius: 50%;
  overflow: hidden;
}

.shot-initials {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* The image sits ON the initials and removes itself on 404, so what is left is
   a named circle rather than a gap or a broken-image icon. */
.shot-img {
  position: relative;
  display: block;
  border-radius: 50%;
  object-fit: cover;
}

/* The gameday card's ground. Both tokens are the on-surface pair. */
.gd-shot {
  /* THE DISC IS THE CONTAINING BLOCK FOR ITS INITIALS. .shot-initials is
     position: absolute; inset: 0, which fills the nearest POSITIONED
     ancestor. On the gameday card that happened to be the slide; on the
     game page's Leaders card there was none, so each initials span filled
     the initial containing block - the whole viewport, 375x812 - and sat
     above every static control. Four of them, one per leader. Every tap on
     the Box tab landed on an invisible span: Back, the tabs, everything,
     while scrolling still worked. Seen on TestFlight on a live game and
     reproduced by elementFromPoint. This one declaration is the fix; the
     test asserts it stays. */
  position: relative;
  display: inline-flex;
  width: 28px;
  height: 28px;
  margin-right: 8px;
  vertical-align: middle;
  background: var(--surface-raised);
  /* FOUND BY ITS EDGE, NOT ONLY BY ITS FILL. On tan the raised ground is a
     lighter brown and the disc reads on its own; on a theme whose card and
     raised grounds are both near-white it measured 1.08 against the card, so a
     404'd headshot left initials floating with no circle at all. The ring is
     the same mechanism .subject-shot already uses, and the same one that
     separates a card from the page where luminance does not. */
  border: 1px solid var(--subject-ring);
}
.gd-shot .shot-initials { color: var(--dim-on-surface); }
/* No fill on the photo (1.3): a filled <img> covered the initials while the
   photo was still on its way - a blank disc. The wrapper is the disc. */
.gd-shot .shot-img { width: 28px; height: 28px; }

.gd-pitching { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }

/* ---------------------------------------------------------------------------
   DIAGNOSTICS, COLLAPSED.

   The panel is a wall of internal state and it was permanently open. It stays
   whole - this only folds it, and puts the one fact a reader wants on the
   outside where they can see it without expanding.
--------------------------------------------------------------------------- */
.diag-details {
  margin-top: 18px;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
}

.diag-summary {
  min-height: 44px;
  display: flex;
  align-items: center;
  padding: 0 14px;
  cursor: pointer;
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  list-style: none;
}

/* The default marker is a black triangle that ignores the palette. */
.diag-summary::-webkit-details-marker { display: none; }

.diag-summary::after {
  content: "";
  margin-left: auto;
  width: 7px;
  height: 7px;
  border-right: 2px solid var(--text-dim);
  border-bottom: 2px solid var(--text-dim);
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform 150ms ease;
}

.diag-details[open] .diag-summary::after { transform: rotate(-135deg) translate(-1px, -1px); }

@media (prefers-reduced-motion: reduce) {
  .diag-summary::after { transition: none; }
}

.diag-details .diag { padding: 0 14px; }
.diag-details .push-btn { margin: 10px 14px 14px; }

/* ---------------------------------------------------------------------------
   THE OFFLINE BANNER.

   It sits on the PAGE ground, not a card, so it takes the page tokens - the
   distinction that made .inj-reports unreadable when it was assumed to be on
   a card. It paints its own surface precisely so it is not one more thing
   guessing what is behind it.
   --------------------------------------------------------------------------- */

.offline-banner {
  margin: 0 0 12px;
  padding: 10px 14px;
  border: 1px solid var(--border-on-surface);
  border-radius: 10px;
  background: var(--surface);
  color: var(--text-on-surface);
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 13px;
  font-weight: 600;
}

/* ---------------------------------------------------------------------------
   THE THEME SWITCHER.

   The swatches are the one place raw brand hues appear as fills, and they are
   exempt from the ink rules because they hold no text - they are three blocks
   of colour with the label beside them, never behind them.
   --------------------------------------------------------------------------- */

/* THE SWITCHER IS A REAL <select>, STYLED AS A CHIP, in the chip row. A
   native select is what gives iOS its wheel picker, and the chip row is
   where a reader already looks to change what they are looking at. The
   swatches are gone: the option text is the theme's name and the page
   itself is the preview. */
.theme-chip-wrap {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
}
.theme-chip {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 30px;
  /* The chip's own colours - a select inherits the UA's otherwise. */
  font-family: 'Inter', sans-serif;
  font-weight: 700;
  color: var(--dim-on-surface);
  background: var(--surface);
}
.theme-chip:focus-visible { outline: 2px solid var(--gold-ink); outline-offset: 2px; }
/* The chevron, in currentColor so every theme's chip ink carries it. */
.theme-chip-wrap::after {
  content: "";
  position: absolute;
  right: 13px;
  top: 50%;
  width: 6px;
  height: 6px;
  border-right: 1.5px solid var(--dim-on-surface);
  border-bottom: 1.5px solid var(--dim-on-surface);
  transform: translateY(-70%) rotate(45deg);
  pointer-events: none;
}

/* ---------------------------------------------------------------------------
   THE BOX SCORE.

   The tables themselves are .ref-scroll / .ref-table / .ref-name - the Stats
   tile's, unchanged. Only the disclosure around them is new, and it follows
   the Diagnostics panel: a 44px summary row, a caret that turns, and nothing
   rendered at all until the reader asks for it.
   --------------------------------------------------------------------------- */

.gd-box {
  margin-top: 14px;
  border-top: 1px solid var(--card-rule);
}

.gd-boxsummary {
  min-height: 44px;
  display: flex;
  align-items: center;
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  /* THE CARD'S 13px INK (2026-09-26): 13px on the Final card is dim - the
     city, the record, the vs - and a size has one colour. The chevron and
     the 44px row say it opens; gold said it too, at the cost of the rule. */
  color: var(--dim-on-surface);
  cursor: pointer;
  list-style: none;
}
.gd-boxsummary::-webkit-details-marker { display: none; }

.gd-boxsummary::after {
  content: "";
  width: 7px;
  height: 7px;
  margin-left: 8px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 150ms ease;
}
.gd-box[open] .gd-boxsummary::after { transform: rotate(-135deg) translate(-1px, -1px); }

@media (prefers-reduced-motion: reduce) {
  .gd-boxsummary::after { transition: none; }
}

/* min-width: 0 AGAIN, AND FOR THE SAME REASON. The wrapper inside handles its
   own scrolling, but a flex or grid child defaults to min-width: auto and will
   hand the table's min-content width straight to the page. */
.gd-boxbody { min-width: 0; padding-bottom: 4px; }

.gd-boxlabel {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim-on-surface);
  margin: 10px 0 6px;
}

/* The team switch. Padres is the default and the server puts them first; this
   only ever remembers the choice for the session. */
.gd-boxteams { display: flex; gap: 6px; margin-bottom: 4px; }

/* ONE TEXT COLOUR FOR BOTH STATES (2026-09-26): 12px in the box score is the
   digits' ink, so the chosen team is told by its FILL, BORDER and WEIGHT -
   never by its text colour, which was dark-on-gold beside dim. */
.gd-boxteam {
  min-height: 32px;
  padding: 0 12px;
  border: 1px solid var(--border-on-surface);
  border-radius: 999px;
  background: transparent;
  color: var(--text-on-surface);
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
}
.gd-boxteam[aria-pressed="true"] {
  background: var(--surface-raised);
  border: 2px solid var(--gold-ink);
  padding: 0 11px;
  font-weight: 800;
}

/* A substitute is indented, the way a box score prints one. */
.gd-boxsub { padding-left: 20px !important; }

/* THE BOX SCORE, ONE COLOUR PER SIZE (2026-09-26), in the Final card's box
   (.gd-box) and the game page's (.gp-box) - not the Stats tables, which share
   the table but not this rule yet. The name column's header is a header
   like the rest (dim, 10px); a player's name is 14px - the card's size for
   text in its full ink - because 13px in the table is the dim of the numbers. */
.gd-box thead .ref-name, .gp-box thead .ref-name { color: var(--dim-on-surface) !important; }
.gd-box tbody .ref-name, .gp-box tbody .ref-name { font-size: 14px; }

/* ---------------------------------------------------------------------------
   THE GAMEDAY CAROUSEL.

   CSS scroll-snap on an overflow container, not a JS gesture handler. A
   hand-rolled swipe listener has to judge, on every touchmove, whether a drag
   is horizontal enough to claim - and when it judges wrong it has stolen a
   vertical scroll that belonged to the feed, which makes the whole PAGE feel
   broken. Native overflow gets momentum, rubber-banding and the browser's own
   axis locking, and cannot fight a gesture it never listens to.

   overscroll-behavior-x: contain stops a swipe past the last slide turning
   into the browser's back-navigation gesture on iOS.

   NAMED gd-navdot, NOT gd-dot. .gd-dot is already the live card's balls,
   strikes and outs - reusing it inherited a 1.5px border and a 50% radius onto
   a nav control, and renaming the live card's instead would have been renaming
   the wrong thing.
--------------------------------------------------------------------------- */
.gd-carousel { position: relative; }

.gd-track {
  display: flex;
  overflow-x: auto;
  /* NO NESTED VERTICAL SCROLL REGION. overflow-x: auto forces overflow-y to
     compute as auto, so any moment when the pinned height is shorter than the
     card turns the track into a vertically scrollable box - and a vertical drag
     starting on the card then scrolls INSIDE the carousel instead of scrolling
     the page. The reader is left unable to reach the standings and the feed
     below it by dragging on the thing filling their screen. Clipping is the
     lesser failure, and the height logic is what stops it happening at all. */
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  /* The height is set in script to the slide being read; this carries it.
     It is switched OFF for the whole of a gesture by .gd-scrolling below - an
     animating container is a resizing container, and resizing a scroll-snap
     container while the browser is resolving a snap is what left two cards
     half visible on a real iPhone. */
  transition: height 180ms ease;
}

/* Applied by wireCarousel for as long as a scroll or a drag is in progress. */
.gd-track.gd-scrolling { transition: none; }

.gd-track::-webkit-scrollbar { display: none; }

/* A visible focus ring, because the track is tabbable - arrow keys are how
   this is reachable without a touchscreen at all. */
.gd-track:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }

.gd-slide {
  /* EXACTLY THE CONTAINER'S WIDTH. box-sizing is border-box globally and the
     track carries no padding, so 100% here is the track's own content width -
     which is what mandatory snapping needs. A slide even a pixel off produces
     snap positions that do not line up with slide edges. */
  flex: 0 0 100%;
  min-width: 0;
  /* ONE alignment value, on the slides, and nowhere else in this sheet. */
  scroll-snap-align: start;
  /* A fast flick may not skip past a card. Without this, momentum can carry
     several slides on and land between two of them; with it every slide is a
     stopping point, which is also what makes a five-slide carousel navigable
     with a thumb rather than a fling. */
  scroll-snap-stop: always;
  /* CONTENT HEIGHT, NOT STRETCHED. A flex item defaults to the full height of
     its row, which would make every slide as tall as the tallest and give the
     script below no smaller height to measure. */
  align-self: flex-start;
}

/* Dots are REAL BUTTONS: a swipe with no affordance is invisible to anyone who
   was not told about it. 44px of hit area around an 8px mark. */
.gd-navdots {
  display: flex;
  justify-content: center;
  margin: 2px 0 6px;
}

.gd-navdot {
  width: 28px;
  height: 44px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  -webkit-tap-highlight-color: transparent;
}

.gd-navdot::after {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-dim);
  opacity: 0.45;
  transition: opacity 150ms ease, background-color 150ms ease;
}

.gd-navdot[aria-selected="true"]::after { background: var(--text); opacity: 1; }
.gd-navdot:focus-visible { outline: 2px solid var(--text); outline-offset: -6px; border-radius: 8px; }

@media (prefers-reduced-motion: reduce) {
  .gd-navdot::after { transition: none; }
  .gd-track { scroll-behavior: auto; transition: none; }
}

/* An upcoming game is context, not the thing the reader opened the app for -
   so it does not wear the pinned card's gold border. */
.gameday-next { border-color: var(--border-on-surface); }

.gd-when {
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 15px;
  font-weight: 600;
  color: var(--text-on-surface);
  margin-bottom: 8px;
}

/* TBD is an absence and reads as one - it must not look like a name. */
.gd-tbd { color: var(--dim-on-surface); font-weight: 400; font-style: italic; }


/* ---------------------------------------------------------------------------
   TEXT THAT SITS ON THE PAGE READS THE BACKING PAIR.

   Every rule here used to read --text or --text-dim straight onto --bg. That
   is still exactly what it does in tan, beach and city, where --backing is
   transparent and the backing inks ARE the page inks. In camo the page is a
   pattern with no contrast ratio, so the same rules paint a raised backing
   under themselves and take the on-surface pair.

   THE LIST WAS NOT WRITTEN BY HAND. test-ground-audit walks index.html and
   every renderer's output with textOnGround: false and reports each rule
   that places text with nothing painted under it - 22 of them, before any of
   this existed. The section labels, the injury board, the transactions feed,
   the reference notes, the off-day line, the tagline (since removed), the footer, the About
   copy, the diagnostics summary and the settings rows. This block is that
   list, and the audit fails if a new rule joins it.

   Card-scale tokens that had been reused on the page went at the same time:
   .gd-stale (brick, 1.5 on tan) and the injury thread's .w2w-date and
   .w2w-head (dim-on-surface, 1.3 on tan) rendered on the page ground in the
   reference views and had never been measured there. They have page-ground
   classes of their own now.
--------------------------------------------------------------------------- */
.inj-board, .tx-feed, .ref-page, .ros-page, .pl-page {
  background: var(--backing);
  border-radius: 12px;
  box-shadow: var(--keyline);
}
.about {
  background: var(--backing);
  border-radius: 12px;
  box-shadow: var(--keyline);
}
.about h2 { color: var(--dim-on-backing); border-top-color: var(--backing-border); }
.about p { color: var(--on-backing); }
.site-footer {
  background: var(--backing);
  border-radius: 12px;
  box-shadow: var(--keyline);
}
.disclaimer-link { color: var(--on-backing); text-decoration-color: var(--dim-on-backing); }
.diag-details { background: var(--backing); border-color: var(--backing-border); }
.diag-summary { color: var(--on-backing); }
.toggle-row { border-bottom-color: var(--backing-border); }
.toggle-label { color: var(--on-backing); }
.toggle-hint { color: var(--dim-on-backing); }
.toggle-group { color: var(--dim-on-backing); border-top-color: var(--backing-border); }

.inj-empty { color: var(--dim-on-backing); }
/* The feed's states sit on the backing too (2026-09-15). */
.state-title { color: var(--on-backing); }
.state-sub { color: var(--dim-on-backing); }
/* The Injuries page's own text; its players are on a card. */
.inj-title { color: var(--on-backing); }
.inj-sub { color: var(--dim-on-backing); }
.inj-updated-at { color: var(--dim-on-backing); }

/* The Transactions page's own text; its rows are on the day's card. */
.tx-title { color: var(--on-backing); }
.tx-sub { color: var(--dim-on-backing); }
.tx-date { color: var(--dim-on-backing); }

/* The Roster page's own text; its table is on the card. */
.ros-title { color: var(--on-backing); }
.ros-updated { color: var(--dim-on-backing); }

.nat-stale { color: var(--dim-on-backing); }
.ref-note { color: var(--dim-on-backing); }
/* The stale note on the PAGE. Brick is card-only - 1.5 on tan's page - and
   this class was the gameday card's, reused where it had never been measured. */
.gd-stale-page { color: var(--dim-on-backing); }

/* THE DOCUMENT PAGES - privacy and support - are .about sections and nothing
   else, so they take every backing rule above and render on camo like the
   Settings screen does. Links inside them, and the policy/support links in
   Settings, read the backing pair too: without a rule the UA paints an
   anchor in its own blue, which no theme measured. */
.doc { padding: 24px 0 40px; }
.doc-link {
  color: var(--on-backing);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--dim-on-backing);
}
.doc-link:active { color: var(--dim-on-backing); }

/* ---------------------------------------------------------------------------
   THE GAME PAGE. Sections are .card, so every theme's fill, edge, keyline and
   on-surface pair apply with no new tokens; the section labels are the
   gameday's .gd-label, on the card. The tap target that opens it is any
   gameday slide, so those show a pointer.
--------------------------------------------------------------------------- */
.gp-page { display: flex; flex-direction: column; gap: 12px; padding: 0 8px; }
#gameday [data-game-pk] { cursor: pointer; }
.gp-back {
  display: inline-block;
  margin-bottom: 8px;
  font-size: 13px;
  font-weight: 600;
  color: var(--gold-ink);
  text-decoration: none;
}
.gp-team { display: flex; align-items: baseline; gap: 10px; padding: 4px 0; }
.gp-abbrev { font-family: 'Plus Jakarta Sans', sans-serif; font-weight: 800; font-size: 15px; width: 44px; color: var(--dim-on-surface); }
.gp-name { flex: 1; font-size: 16px; color: var(--text-on-surface); }
.gp-us .gp-name, .gp-us .gp-abbrev { color: var(--text-on-surface); font-weight: 700; }
.gp-score { font-family: 'Plus Jakarta Sans', sans-serif; font-size: 26px; font-weight: 800; color: var(--text-on-surface); font-variant-numeric: tabular-nums; }
.gp-scoring { list-style: none; margin: 0; padding: 0; }
.gp-play {
  display: grid;
  grid-template-columns: 34px 1fr auto;
  gap: 8px;
  align-items: baseline;
  padding: 7px 0;
  border-top: 1px solid var(--card-rule);
  font-size: 14px;
  line-height: 1.4;
  color: var(--text-on-surface);
}
.gp-play:first-child { border-top: none; }
.gp-half { font-family: 'Plus Jakarta Sans', sans-serif; font-weight: 700; font-size: 12px; color: var(--dim-on-surface); }
.gp-tally { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--gold-ink); }
.gp-person { display: flex; align-items: center; gap: 10px; padding: 6px 0; color: var(--text-on-surface); font-size: 14px; }
.gp-shot { flex: none; }
.gp-role { width: 30px; font-family: 'Plus Jakarta Sans', sans-serif; font-weight: 800; font-size: 12px; color: var(--gold-ink); }
.gp-who { flex: 1; font-weight: 600; }
.gp-stat { color: var(--dim-on-surface); font-variant-numeric: tabular-nums; white-space: nowrap; }
.gp-teamlabel { margin: 8px 0 2px; font-family: 'Plus Jakarta Sans', sans-serif; font-weight: 800; font-size: 12px; letter-spacing: 0.06em; color: var(--dim-on-surface); }
.gp-info { display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; margin: 0; font-size: 14px; }
.gp-info dt { color: var(--dim-on-surface); }
.gp-info dd { margin: 0; color: var(--text-on-surface); }
/* THE CHART. The line is the accent ink (a graphic, 3:1); the midline is the
   card's hairline; the labels are the card's ink via currentColor so that the
   ground audit's colour reading IS the paint. No gradient, nothing encoded by
   colour alone - the labels say which side is which. */
.wp-chart { display: block; width: 100%; height: auto; margin-top: 4px; }
.wp-line { stroke: var(--gold-ink); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.wp-mid { stroke: var(--border-on-surface); stroke-width: 1; stroke-dasharray: 3 3; }
.wp-label { color: var(--text-on-surface); fill: currentColor; font-family: 'Plus Jakarta Sans', sans-serif; font-size: 11px; font-weight: 700; }

/* THE TABS - Game / Box / Plays - sit at the foot of the header card, so
   their text is on the card's surface in every theme (never on the page
   ground, which on Sunday Camos is a pattern). The selected tab is the
   card's ink with the accent as an underline; the others are the dim ink.
   Panels are stacks of cards like the page itself. */
.gp-tabs { display: flex; margin-top: 10px; border-top: 1px solid var(--card-rule); }
.gp-tab {
  flex: 1;
  min-height: 44px;
  padding: 0;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--dim-on-surface);
  cursor: pointer;
}
.gp-tab[aria-selected="true"] { color: var(--text-on-surface); border-bottom-color: var(--gold-ink); }
.gp-panel { display: flex; flex-direction: column; gap: 12px; }
.gp-empty { color: var(--dim-on-surface); font-size: 14px; }
.gp-unavailable .gp-empty { margin: 0; }

/* PLAY-BY-PLAY. Each half-inning is a disclosure in the box score's style:
   a 44px summary row with the half and team on the left, the linescore's
   line for it on the right, and a caret that turns. Rows are the scoring
   summary's type and rule; the events MLB recorded inside a plate
   appearance (a pitching change, a wild pitch) are a smaller dim line above
   the play they preceded; a scoring play is bold and carries the score. */
.pp-half { border-top: 1px solid var(--card-rule); }
.pp-half:first-of-type { border-top: none; }
.pp-halfsummary {
  min-height: 44px;
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--text-on-surface);
  cursor: pointer;
  list-style: none;
}
.pp-halfsummary::-webkit-details-marker { display: none; }
.pp-halfname { flex: 1; font-family: 'Plus Jakarta Sans', sans-serif; font-weight: 700; font-size: 13px; }
.pp-halfline { font-size: 12px; color: var(--dim-on-surface); font-variant-numeric: tabular-nums; white-space: nowrap; }
.pp-halfsummary::after {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  margin-right: 2px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 150ms ease;
}
.pp-half[open] .pp-halfsummary::after { transform: rotate(-135deg) translate(-1px, -1px); }
@media (prefers-reduced-motion: reduce) {
  .pp-halfsummary::after { transition: none; }
}
.pp-list { list-style: none; margin: 0; padding: 0 0 6px; }
.pp-play {
  padding: 6px 0;
  border-top: 1px solid var(--card-rule);
  font-size: 14px;
  line-height: 1.4;
  color: var(--text-on-surface);
}
.pp-event { padding: 1px 0 3px; font-size: 13px; color: var(--dim-on-surface); }
.pp-main { display: flex; align-items: baseline; gap: 10px; }
.pp-line { flex: 1; }
.pp-scoring .pp-line { font-weight: 700; }
.pp-score { font-weight: 700; color: var(--text-on-surface); font-variant-numeric: tabular-nums; white-space: nowrap; }
.pp-now .pp-line { color: var(--dim-on-surface); }

/* THE FULL BOX SCORE on the Box tab reuses the Stats tile's tables and the
   gameday box's team switch, label and substitute indent. What is new: a
   role after a name (the position, or PH / PR, or the decision after a
   pitcher), and a Total row in the card's ink. */
.gp-boxside { min-width: 0; }
.gp-pos { margin-left: 6px; font-size: 11px; font-weight: 400; color: var(--dim-on-surface); }
.gp-total td { font-weight: 700; color: var(--text-on-surface); border-top: 1px solid var(--border-on-surface); }
.gp-notes { margin-top: 2px; font-size: 13px; }
/* ONE COLOUR PER SIZE IN THE BOX SCORE (2026-09-26): the Total row's numbers
   are 13px, the numbers' size, so they take the numbers' dim and say "total"
   by weight and the rule above them; a note's value is 14px - the box score's
   size for its full ink - beside its dim 13px label. */
.gp-box .gp-total td:not(.ref-name) { color: var(--dim-on-surface); }
.gp-box .gp-notes dd { font-size: 14px; }

/* ---------------------------------------------------------------------------
   THE FIRST-RUN PRIMER AND THE ALERTS ROW (primer.js).

   The sheet is a .card fixed to the foot of the screen under a scrim, so it
   takes every theme's card fill, edge, keyline and on-surface inks with no
   new tokens; its buttons are the Settings page's own. The row sits on the
   page ground above the feed, so it wears the backing pair like the chip row
   - on Sunday Camos the page is a pattern and only a painted ground counts.
   Both are in index.html hidden, and the ground audit walks them like any
   other surface.
   --------------------------------------------------------------------------- */
.primer-scrim { position: fixed; inset: 0; z-index: 40; background: rgba(0, 0, 0, 0.45); }
.primer {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: max(12px, env(safe-area-inset-bottom));
  z-index: 41;
  max-width: 480px;
  margin-inline: auto;
  padding: 18px 16px;
}
.primer-title { margin: 0 0 8px; font-family: 'Plus Jakarta Sans', sans-serif; font-size: 20px; font-weight: 800; color: var(--text-on-surface); }
.primer-body { margin: 0 0 10px; font-size: 15px; line-height: 1.45; color: var(--text-on-surface); }
/* Doubled up on purpose: .card p (0,1,1) sets the dim ink and 14px on
   every paragraph in a card and out-ranks a lone class here - the 15px
   above never applied until this rule, measured at 375x667. */
.primer .primer-body { font-size: 15px; color: var(--text-on-surface); }
.primer .primer-note { font-size: 13px; color: var(--dim-on-surface); }
.primer-actions { display: flex; gap: 10px; margin-top: 12px; }
.primer-actions .push-btn { flex: 1; min-height: 44px; }

.push-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 8px 12px;
  padding: 8px 6px 8px 14px;
  border-radius: 10px;
  background: var(--backing);
  color: var(--on-backing);
  box-shadow: var(--keyline);
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.35;
}
.push-row-text { flex: 1; }
.push-row-btn {
  flex: none;
  min-height: 36px;
  padding: 0 14px;
  border: 0;
  border-radius: 999px;
  background: var(--gold);
  color: var(--on-gold);
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}
.push-row-x {
  flex: none;
  width: 36px;
  height: 36px;
  border: 0;
  background: transparent;
  color: var(--dim-on-backing);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}

/* Every top-level card wears the keyline. Inner pills and rows do not: they
   sit on a card or a backing, which IS a colour, and lift off it. */
.hdr-status, .offline-banner { box-shadow: var(--keyline); }
.filters { background: var(--backing); box-shadow: var(--keyline); }

/* ---------------------------------------------------------------------------
   THE STORY PAGE (#story/<id>, 2026-09-14).

   FULL SCREEN: the home header and its menu, the tiles, the chips, the alerts
   row, the footer and the settings sections step aside, and the page brings
   its own band - back, the theme's lockup, the overflow menu - in the home
   header's tokens. The story itself is a .card, so it takes each theme's card
   fill, edge and keyline, and every colour below is an existing token.
--------------------------------------------------------------------------- */
body.story-open .header,
body.story-open #hdr-menu-panel,
body.story-open .reference-tiles,
body.story-open .filters,
body.story-open .push-row,
body.story-open .site-footer,
body.story-open .about { display: none !important; }

.story-page { padding-bottom: 32px; }
.story-head {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr) 44px;
  align-items: center;
  gap: 4px;
  padding: 6px 12px;
  background: var(--header-band);
  border-bottom: 1px solid var(--border);
}
.story-wordmark { justify-self: center; display: block; width: auto; height: 40px; max-width: 100%; }
.story-wordmark-alt { justify-self: center; font-family: 'Plus Jakarta Sans', sans-serif; font-size: 16px; font-weight: 800; color: var(--on-header-band); }

.story-card { width: min(100% - 32px, 608px); margin: 16px auto 0; }
.story-card:active { transform: none; }
.story-card .meta { flex-wrap: wrap; }
/* Share: a 44px control at the end of the meta row, shown only where the
   platform can share (canShareStory). */
.story-share {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 0 14px;
  border: 1px solid var(--border-on-surface);
  border-radius: 999px;
  background: transparent;
  color: var(--text-on-surface);
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}
.story-share svg { width: 16px; height: 16px; flex: none; }

.story-lede { display: flex; align-items: flex-start; gap: 14px; }
.story-portrait {
  flex: none;
  width: 88px;
  height: 110px;
  border-radius: 12px;
  object-fit: cover;
  background: var(--surface-raised);
}
/* THE TILE WITH HIS INITIALS (1.3), the photo over them - never an empty tile. */
span.story-portrait { position: relative; display: block; overflow: hidden; }
.story-portrait-initials {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-family: 'Plus Jakarta Sans', sans-serif; font-size: 22px; font-weight: 700;
  color: var(--dim-on-surface);
}
span.story-portrait .story-portrait-img { position: relative; display: block; width: 100%; height: 100%; object-fit: cover; }
/* The portrait as a link to his page: the link takes the portrait's place in
   the row and its shape, and adds nothing to look at. */
.story-portrait-link { flex: none; display: block; line-height: 0; border-radius: 12px; }
/* His name, above the chips: the Roster link (.row-link) on the card's text. */
.story-player { margin: 12px 0 0; font-size: 15px; font-weight: 700; color: var(--text-on-surface); }
.story-player + .story-chips { margin-top: 8px; }
.story-words { flex: 1; min-width: 0; }
.story-headline {
  margin: 0 0 8px;
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 22px;
  font-weight: 800;
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--text-on-surface);
  overflow-wrap: anywhere;
}
.story-card .story-summary { margin: 0; }

.story-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0 0; padding: 0; list-style: none; }
.story-chip {
  padding: 4px 10px;
  border: 1px solid var(--border-on-surface);
  border-radius: 999px;
  background: var(--surface-raised);
  color: var(--text-on-surface);
  font-size: 12px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* The rail: the card's source control, open, and the thing that folds the
   timeline away. */
.story-rail {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 44px;
  margin-top: 16px;
  padding: 6px 10px;
  background: transparent;
  border: 1px solid var(--gold-edge);
  border-radius: 12px;
  cursor: pointer;
}
.story-rail[aria-expanded="true"] .src-chevron { transform: rotate(-135deg) translate(-1px, -1px); }

/* THE TIMELINE: a rail line, a dot per report, newest at the top. */
.story-timeline { list-style: none; margin: 12px 0 0; padding: 0; }
.tl-item { position: relative; padding-left: 22px; }
.tl-item::before { content: ""; position: absolute; left: 5px; top: 0; bottom: 0; border-left: 2px solid var(--card-rule); }
.tl-item:first-child::before { top: 18px; }
.tl-item:last-child::before { bottom: auto; height: 18px; }
.tl-item:only-child::before { display: none; }
.tl-dot {
  position: absolute;
  left: 0;
  top: 13px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--dim-on-surface);
  box-shadow: 0 0 0 2px var(--surface);
}
.tl-row {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-height: 44px;
  padding: 8px 26px 10px 0;
  border-bottom: 1px solid var(--card-rule);
  color: var(--text-on-surface);
  text-decoration: none;
}
.tl-item:last-child .tl-row { border-bottom: 0; }
.tl-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; }
.tl-who { font-size: 14px; font-weight: 700; color: var(--text-on-surface); }
/* 12px (1.3): the timeline's dim size, as the gap beside it and the card's
   outlet. At 12.5px it was a second colour beside the stated time. */
.tl-outlet { font-size: 12px; color: var(--dim-on-surface); }
.tl-when { font-size: 12.5px; font-weight: 600; color: var(--text-on-surface); font-variant-numeric: tabular-nums; }
/* The stated time's ink (1.3), as on the card: at 12.5px the timeline has one
   colour. The italic and the words "picked up" say whose clock it is. */
.tl-when-ours { font-weight: 400; font-style: italic; }
.tl-gap { font-size: 12px; color: var(--dim-on-surface); }
.tl-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 2px; }
/* The two ends, in gold: the card's accent fill with its own ink. */
.tl-end {
  padding: 2px 7px;
  border-radius: 6px;
  background: var(--gold);
  color: var(--on-gold);
  /* 11px (1.3), a size nothing else in the timeline uses: at 10.5px the gold
     pill's ink sat beside the grey role words - two colours on one size. The
     fill still marks the two ends; the words say which. */
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.tl-chev {
  position: absolute;
  right: 6px;
  top: 50%;
  width: 8px;
  height: 8px;
  border-top: 2px solid var(--dim-on-surface);
  border-right: 2px solid var(--dim-on-surface);
  transform: translateY(-50%) rotate(45deg);
}

/* Loading, or a story that is not there: on the page, so it wears the backing. */
.story-empty {
  width: min(100% - 32px, 608px);
  margin: 24px auto;
  padding: var(--backing-pad);
  background: var(--backing);
  border-radius: 12px;
  box-shadow: var(--keyline);
  color: var(--dim-on-backing);
  font-size: 15px;
  text-align: center;
}
