/* Ribbie — shared styles for the admin and the public site.
   Mobile-first: base rules target a phone, media queries add width.
   No build step, no framework. */

/* Self-hosted, latin-subset only (this is an English-only site) — see
   /fonts/. "Ballpark" typographic system (2026-08 redesign, superseding the
   earlier Barlow-based one): Bevan is the display face (league name, game
   times, scores, h1/h2 — see --font-display below, never used under ~15px,
   it is a heavy slab serif with only one weight and no bold), Archivo is
   body/UI copy, IBM Plex Mono is the "kicker" face (dates, uppercase labels,
   monograms, table headers). Archivo ships from Google as a single variable
   file covering weights 400-700 — one @font-face per weight, same file,
   is the standard (and correct) way to serve it: real browsers read the
   font's own variation axis to pick the right weight even though each rule
   below declares a single fixed font-weight rather than a range. */
@font-face { font-family: "Bevan"; font-style: normal; font-weight: 400; font-display: swap; src: url("/fonts/bevan-400-v2.woff2") format("woff2"); }
@font-face { font-family: "Archivo"; font-style: normal; font-weight: 400; font-display: swap; src: url("/fonts/archivo-variable-v2.woff2") format("woff2"); }
@font-face { font-family: "Archivo"; font-style: normal; font-weight: 500; font-display: swap; src: url("/fonts/archivo-variable-v2.woff2") format("woff2"); }
@font-face { font-family: "Archivo"; font-style: normal; font-weight: 600; font-display: swap; src: url("/fonts/archivo-variable-v2.woff2") format("woff2"); }
@font-face { font-family: "Archivo"; font-style: normal; font-weight: 700; font-display: swap; src: url("/fonts/archivo-variable-v2.woff2") format("woff2"); }
@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400; font-display: swap; src: url("/fonts/ibm-plex-mono-400-v2.woff2") format("woff2"); }
@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 500; font-display: swap; src: url("/fonts/ibm-plex-mono-500-v2.woff2") format("woff2"); }
@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 600; font-display: swap; src: url("/fonts/ibm-plex-mono-600-v2.woff2") format("woff2"); }

:root {
  /* Raw palette layer — a league's chosen color scheme (see data-palette
     blocks below) lives here. Defaults below are the "sandlot" look (the
     redesign's own palette, and the system default — see js/lib/ai.js's
     DEFAULT_THEME), used verbatim on any page that doesn't carry a
     data-palette attribute. Each palette below defines its OWN light *and*
     dark neutrals (--pal-*-dark) — deliberately not one shared dark ramp
     for every palette, so a league's paper/ink identity survives into dark
     mode instead of every league going the same charcoal at night. */
  --pal-bg: #E4DCCB; --pal-surface: #FDFAF3; --pal-ink: #221D18; --pal-ink-2: #4A4038;
  --pal-muted: #8A7C6D; --pal-line: #D8CDB8;
  --pal-accent: #1E4FD8; --pal-accent-2: #16389A; --pal-onaccent: #FFFFFF; --pal-tint: #EEF1FC;
  --pal-bg-dark: #191510; --pal-surface-dark: #241E18; --pal-ink-dark: #F2EADC;
  --pal-line-dark: #3A3128; --pal-muted-dark: #A2937F;

  /* Semantic layer — every existing rule in this file reads these, not
     --pal-*, so the palette/theme system composes underneath without
     touching component CSS. */
  --bg: var(--pal-bg);
  --surface: var(--pal-surface);
  --border: var(--pal-line);
  --text: var(--pal-ink);
  --ink-2: var(--pal-ink-2);
  --muted: var(--pal-muted);
  --accent: var(--pal-accent);
  --accent-text: var(--pal-onaccent);
  --positive: #2F6B3F;
  --danger: #9D3B25;
  --warn-bg: #F6EFDB;
  --warn-border: #D9C48A;
  --warn-text: #6B5320;

  --radius: 8px;
  --radius-sm: 6px;
  --radius-lg: 14px;
  --gap: 1rem;
  --maxw: 60rem;
  --space-1: .25rem;
  --space-2: .5rem;
  --space-3: .75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;

  --font-display: "Bevan", Georgia, serif; /* display only — never below ~15px, no bold */
  --font-body: "Archivo", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-head: var(--font-display); /* kept as an alias — existing rules read --font-head */
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: var(--pal-bg-dark);
    --surface: var(--pal-surface-dark);
    --border: var(--pal-line-dark);
    --text: var(--pal-ink-dark);
    --ink-2: var(--pal-ink-dark);
    --muted: var(--pal-muted-dark);
    /* Accent stays tied to the league's chosen palette even in dark mode —
       lightened toward white so it holds contrast on a dark surface —
       rather than falling back to a fixed color unrelated to the palette. */
    --accent: color-mix(in srgb, var(--pal-accent) 82%, white);
    --accent-text: #0b1f16;
    --positive: color-mix(in srgb, #2F6B3F 78%, white);
    --danger: color-mix(in srgb, #9D3B25 74%, white);
    --warn-bg: color-mix(in srgb, var(--pal-surface-dark) 70%, #6B5320);
    --warn-border: #8a7412;
    --warn-text: #E8D9A8;
  }
}

/* Per-league color palette (admin setting, see leagues.settings_json.theme /
   js/lib/ai.js's parseThemeSettings). Server-rendered as a static
   data-palette attribute on <html> in renderShell() — no flash-of-unstyled-
   color like the client-set data-theme below, since it's correct on first
   byte. Overrides the raw --pal-* layer (light AND dark neutrals, plus
   accent) as one unit; light/dark selection itself stays a fully separate,
   composable concern (see :root[data-theme] below). */
:root[data-palette="crimson"] {
  --pal-bg: #E9DFD6; --pal-surface: #FDF9F5; --pal-ink: #1F1512; --pal-ink-2: #4A3A34;
  --pal-muted: #8C7A70; --pal-line: #DBCCC0;
  --pal-accent: #BD3039; --pal-accent-2: #0C2340; --pal-onaccent: #FFFFFF; --pal-tint: #F7E5E6;
  --pal-bg-dark: #1A100D; --pal-surface-dark: #251916; --pal-ink-dark: #F4E9E3;
  --pal-line-dark: #3D2E28; --pal-muted-dark: #A99183;
}
:root[data-palette="pinstripe"] {
  --pal-bg: #E3E4E8; --pal-surface: #FAFBFC; --pal-ink: #14181F; --pal-ink-2: #3B4250;
  --pal-muted: #7A828F; --pal-line: #CDD1D8;
  --pal-accent: #0C2340; --pal-accent-2: #2E3A4B; --pal-onaccent: #FFFFFF; --pal-tint: #E4E8EF;
  --pal-bg-dark: #10141A; --pal-surface-dark: #1A2029; --pal-ink-dark: #E7EBF1;
  --pal-line-dark: #2C3541; --pal-muted-dark: #939EAD;
}
:root[data-palette="royal"] {
  --pal-bg: #DFE6ED; --pal-surface: #FAFCFE; --pal-ink: #10202F; --pal-ink-2: #35485B;
  --pal-muted: #74869A; --pal-line: #C8D4E0;
  --pal-accent: #005A9C; --pal-accent-2: #0B1B2E; --pal-onaccent: #FFFFFF; --pal-tint: #E1EDF8;
  --pal-bg-dark: #0C1520; --pal-surface-dark: #16212E; --pal-ink-dark: #E4EDF5;
  --pal-line-dark: #27364A; --pal-muted-dark: #8B9DB0;
}
:root[data-palette="sandlot"] {
  --pal-bg: #E4DCCB; --pal-surface: #FDFAF3; --pal-ink: #221D18; --pal-ink-2: #4A4038;
  --pal-muted: #8A7C6D; --pal-line: #D8CDB8;
  --pal-accent: #1E4FD8; --pal-accent-2: #16389A; --pal-onaccent: #FFFFFF; --pal-tint: #EEF1FC;
  --pal-bg-dark: #191510; --pal-surface-dark: #241E18; --pal-ink-dark: #F2EADC;
  --pal-line-dark: #3A3128; --pal-muted-dark: #A2937F;
}
:root[data-palette="dugout"] {
  --pal-bg: #DFE5DE; --pal-surface: #FAFCF9; --pal-ink: #131C17; --pal-ink-2: #37453D;
  --pal-muted: #78867D; --pal-line: #C9D3C9;
  --pal-accent: #00584A; --pal-accent-2: #12251E; --pal-onaccent: #FFFFFF; --pal-tint: #E1EFE8;
  --pal-bg-dark: #0E140F; --pal-surface-dark: #18211B; --pal-ink-dark: #E6EEE7;
  --pal-line-dark: #29352C; --pal-muted-dark: #8FA093;
}

/* Explicit Light/Dark override for the public site's theme toggle (see
   themeToggle() in functions/_public.js and js/ui.js). The media query
   above is what "System" means; these win over it whenever data-theme is
   set, in either direction — a user who picks Light on a dark-mode OS must
   actually get light, and vice versa. Both branches read the SAME per-
   palette raw tokens the system-preference block above does (light vs.
   -dark), so a league's own paper/ink still composes with an explicit
   choice — declared after the data-palette blocks above for that reason. */
:root[data-theme="light"] {
  --bg: var(--pal-bg); --surface: var(--pal-surface); --border: var(--pal-line);
  --text: var(--pal-ink); --ink-2: var(--pal-ink-2); --muted: var(--pal-muted);
  --accent: var(--pal-accent); --accent-text: var(--pal-onaccent);
  --positive: #2F6B3F; --danger: #9D3B25; --warn-bg: #F6EFDB; --warn-border: #D9C48A; --warn-text: #6B5320;
}
:root[data-theme="dark"] {
  --bg: var(--pal-bg-dark); --surface: var(--pal-surface-dark); --border: var(--pal-line-dark);
  --text: var(--pal-ink-dark); --ink-2: var(--pal-ink-dark); --muted: var(--pal-muted-dark);
  --accent: color-mix(in srgb, var(--pal-accent) 82%, white); --accent-text: #0b1f16;
  --positive: color-mix(in srgb, #2F6B3F 78%, white); --danger: color-mix(in srgb, #9D3B25 74%, white);
  --warn-bg: color-mix(in srgb, var(--pal-surface-dark) 70%, #6B5320); --warn-border: #8a7412; --warn-text: #E8D9A8;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 16px/1.5 var(--font-body);
  -webkit-text-size-adjust: 100%;
}

h1, h2, h3 { font-family: var(--font-display); font-weight: 400; line-height: 1.1; margin: 0 0 .5rem; }
h1 { font-size: 1.9rem; }
h2 { font-size: 1.35rem; }
h3 { font-size: 1.05rem; }
p { margin: 0 0 .75rem; }
a { color: var(--accent); }

/* The mono "kicker" convention — used throughout the redesign for dates,
   uppercase section labels, and anything that reads as metadata rather
   than content (see gameCard()/resultCard() in functions/_public.js). */
.kicker {
  font-family: var(--font-mono); font-weight: 500; font-size: .66rem;
  letter-spacing: .14em; text-transform: uppercase; color: var(--muted);
}

/* ── layout ─────────────────────────────────────────────────────────── */

/* The topbar — a dark ink bar on BOTH the public site and the admin SPA
   (admin.html's #topbar carries the same .topbar class), matching real
   feedback that the two apps needed one shared look, not two. Setting
   `color` here once means every child (nav pills, the theme/AI toggles,
   even the SVG icons via currentColor) inherits a readable tone against
   the dark bar with no per-component override needed, except the specific
   states below that need to stay legible against an ACCENT-filled pill. */
.topbar {
  display: flex;
  align-items: stretch;
  flex-wrap: wrap;
  gap: 0;
  padding: 0 1.1rem;
  background: var(--pal-ink);
  color: var(--pal-surface);
  border-bottom: none;
  position: sticky;
  top: 0;
  z-index: 10;
}

/* A "Jump to today" link (public schedule/team pages, admin games list)
   scrolls its #today target flush to the top of the viewport by default —
   but .topbar is sticky right there with an opaque background, so the
   target landed hidden underneath it and the jump looked like it did
   nothing. scroll-margin-top leaves room above the target for the topbar
   (~3.7rem tall) plus a little breathing room. */
#today { scroll-margin-top: 4.2rem; }

.topbar .brand,
.topbar-brand {
  display: flex; align-items: center; gap: .6rem;
  padding: .8rem 0; color: inherit; text-decoration: none; font-weight: 700; letter-spacing: -.02em;
}
.topbar .brand span { color: color-mix(in srgb, var(--pal-surface) 65%, transparent); font-weight: 400; font-size: .85rem; }
/* The admin SPA's own brand — no per-league monogram badge here (unlike
   the public site's .topbar-brand), since one admin session spans every
   league, not just one. Bevan for the wordmark alone keeps the same
   typographic voice as the public site's league name. */
.topbar .brand a { font-family: var(--font-display); font-weight: 400; font-size: 1.05rem; }

/* Admin topbar's plain text links (Help, What's new, View public site,
   Sign out) and the public shell's non-.topbar-nav links all need to read
   against the dark ink bar — the global `a { color: var(--accent) }` rule
   (styles.css, way below) would otherwise put a saturated per-league
   accent color on a dark bar meant to read as neutral chrome. */
.topbar a.small,
.topbar button.link,
.topbar span.small.muted {
  /* .topbar is `align-items: stretch`, so every loose child (renderShell()
     in js/admin/app.js appends these directly, with no grouping wrapper —
     unlike .topbar-nav/.topbar-actions, which center their OWN content)
     stretches to the full bar height. A plain <a>/<span>'s text then sits
     at the top of that stretched box by default, while a <button>'s does
     NOT — browsers vertically center a button's content by default — so
     "Sign out" (a button) and "jess" (a span) landed on two different
     baselines even though they're adjacent siblings ("jess" noticeably
     higher than "Sign out", a real bug from a real screenshot). Making
     every one of these a centered flex box itself is what keeps them
     level regardless of the underlying element type. */
  display: flex;
  align-items: center;
}
.topbar a.small,
.topbar button.link {
  color: color-mix(in srgb, var(--pal-surface) 80%, transparent);
  /* .topbar itself is `gap: 0` (its grouped children — .brand,
     .topbar-nav, .topbar-actions — each handle their OWN internal
     spacing already). The admin shell's topbar has no such grouping
     wrapper — renderShell() (js/admin/app.js) appends these loose links
     directly — so with no gap anywhere they render flush against each
     other ("HelpWhat's new", real bug from a real screenshot). Each one
     carries its own trailing margin instead. */
  margin-right: .75rem;
}
.topbar a.small:hover,
.topbar button.link:hover {
  color: var(--pal-surface);
}
/* Same reasoning as above, for the admin topbar's brand wordmark and the
   signed-in user's name — both sit as loose .topbar children too. */
.topbar > .brand,
.topbar span.small.muted {
  margin-right: .75rem;
}
.topbar .theme-toggle {
  margin-right: .75rem;
}

/* The circular league-monogram badge — leagueMonogram() in js/lib/format.js.
   Filled with the league's own accent, same visual language as a team badge
   but round and a size up, since this is the ONE league-level identity mark
   rather than one of several team ones. */
.topbar-monogram {
  width: 2.1rem; height: 2.1rem; flex: none; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--accent-text);
  font-family: var(--font-display); font-weight: 400; font-size: .9rem;
}
.topbar-name-block { display: flex; flex-direction: column; gap: .05rem; line-height: 1.15; }
.topbar-name { font-family: var(--font-display); font-weight: 400; font-size: .95rem; letter-spacing: .005em; }
.topbar-kicker { color: color-mix(in srgb, var(--pal-surface) 62%, transparent); }

.topbar .spacer { margin-left: auto; }

/* Nav pills — the active section gets an accent-filled pill (replacing the
   old inline `style="font-weight:700"` signal) instead of a plain color/
   weight change, which read too quietly against a dark bar. `.topbar-links`
   is kept as the class the mobile media query below hides — same name the
   codebase already used for "the primary nav, absent on a phone in favor
   of the bottom tab bar," just applied to a <nav> of pills now instead of a
   bare list of <a>s. */
.topbar-nav { display: flex; gap: .1rem; align-items: center; margin-left: auto; }
.topbar-nav a {
  padding: .5rem .75rem; border-radius: 5px; font-size: .8rem; font-weight: 500;
  color: color-mix(in srgb, var(--pal-surface) 80%, transparent); text-decoration: none;
}
.topbar-nav a:hover { color: var(--pal-surface); }
.topbar-nav a.active { background: var(--accent); color: var(--accent-text); font-weight: 600; }

.topbar-actions {
  display: flex; align-items: center; gap: .55rem;
  padding: 0 0 0 1rem; margin-left: .75rem;
  border-left: 1px solid color-mix(in srgb, var(--pal-surface) 18%, transparent);
}
.topbar-manager-link {
  font-size: .74rem; font-weight: 500; white-space: nowrap;
  color: color-mix(in srgb, var(--pal-surface) 78%, transparent); text-decoration: none;
}
.topbar-manager-link:hover { color: var(--pal-surface); }

/* Public-site theme toggle (functions/_public.js themeToggle(), and the
   admin SPA's own js/ui.js themeToggle() — same markup, same class, one
   set of rules for both) — three icon buttons (system/light/dark) instead
   of a text <select>: "Light" spelled out in a page corner read as
   meaningless to a real user, where sun/moon/monitor glyphs read at a
   glance like the weather icons elsewhere on the site. aria-pressed (set
   by THEME_SELECT_SCRIPT / js/ui.js's paintTheme()) is the one source of
   truth for which choice is active — this just styles it. Border/text
   color come from the inherited --pal-surface set on .topbar above; this
   works identically wherever .topbar lives light or dark, since it never
   hardcodes a color of its own except the pressed state below. */
.theme-toggle {
  display: flex; gap: .15rem; border-radius: 8px; padding: .15rem;
  border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
}
.theme-toggle button {
  border: 0; background: none; color: inherit; border-radius: 6px; padding: .2rem .4rem;
  font-size: .9rem; line-height: 1.4; cursor: pointer;
}
.theme-toggle button[aria-pressed="true"] { background: var(--accent); color: var(--accent-text); }

/* AI-summary toggle (functions/_public.js's aiSummaryToggle()) — a single
   text+icon button, not an icon-only group like .theme-toggle, since "on"
   vs "off" needs to actually say so: an icon alone can't carry that meaning
   the way sun/moon/monitor can for a theme choice. aria-pressed="true" means
   summaries are OFF (the button reflects "is this override active"), and
   gets the same active-state treatment as the theme buttons for consistency. */
.ai-toggle {
  display: flex; align-items: center; gap: .35rem;
  border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
  border-radius: 8px; background: none;
  padding: .2rem .5rem; font-size: .8rem; line-height: 1.4; cursor: pointer; color: inherit;
}
.ai-toggle[aria-pressed="true"] { opacity: .6; }
.ai-toggle span { white-space: nowrap; }
@media (max-width: 640px) { .ai-toggle span { display: none; } }

main { max-width: var(--maxw); margin: 0 auto; padding: 1rem; }

/* Admin left rail (admin.html, js/admin/app.js's renderRail()) — replaces
   the season page's old inline "Teams · Fees · Season dates" anchor row
   with something reachable from every admin page. `.admin-layout` wraps
   `#rail` + `#root`; `#root` overrides the general `main` rule above (it's
   also a <main>) so it stops trying to center itself now that it has a
   sibling. */
.admin-layout {
  display: flex; align-items: flex-start; gap: 1.5rem;
  max-width: calc(var(--maxw) + 14rem + 1.5rem); margin: 0 auto; padding: 0 1rem;
}
/* Admin is denser than the public site by design: a season page alone can
   stack a dozen cards (Teams, Manager links, Fees, Season dates, ...),
   each with its own cardTitle() <h2> — Bevan (the display face) at every
   one of those turns the page shouty. Reserved here for page-level <h1>
   only (season/team/league names — still the global h1-h3 rule below);
   card and section headings fall back to Archivo bold instead. The
   public site, /score, and /ump are NOT inside .admin-layout, so their
   own card headings (Standings, Next up, Notices, ...) keep the full
   display voice this redesign is otherwise built around. */
.admin-layout h2, .admin-layout h3 {
  font-family: var(--font-body); font-weight: 700; letter-spacing: 0;
}
/* padding-bottom: 100vh guarantees ANY hash-anchored card can reach the top
   of the viewport (see scrollToHash() in js/admin/app.js) even when it's the
   last thing on a short page — without this, the browser simply can't
   scroll past the end of the real content, so a target near the bottom
   (e.g. #season-settings, always last on the season page) lands wherever
   the page happened to run out, not at the top like every other anchor. */
.admin-layout main { max-width: none; margin: 0; padding: 1rem 0 100vh; flex: 1 1 auto; min-width: 0; }
.admin-rail {
  flex: 0 0 13rem; display: flex; flex-direction: column; gap: .1rem;
  position: sticky; top: 3.5rem; padding: 1rem 0;
}
.admin-rail:empty { display: none; }
.admin-rail a {
  padding: .4rem .6rem; border-radius: 8px; color: var(--text); text-decoration: none; font-size: .9rem;
}
.admin-rail a:hover { background: var(--bg); }
.admin-rail a.active { background: var(--accent); color: var(--accent-text); font-weight: 600; }
/* The league/season name at the top of each rail group — a real link back
   to that page, not just a label (see renderRail() in js/admin/app.js), so
   there's always a stable way back even when the section below it is out of
   view or briefly empty. Qualified with the tag so it reliably beats the
   plain `.admin-rail a` color rule above regardless of source order. */
.admin-rail a.admin-rail-heading {
  display: block;
  font-size: .7rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
  margin: 1rem 0 .3rem; padding: 0 .6rem;
}
.admin-rail a.admin-rail-heading:first-child { margin-top: 0; }
.admin-rail a.admin-rail-heading:hover { background: none; color: var(--text); }
/* A heading is a label (and a way back), never a nav destination, so it must
   not take the accent pill even when its own href is the current route —
   landing on the season page otherwise wrapped the season's NAME in a solid
   green bar, which read as a rendering glitch rather than "you are here".
   The section links below it already carry that signal. */
.admin-rail a.admin-rail-heading.active {
  background: none; color: var(--muted); font-weight: 600;
}

/* Anchor links that jump to a section of the page named above them, rather
   than to a page of their own. Indented and marked so the rail reflects the
   real structure — a flat list gave no way to tell "its own screen" from
   "a card further down the page you're already on". */
.admin-rail a.admin-rail-sub {
  padding-left: 1.5rem; font-size: .85rem; color: var(--muted);
}
.admin-rail a.admin-rail-sub::before {
  content: '└ '; opacity: .5;
}
.admin-rail a.admin-rail-sub.active { color: var(--accent-text); }

@media (max-width: 55rem) {
  .admin-layout { flex-direction: column; padding: 0; }
  .admin-rail {
    flex-direction: row; align-items: center; overflow-x: auto; position: static; width: 100%;
    padding: .5rem 1rem; gap: .3rem; border-bottom: 1px solid var(--border);
  }
  .admin-rail a.admin-rail-heading { display: none; }
  .admin-rail a { white-space: nowrap; flex: none; }
  .admin-layout main { padding: 1rem; }
}

.crumbs { font-size: .85rem; color: var(--muted); margin-bottom: .75rem; }
.crumbs a { color: var(--muted); }

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem;
  margin-bottom: var(--gap);
}
.card > h2:first-child, .card > h3:first-child { margin-top: 0; }
/* Notable card — a left accent border in the league's own color, used for
   things worth a second look (the manager page's "scores to post" hero,
   an active notice), not for every card. Additive modifier, doesn't
   replace bare .card. Background is composed from the CURRENT surface +
   accent rather than the raw --pal-tint token — --pal-tint has no dark-
   mode counterpart (it was defined once per palette, light only), which
   read as a jarring light card dropped into an otherwise dark page the
   first time something actually used this class. color-mix against
   --surface is theme-aware for free in both directions, no new per-
   palette tokens needed. */
.card.accent { border-left: 3px solid var(--accent); background: color-mix(in srgb, var(--accent) 10%, var(--surface)); }

/* Team color + monogram, set once per team (see teamMonogram()/teamBadge()
   in js/lib/format.js and functions/_public.js). Falls back to the league's
   own accent color when a team has no color of its own, so an unbranded
   team still reads as part of the league rather than a random hue. Mono
   face, matching the redesign's convention that a team monogram is a
   kicker-style abbreviation, not a headline. */
.team-badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.5rem; height: 1.5rem; flex: none;
  margin-right: .5rem; vertical-align: middle;
  border-radius: var(--radius-sm); font-family: var(--font-mono); font-weight: 600;
  font-size: .7rem; line-height: 1; letter-spacing: 0;
  color: #fff; background: var(--accent);
}

.row { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
.row.end { justify-content: flex-end; }
.grow { flex: 1 1 auto; }
.muted { color: var(--muted); }
.small { font-size: .85rem; }
.stack > * + * { margin-top: .75rem; }

/* ── forms ──────────────────────────────────────────────────────────── */

label { display: block; font-size: .85rem; font-weight: 600; margin-bottom: .25rem; }

input, select, textarea, button {
  font: inherit;
  color: inherit;
}

input[type=text], input[type=email], input[type=password], input[type=date],
input[type=time], input[type=number], input[type=url], input[type=search], select, textarea {
  width: 100%;
  padding: .55rem .65rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
}
textarea { min-height: 5rem; resize: vertical; }

input:focus, select:focus, textarea:focus, button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.field { margin-bottom: .75rem; }
.field .err { color: var(--danger); font-size: .8rem; margin-top: .25rem; }
.field.bad input, .field.bad select { border-color: var(--danger); }

.fields2 { display: grid; gap: .75rem; }
@media (min-width: 40rem) { .fields2 { grid-template-columns: 1fr 1fr; } }

button {
  padding: .55rem .9rem;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface);
  cursor: pointer;
}
button:hover { border-color: var(--muted); }
button[disabled] { opacity: .55; cursor: not-allowed; }
button.primary { background: var(--accent); color: var(--accent-text); border-color: transparent; font-weight: 600; }
button.danger { color: var(--danger); }
button.link { border: 0; background: none; color: var(--accent); padding: .25rem; text-decoration: underline; }

/* ── tables ─────────────────────────────────────────────────────────── */

.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table { width: 100%; border-collapse: collapse; font-size: .95rem; }
th, td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--border); }
th { font-family: var(--font-mono); font-weight: 500; font-size: .7rem; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
td.num { font-family: var(--font-mono); font-weight: 600; }
tr:last-child td { border-bottom: 0; }

/* ── misc ───────────────────────────────────────────────────────────── */

.badge {
  display: inline-block; padding: .1rem .45rem; border-radius: 99px;
  font-size: .75rem; border: 1px solid var(--border); color: var(--muted);
}
.badge.on { background: var(--accent); color: var(--accent-text); border-color: transparent; }
/* A game overdue for a score — text and color both carry the meaning
   (see the weather-cell comment below on why color alone is never enough). */
.badge.warn { background: var(--warn-bg); border-color: var(--warn-border); color: var(--text); }
/* Doubleheader marker — paired with the .row-dh tint below on the same row,
   so a colorblind reader still has the "DH" text to go on. */
.badge.dh { border-color: var(--accent); color: var(--accent); }

/* A small rounded pill used on a game/result card (see below) for anything
   that would be a badge or a table column on the flat table view — DH,
   weather, location, "needs a score." Generic enough to reuse for all of
   them; the specific color comes from a modifier class or an inline style
   set by the caller (weatherChip() picks its own text based on status). */
.chip {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .2rem .55rem; border-radius: 99px; font-size: .72rem; line-height: 1.4;
  white-space: nowrap; border: 1px solid var(--border); color: var(--muted); background: var(--bg);
}
.chip.wx-heavy_recent_rain, .chip.wx-rain_likely { color: var(--text); }
.chip.chip-closure { color: var(--danger); border-color: var(--danger); background: transparent; }
.chip.chip-dh { border-color: var(--accent); color: var(--accent); }
.chip.chip-warn { background: var(--warn-bg); border-color: var(--warn-border); color: var(--warn-text); }

/* ── game cards — "Tonight and this week" on the home page ────────────
   The home page's treatment of an upcoming game; gamesTable() and its
   mobile stacked-card fallback are UNCHANGED and still used everywhere
   else (schedule/results/team pages, the missing-score card) — this is a
   second, additive presentation, not a replacement. */
.game-cards { display: grid; gap: .55rem; }
.game-card {
  display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); padding: .7rem .9rem;
}
.game-card-when { flex: 0 0 3.6rem; }
.game-card-time { font-family: var(--font-display); font-weight: 400; font-size: 1.25rem; line-height: 1.1; }
.game-card-divider { width: 1px; align-self: stretch; min-height: 2rem; background: var(--border); }
.game-card-teams { flex: 1 1 12rem; display: flex; align-items: center; gap: .6rem; min-width: 0; }
.game-card-team { display: inline-flex; align-items: center; gap: .4rem; min-width: 0; max-width: 11rem; }
.game-card-team .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.game-card-team .name.winner { font-weight: 600; }
.game-card-at { font-family: var(--font-mono); font-size: .68rem; color: var(--muted); }
.game-card-chips { display: flex; align-items: center; gap: .4rem; flex: 0 0 auto; margin-left: auto; }
@media (max-width: 30rem) {
  .game-card-divider { display: none; }
  .game-card-when { flex: 1 1 100%; }
  .game-card-chips { flex-basis: 100%; margin-left: 0; }
}

/* ── result cards — "Latest results" on the home page ──────────────────
   The ONE place fmtResultLine()'s winner-first exception is used visually
   (CLAUDE.md invariant 1) — the loser is muted, the winner's row is bold,
   both scores are shown so nothing is lost versus the visitor-first table
   everywhere else on the site. */
/* Shared by resultsCards() and gameCards() (functions/_public.js) — the day
   header above a run of same-date home-page cards. */
.day-kicker { margin: 1.1rem 0 .5rem; padding-bottom: .4rem; border-bottom: 1px solid var(--border); }
.day-kicker:first-child { margin-top: 0; }
.result-cards { display: grid; grid-template-columns: 1fr 1fr; gap: .65rem; }
@media (max-width: 30rem) { .result-cards { grid-template-columns: 1fr; } }
.result-card { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); padding: .65rem .8rem; }
.result-card-row { display: flex; align-items: center; justify-content: space-between; padding: .2rem 0; gap: .5rem; }
.result-card-row .team { display: inline-flex; align-items: center; gap: .4rem; min-width: 0; }
.result-card-row .team .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
.result-card-row .runs { font-family: var(--font-display); font-weight: 400; font-size: 1.3rem; color: var(--muted); }
.result-card-row.winner .team .name { color: var(--text); font-weight: 600; }
.result-card-row.winner .runs { color: var(--text); }
.result-card-foot { margin-top: .35rem; font-size: .72rem; color: var(--muted); }

/* Doubleheader legs get a faint tint down the row, in addition to the DH
   badge — see gameRow() in functions/_public.js. Deliberately a light tint
   of the league's own accent color rather than a new hue, so it reads as
   "notable" without competing with the weather/closure colors, which mean
   something more urgent. */
tr.row-dh > td { background: color-mix(in srgb, var(--accent) 8%, transparent); }
/* A scheduled/rescheduled game whose date has already passed — needs a
   score. Uses the same warning color as .badge.warn so the row and its
   badge read as one signal. */
tr.row-needs-score > td { background: color-mix(in srgb, var(--warn-border) 12%, transparent); }

/* Week/day header rows inserted by gamesTable()'s grouped mode — one real
   <table> still, so the existing .table-wrap horizontal scroll keeps
   working, this just breaks up a long season into scannable chunks the
   same way the admin games list already does (see CLAUDE.md). */
tr.group-row > td {
  background: var(--bg); border-bottom: 1px solid var(--border);
  padding: .4rem .6rem; white-space: nowrap;
}
tr.group-week > td {
  font-weight: 700; text-transform: uppercase; font-size: .75rem;
  letter-spacing: .03em; color: var(--muted); padding-top: .9rem;
}
tr.group-day > td { font-size: .8rem; color: var(--muted); }

/* The Result column (functions/_public.js gameRow(), "Tappe def. Clients
   7-3") is real prose, often longer than any other cell in the row — and
   table-layout is `auto` (default), so with no guidance the browser was
   free to shrink WHICHEVER column it wanted to make room for it, and it
   usually picked Away/Home: a team name wrapping onto a second line while
   its neighbors on the same row stayed single-line made rows visibly
   different heights and cells look misaligned scanning down the page — a
   real report, not a hypothetical. Date/Time/Away/Home/Score hold short,
   atomic values that should never wrap; forcing them not to pushes any
   unavoidable shrinking onto Result/Weather instead, which read fine
   wrapped, with the pre-existing .table-wrap horizontal scroll (comment
   below) as the fallback if a row still doesn't fit. */
.games-table td[data-label="Date"],
.games-table td[data-label="Time"],
.games-table td[data-label="Away"],
.games-table td[data-label="Home"],
.games-table td[data-label="Score"] {
  white-space: nowrap;
}

/* Public games table (functions/_public.js gamesTable()) — below ~40rem this
   stops being a literal 7-column table (Date/Time/Away/Home/Score/Status/
   Weather couldn't fit a 375px phone even before the Score/Status/Weather
   columns, forcing a horizontal scroll that buried them off-screen) and
   becomes one stacked card per game instead, every field on its own line.
   Each <td> carries a data-label attribute (see gameRow()/weatherCell())
   that ::before turns into the visible field name here. The week/day group
   header rows (.group-row) are excluded from the card treatment — they're
   already full-width single-column dividers, not a game's fields. */
@media (max-width: 40rem) {
  .games-table thead { display: none; }
  .games-table, .games-table tbody { display: block; width: 100%; }
  .games-table tr:not(.group-row) {
    display: block; border: 1px solid var(--border); border-radius: var(--radius);
    padding: .5rem .75rem; margin-bottom: .6rem;
  }
  .games-table tr:not(.group-row):last-child { margin-bottom: 0; }
  .games-table tr:not(.group-row) td {
    display: flex; gap: .5rem; border-bottom: 0; padding: .2rem 0; text-align: left;
  }
  .games-table tr:not(.group-row) td[data-label]::before {
    content: attr(data-label); flex: 0 0 5rem;
    font-size: .7rem; text-transform: uppercase; letter-spacing: .03em; color: var(--muted);
  }
  /* No data-label (the empty-weather case), or a label with nothing after
     it (no score reported yet) — collapse either rather than rendering a
     blank or label-only line. */
  .games-table tr:not(.group-row) td:not([data-label]),
  .games-table tr:not(.group-row) td[data-label]:empty { display: none; }
  .games-table tr.group-row { display: block; }
  .games-table tr.group-row td { padding: .4rem .6rem; }
}

/* Admin playoff schedule candidate/draft tables (js/admin/views/
   playoffs.js generatedRoundCandidate()/savedDraftCard()) — a real
   report: a proposed schedule's Date cell ("Mon, Aug 24 7:15 pm") wrapped
   across five separate lines on a phone, making the table too tall and
   ugly to actually screenshot and paste into a text thread — exactly the
   use case these candidates exist for ("Copy as table" pastes as plain
   text/HTML, which several chat apps don't render usefully either, so a
   clean screenshot is often the only thing that actually works). Same
   mobile stacked-card mechanism as .games-table above — a distinct
   selector, since this table's columns and row semantics differ from
   the public site's own games table, but every <td> here carries the
   identical data-label pattern for the exact same reason. */
@media (max-width: 40rem) {
  .candidate-table thead { display: none; }
  .candidate-table, .candidate-table tbody { display: block; width: 100%; }
  .candidate-table tr {
    display: block; border: 1px solid var(--border); border-radius: var(--radius);
    padding: .5rem .75rem; margin-bottom: .6rem;
  }
  .candidate-table tr:last-child { margin-bottom: 0; }
  .candidate-table td {
    display: flex; gap: .5rem; border-bottom: 0; padding: .2rem 0; text-align: left;
  }
  .candidate-table td[data-label]::before {
    content: attr(data-label); flex: 0 0 5rem;
    font-size: .7rem; text-transform: uppercase; letter-spacing: .03em; color: var(--muted);
  }
  .candidate-table td:not([data-label]) { display: none; }
}

/* ── standings page: tabs, run-stat toggle, sortable columns ───────────── */

.standings-tabs { display: flex; gap: .3rem; flex-wrap: wrap; align-items: center; margin-bottom: .75rem; }
.standings-tabs button[role="tab"],
.standings-tabs #runstat-toggle {
  padding: .35rem .75rem; border: 1px solid var(--border); border-radius: 99px;
  background: var(--bg); color: var(--muted); font-size: .8rem; cursor: pointer;
}
.standings-tabs button[aria-selected="true"] { background: var(--accent); color: var(--accent-text); border-color: transparent; font-weight: 600; }
/* A toggle, not a 5th mutually-exclusive view — a small left margin keeps
   it visually grouped with the tabs (same row, same pill shape, "next to
   Head to head" per real feedback) while still reading as its own thing.
   Active state keys off aria-pressed, not aria-selected, same convention
   as every other toggle in this app. */
.standings-tabs #runstat-toggle { margin-left: .4rem; }
.standings-tabs #runstat-toggle[aria-pressed="true"] { background: var(--accent); color: var(--accent-text); border-color: transparent; font-weight: 600; }

/* A sparse table (few columns — the home page's compact standings, and the
   Home & Away / Last 10 standings-tab views) left a lot of dead horizontal
   space when it stretched to the full width of its card, per real
   feedback ("a lot of wasted space"). Shrink-to-content instead, desktop
   only — the mobile stacked-card transform below still needs full width,
   so this is explicitly reset there rather than left to fight it. */
@media (min-width: 40.01rem) {
  .table-compact { width: auto; max-width: 28rem; }
}
@media (max-width: 40rem) {
  .table-compact { max-width: none; }
}

/* Seed projection — a grid of small cards, one per team, denser than a
   stacked row list on a wide screen. A team that's clinched BOTH the bye
   and the field (nothing left to decide) gets a subtle accent tint, same
   color-mix approach as .card.accent elsewhere, so the "nothing to see
   here" cards visually recede behind the ones still worth reading. */
.seed-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: .6rem; margin-top: .5rem; }
.seed-card { border: 1px solid var(--border); border-radius: 10px; padding: .75rem; }
.seed-card-num { font-family: var(--font-display); font-weight: 400; font-size: 1.3rem; margin: .3rem 0 .2rem; }
.seed-card-clinched { background: color-mix(in srgb, var(--accent) 8%, var(--surface)); border-color: var(--accent); }

/* Last 5 as five small colored squares (js/lib/standings.js's
   last5_sequence) — oldest game on the left, a neutral square for a team
   with fewer than 5 decided games. `title` on the wrapping <td> still
   carries the plain "3-2" text for a quick hover/screen-reader read. */
.last5-squares { display: inline-flex; gap: 2px; vertical-align: middle; }
.last5-square { width: .55rem; height: .55rem; border-radius: 2px; display: inline-block; }
.last5-square.win { background: var(--positive); }
.last5-square.loss { background: var(--danger); }
.last5-square.tie { background: var(--muted); }
.last5-square.none { background: var(--border); }

/* A sortable header (js/public-standings.js) gets a pointer cursor and a
   small direction indicator once clicked; the `#` (rank) column never gets
   data-sort, so it can't be sorted away from the true standings order. */
table[data-sortable] th[data-sort] { cursor: pointer; user-select: none; }
table[data-sortable] th[aria-sort="ascending"]::after { content: ' \25B2'; font-size: .65em; }
table[data-sortable] th[aria-sort="descending"]::after { content: ' \25BC'; font-size: .65em; }

/* RS/RA/DIFF stay hidden until "Show run stats" is tapped — server always
   renders them (the page works with JS off, just showing everything), the
   toggle only adds/removes this class on the wrapping card. */
.hide-runstats .runstat { display: none; }

/* A team sitting exactly at the bye or playoff-field cutoff — a border on
   ITS OWN row rather than a separate divider `<tr>`, specifically so this
   stays meaningful after a column sort re-orders the visible rows: it is
   still true that THIS team is the last one in (or with a bye), even if it
   is no longer drawn in standings order. See loadPlayoffLines(). */
tr.row-bye-line > td { border-bottom: 2px solid var(--accent); }
tr.row-playoff-line > td { border-bottom: 2px dashed var(--muted); }

/* The Home & Away / Last 10 / Head-to-head standings sub-tables get the
   same mobile stacked-card transform as .games-table above (see that
   comment for the reasoning) — a distinct class rather than reusing
   .games-table itself, since the two tables' columns and row semantics
   differ, but the mechanism is identical: every <td> carries a data-label
   the ::before turns into a field name. Deliberately excludes
   .standings-table-overall and .standings-table-home — see the blocks
   below for why those two get a different treatment. */
@media (max-width: 40rem) {
  .standings-table:not(.standings-table-overall):not(.standings-table-home) thead { display: none; }
  .standings-table:not(.standings-table-overall):not(.standings-table-home),
  .standings-table:not(.standings-table-overall):not(.standings-table-home) tbody { display: block; width: 100%; }
  .standings-table:not(.standings-table-overall):not(.standings-table-home) tr {
    display: block; border: 1px solid var(--border); border-radius: var(--radius);
    padding: .5rem .75rem; margin-bottom: .6rem;
  }
  .standings-table:not(.standings-table-overall):not(.standings-table-home) tr:last-child { margin-bottom: 0; }
  .standings-table:not(.standings-table-overall):not(.standings-table-home) td {
    display: flex; gap: .5rem; border-bottom: 0; padding: .2rem 0; text-align: left;
  }
  .standings-table:not(.standings-table-overall):not(.standings-table-home) td[data-label]::before {
    content: attr(data-label); flex: 0 0 5rem;
    font-size: .7rem; text-transform: uppercase; letter-spacing: .03em; color: var(--muted);
  }
}

/* The home page's standings widget — same "stay a real grid, don't stack
   into one-field-per-line cards" fix as .standings-table-overall just
   below, for the identical reason (real feedback called the stacked-card
   version an "odd list" on a narrow portrait phone). A smaller column set
   to start with (7, not 15), so only Pct/Strk/L5 need to drop out to fit;
   #/Team/W-L/GB stay a genuine compact grid down to any width this site
   supports. Anything wider than a portrait phone — landscape, tablet,
   desktop — already renders the full, untouched table above this
   breakpoint. */
@media (max-width: 40rem) {
  .standings-table-home { font-size: .82rem; }
  .standings-table-home th, .standings-table-home td { padding: .4rem .35rem; }
  .standings-table-home [data-label="Pct"],
  .standings-table-home [data-label="Strk"],
  .standings-table-home [data-label="L5"] {
    display: none;
  }
}

/* The Overall table is a LEADERBOARD — its whole point is comparing every
   team at a glance, which is exactly what the one-field-per-line stacked
   card above defeats: even trimmed to 8 of its 15 columns, that still
   turned every team into an 8-line card, so scanning 6 teams meant
   scrolling past 48 stacked lines — real feedback called this unreadable,
   and a second look after trimming the columns confirmed the card shape
   itself was the actual problem, not the column count. This stays a real
   `<table>` at every width instead: only the columns hidden below (T/
   Sched/GP/CG/FF) drop out below this breakpoint, both the header cell AND
   the body cell so the grid doesn't misalign — a wider phone, a tablet, or
   flipping to landscape all just show the ordinary un-hidden table, no
   layout switch to undo. sortableTh() (functions/_public.js) puts the same
   data-label on the <th> that gameRow()/this table's <td>s already carry,
   so one selector list hides both. .table-wrap's existing horizontal
   scroll is still there as a fallback if even 8 columns is tight on a
   narrow phone — nothing new needed for that, it already wraps every
   table on this site. */
@media (max-width: 40rem) {
  .standings-table-overall { font-size: .82rem; }
  .standings-table-overall th, .standings-table-overall td { padding: .4rem .4rem; }
  .standings-table-overall [data-label="T"],
  .standings-table-overall [data-label="Sched"],
  .standings-table-overall [data-label="GP"],
  .standings-table-overall [data-label="CG"],
  .standings-table-overall [data-label="FF"] {
    display: none;
  }
}

/* Public phone bottom tab bar — see tabBar() in functions/_public.js. The
   topbar's own text links (.topbar-links) hide at this width in favor of it,
   but the topbar itself (brand + theme toggle) stays put at the top.
   "Manager sign in" is text-only with no icon (unlike the AI/theme
   toggles), so it's the first thing to go on a phone — real overflow was
   measured at 375px with it in place (409px scrollWidth against a 375px
   viewport). It stays reachable via the Help page either way. */
@media (max-width: 40rem) {
  .topbar-links { display: none; }
  .topbar-manager-link { display: none; }
  main.has-tab-bar { padding-bottom: 4.5rem; }
  .tab-bar {
    display: grid; grid-template-columns: repeat(5, 1fr);
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 10;
    background: var(--surface); border-top: 1px solid var(--border);
  }
}
@media (min-width: 40.01rem) {
  .tab-bar { display: none; }
}
.tab-bar-item {
  display: flex; flex-direction: column; align-items: center; gap: .2rem;
  padding: .5rem 0 .6rem; color: var(--muted); text-decoration: none;
  font-size: .65rem; letter-spacing: .02em; text-transform: uppercase;
}
.tab-bar-item.active { color: var(--accent); font-weight: 600; }

.notice { background: var(--warn-bg); border: 1px solid var(--warn-border); border-radius: var(--radius); padding: .75rem; margin-bottom: var(--gap); }
.error { color: var(--danger); }

/* Game-day verdict banner — see verdictBanner() in functions/_public.js.
   Sits below the topbar on every public page, absent entirely on a normal
   day (no verdict set). Three states, each with a distinct color so the
   answer to "are we playing" reads at a glance, not just from the words. */
.verdict-banner {
  display: flex; flex-wrap: wrap; justify-content: center; gap: .4rem 1rem; align-items: baseline;
  padding: .6rem 1rem; font-size: .9rem; border-bottom: 1px solid var(--border); text-align: center;
}
.verdict-on { background: color-mix(in srgb, var(--accent) 12%, transparent); }
.verdict-off { background: var(--warn-bg); color: var(--danger); }
.verdict-pending { background: var(--warn-bg); }
.verdict-banner strong { font-family: var(--font-display); font-weight: 400; text-transform: uppercase; letter-spacing: .03em; }

.empty { text-align: center; color: var(--muted); padding: 2rem 1rem; }

#toast {
  position: fixed; left: 50%; bottom: 1.25rem; transform: translateX(-50%);
  background: var(--text); color: var(--bg);
  padding: .6rem 1rem; border-radius: 99px; font-size: .9rem;
  opacity: 0; pointer-events: none; transition: opacity .18s; z-index: 100;
  max-width: calc(100vw - 2rem);
}
/* pointer-events stays 'none' while hidden so the invisible toast never
   blocks clicks on whatever's underneath it. Real bug fixed here: without
   this override, #toast.show inherited 'none' from the base rule above
   forever, meaning the toast's own action buttons (Undo, Share to
   WhatsApp) were never actually clickable, and its text couldn't be
   selected to copy — pointer-events:none blocks mouse-driven text
   selection too. */
#toast.show { opacity: 1; pointer-events: auto; }
#toast.bad { background: var(--danger); color: #fff; }

.signin { max-width: 22rem; margin: 3rem auto; }

/* Day-of-week preference grid */
.days { display: grid; grid-template-columns: repeat(7, 1fr); gap: .25rem; }
.days label { text-align: center; font-size: .7rem; margin-bottom: .15rem; }
.days select { padding: .35rem .2rem; font-size: .75rem; }

/* Day-picker pills — the location "Quick add" slot generator
   (js/admin/views.js). Checkboxes styled as tappable pills instead of the
   cramped 7-column .days grid above, since there's no per-day value to
   show here, just on/off. */
.pick-days { display: flex; flex-wrap: wrap; gap: .4rem; }
.pick-days label {
  display: flex; align-items: center; gap: .35rem; font-size: .8rem; font-weight: 400;
  padding: .3rem .6rem; border: 1px solid var(--border); border-radius: 8px; margin: 0;
  background: var(--bg); cursor: pointer;
}
.pick-days input:checked + span { font-weight: 600; }

/* Rain outlook on game listings — see weatherCell() in functions/_public.js.
   Colour carries emphasis only; the icon and text carry the meaning, so this
   still reads correctly in monochrome or with colour-vision differences.

   Deliberately NOT red/danger: a forecast has not canceled anything, and
   alarm-coloured weather makes players assume a game is off when it isn't.
   Red is reserved for an actual town closure (.closure below), which is the
   only non-league source that gets to say a field is unavailable. */
.nowrap { white-space: nowrap; }
.wx-heavy_recent_rain,
.wx-rain_likely { color: var(--text); }
.wx-field_wet,
.wx-rain_possible { color: var(--muted); }

/* An actual closure published by the town — authoritative, unlike a forecast. */
.closure { color: var(--danger); font-weight: 600; }

/* A weather icon/chip's "Weather details" disclosure — native <details>,
   no JS. weatherDisclosure() (functions/_public.js) wraps the same
   icon+label markup the plain (non-expandable) version uses, so this must
   read identically until it's actually opened.
   .chip is display:inline-flex, but <summary> defaults to display:list-
   item with its own browser-drawn marker — reset both so a <details>
   sitting where a <span class="chip"> used to renders the same, then add
   a small caret back via ::after so it's still visually obvious this
   expands (removing the native marker takes that away otherwise). */
.wx-details { display: inline-block; max-width: 100%; }
.wx-details > summary { list-style: none; cursor: pointer; }
.wx-details > summary::-webkit-details-marker { display: none; }
.wx-details > summary::after { content: '▾'; opacity: .55; font-size: .7em; margin-left: .3em; }
.wx-details[open] > summary::after { content: '▴'; }
.wx-details > summary.chip { display: inline-flex; }
/* max-width keeps the popped-open box from stretching full-width across a
   wide desktop table row; min() caps it at the viewport too so it can
   never itself force page-level horizontal scroll on a narrow phone — the
   table inside still gets its own .table-wrap scroll for anything that
   doesn't fit even that. */
.wx-detail-body {
  margin-top: .4rem; padding: .5rem .6rem; border: 1px solid var(--border);
  border-radius: var(--radius); background: var(--surface); max-width: min(26rem, 100%);
}
/* The game's own hour(s), highlighted the same way admin's detail table
   does (js/ui.js's weatherDetailBody() uses the identical .row-dh class,
   already styled elsewhere for the doubleheader-row tint — reused here
   for "the hour that matters", not a doubleheader). */
.wx-detail-body table { font-size: .85rem; }

/* Tap-to-enter score pad — see scorePad() in js/ui.js. A real number input
   (not a plain div), so tapping it opens the device's own number keyboard. */
.scorepad-display {
  display: block; width: 100%; box-sizing: border-box;
  font-family: var(--font-display); font-size: 2.2rem; font-weight: 400; font-variant-numeric: tabular-nums;
  text-align: center; padding: .35rem 0; margin-bottom: .4rem;
  background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
  color: inherit;
  -moz-appearance: textfield; /* hide Firefox's spinner; keep the tap-friendly look */
}
.scorepad-display::-webkit-outer-spin-button,
.scorepad-display::-webkit-inner-spin-button {
  -webkit-appearance: none; margin: 0; /* hide Chrome/Safari's spinner arrows */
}
.scorepad-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: .35rem; margin-bottom: .35rem;
}
.scorepad-grid button {
  font-size: 1.15rem; padding: .7rem 0; /* big tap targets */
}

@media print {
  .topbar, .tab-bar, button, #toast { display: none !important; }
  body { background: #fff; }
}
