/*
 * NatStat public site design system (Phase 0). New, not inherited from
 * ComCen/Office's admin-panel CSS - those are internal tools, this is the
 * public-facing site: FIBA-inspired nav (basketball.fiba.basketball, the
 * reference the user picked out), Dosis throughout, light/dark toggle,
 * and a Level color triad (--level-color/--level-highlight/--level-dark)
 * that defaults to slate gray on non-Level pages and gets overridden
 * inline per-page once a Level is known (see partials/header.php).
 */

:root {
  --font-display: 'Dosis', 'Trebuchet MS', sans-serif;
  --font-body: 'Dosis', 'Trebuchet MS', sans-serif;

  /* Type scale (2026-09-19): every literal font-size in this file snaps to one of these steps. */
  --fs-1: 0.6875rem; /* 11px */
  --fs-2: 0.75rem; /* 12px */
  --fs-3: 0.8125rem; /* 13px */
  --fs-4: 0.875rem; /* 14px */
  --fs-5: 1rem; /* 16px */
  --fs-6: 1.125rem; /* 18px */
  --fs-7: 1.25rem; /* 20px */
  --fs-8: 1.5rem; /* 24px */
  --fs-9: 1.75rem; /* 28px */
  --fs-10: 2rem; /* 32px */
  --fs-11: 2.5rem; /* 40px */

  /* Level color triad - slate blue default for non-Level pages (per the
     user, 2026-08-29 - each Level's own three-color Color/Highlight/
     DarkColor triad becomes that page's real accent scheme; slate blue is
     reserved for pages with no Level at all). Level pages override all
     three via an inline <style> block (partials/header.php). */
  --level-color: #64748b;
  --level-highlight: #cbd5e1;
  --level-dark: #334155;

  --bg: #f7f8fa;
  --bg-raised: #ffffff;
  --text: #1b1f24;
  --text-dim: #5b6472;
  --border: #e1e4e9;
  --shadow: 0 4px 16px rgba(20, 24, 30, .08);
  --radius: 10px;
  /* Sidebar CTA button text - per the user (2026-09-15): light mode stays
     white (already correct), dark mode should be black, not white -
     white-on-level-color reads fine in light but washes out against this
     theme's own darker level accents. */
  --sidebar-cta-text: #fff;
  /* Real bug, not cosmetic (2026-09-12, per the user comparing directly
     against Kagi - "it's not difficult") - with no color-scheme set,
     Chrome/Safari render every native form control (a <select>'s
     popup, and worst, an <input type="date">'s own calendar icon and
     internal segment text) using the BROWSER's light-mode assumption
     regardless of this site's own dark palette above - the icon reads
     as a dark smudge on a dark input instead of Kagi's crisp light
     one. Set once here, theme-aware via the same 3-block pattern every
     other token on this page already follows, so it's never
     recreated ad hoc per component. */
  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #14171c;
    --bg-raised: #1d2127;
    --text: #eef0f3;
    --text-dim: #9aa3b0;
    --border: #2a2f37;
    --shadow: 0 4px 20px rgba(0, 0, 0, .4);
    --sidebar-cta-text: #000;
    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --bg: #14171c;
  --bg-raised: #1d2127;
  --text: #eef0f3;
  --text-dim: #9aa3b0;
  --sidebar-cta-text: #000;
  --border: #2a2f37;
  --shadow: 0 4px 20px rgba(0, 0, 0, .4);
  color-scheme: dark;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--fs-5);
  line-height: 1.5;
}

/* Form controls don't inherit the page's own font-family by default in
   any browser (2026-09-11, per the user - "everything is Dosis on this
   site," found via the Data Corrections modal's textarea rendering in
   the system UI font) - this was being patched per-component with a
   one-off `font-family: inherit` wherever someone happened to notice
   (.confirm-cancel, .support-section-btn, etc. - still harmless, just
   redundant now), instead of fixed once at the root. One rule here
   covers every input/textarea/select/button site-wide going forward. */
input, textarea, select, button { font-family: inherit; }

/* Site-wide <select> base (2026-09-11, per the user) - a plain <select>
   with no class of its own (level-export-page.php's Data/Format
   pickers, support-page.php's Sport/League picker, and any future one)
   was falling back to the browser's own white-background/tiny-text
   default, which looked jarringly out of place - especially in dark
   mode - next to every other themed control on the site. This is the
   real style the user found already correct and mode-sensitive on
   .stat-rankings-picker's own select (/{level}/rankings/stat/{stat}) -
   promoted here to a bare `select` rule so every select site-wide gets
   it automatically, with .stat-rankings-picker's own scoped copy folded
   into this one instead of kept as a duplicate. -2px of padding off
   the original (2026-09-11, per the user - "can we take 2px of padding
   out of this"). A page can still override for something more specific
   (.detail-season-picker's own deliberately bare/borderless look,
   .scoreboard-cal-header select's compact one) since a class selector
   always wins the specificity tie against this bare-tag one. */
select {
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-raised);
  color: var(--text);
  font-size: var(--fs-5);
  cursor: pointer;
}

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 700;
  text-wrap: balance;
  margin: 0 0 .5em;
}

h1 { font-size: clamp(1.75rem, 4vw, 2.5rem); }
/* REVISED 2026-09-05, per the user: "the subheaders (Roster, Season
   ELO, etc) tend to be the same size as the main page header" - h2's
   own clamp topped out at the exact same 1.75rem .detail-result (the
   H1) uses, so a section subheader and the page's own title read as
   the same visual weight. Shrunk ~2px at both ends of the clamp and
   dimmed - var(--text-dim), not a literal hex, so it stays correct in
   both themes rather than only the light one the user's own "#dedede"
   example was written against.
   REVISED AGAIN 2026-09-05, per the user: plain var(--text-dim) reads
   too dark - it's the exact same token .game-stat-grid-player-faded
   uses for a boxscore's own non-starter/bench player names, a
   deliberately muted, de-emphasized look that's too subdued for a
   section header a reader should still find at a glance. Split the
   difference with color-mix rather than reach for a literal hex again
   (still needs to stay correct in both themes). */
h2 { font-size: clamp(1.2rem, 3vw, 1.625rem); color: color-mix(in srgb, var(--text-dim) 50%, var(--text) 50%); }
h3 { font-size: var(--fs-6); }

a { color: var(--level-color); text-decoration: none; }
a:hover { text-decoration: underline; }

code {
  font-family: ui-monospace, monospace;
  background: var(--border);
  border-radius: 4px;
  padding: .1em .4em;
  font-size: .9em;
}

/* ---------------------------------------------------------- system notice */
/* Site-wide banner, staff-set in Front Office (public/system_notice.php).
   Three severities per the user (2026-08-30): normal is deliberately a
   fixed white surface with fixed dark text (not the --text/--bg theme
   tokens - --text flips to a light color in dark mode, which would go
   invisible on a literal white background), warning is a subdued grey
   meant to be noticed without being annoying, urgent is a dark red meant
   to be hard to miss. Static, not sticky - scrolls away with the page,
   avoiding z-index/offset coordination with .site-header's own sticky
   position. Hidden by default (inline style, see the partial) so
   already-dismissed notices never flash before JS hides them.
*/

/* Dosis, self-hosted (2026-09-08) - was a render-blocking fonts.googleapis.com
   stylesheet link in header.php, which could stall initial page load waiting
   on an external DNS/TLS round-trip to Google (found live via the browser's
   own "Waiting for google.com" status). Same reasoning as the venue map's
   earlier move off Google Maps - no external dependency for something this
   core to every page's render. Dosis ships as a true variable font (Google's
   own CSS API serves the identical file for every named weight 300-800), so
   one @font-face per script subset with a weight RANGE covers the whole
   --font-display/--font-body scale used across the site, not six separate
   file loads. latin-ext keeps most European-language diacritics (accented
   player/team names) rendering in-family instead of falling back silently;
   Vietnamese-only combining marks were left off google.com rather than
   downloaded, since nothing in the DB is likely to need them.
 */
@font-face {
  font-family: 'Dosis';
  font-style: normal;
  font-weight: 300 800;
  font-display: swap;
  src: url('/assets/fonts/dosis-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Dosis';
  font-style: normal;
  font-weight: 300 800;
  font-display: swap;
  src: url('/assets/fonts/dosis-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
.system-notice {
  display: flex;
  align-items: center;
  max-width: 1200px;
  margin: 0 auto;
  padding: 10px 20px;
  font-size: var(--fs-4);
}
/* Whole banner is clickable when there's a URL (per the user, 2026-08-30)
   - .system-notice-inner is the <a> itself in that case (see the
   partial), sized to fill everything except the dismiss button rather
   than just the link label. */
.system-notice-inner {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}
a.system-notice-inner:hover { opacity: .92; text-decoration: none; }
.system-notice-icon { flex: none; font-size: var(--fs-5); }
.system-notice-message { flex: 1; min-width: 0; }
.system-notice-link { flex: none; font-weight: 700; text-decoration: underline; }
.system-notice-dismiss {
  flex: none;
  background: none;
  border: none;
  color: inherit;
  font-size: var(--fs-8);
  line-height: 1;
  padding: 0 0 0 6px;
  cursor: pointer;
  opacity: .7;
}
.system-notice-dismiss:hover { opacity: 1; }

.system-notice-normal { background: #ffffff; color: #1b1f24; border-bottom: 1px solid #e1e4e9; }
.system-notice-normal .system-notice-link { color: #1b1f24; }

.system-notice-warning { background: #52525b; color: #ffffff; }
.system-notice-warning .system-notice-icon { color: #facc15; }
.system-notice-warning .system-notice-link { color: #ffffff; }

.system-notice-urgent { background: #7f1d1d; color: #ffffff; }
.system-notice-urgent .system-notice-icon { color: #fca5a5; }
.system-notice-urgent .system-notice-link { color: #ffffff; }

/* ---------------------------------------------------------------- header */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--bg-raised);
  border-bottom: 1px solid var(--border);
}

.site-header-inner {
  max-width: 1280px;
  margin: 0 auto;
  padding: 12px 20px;
  display: flex;
  align-items: center;
  gap: 24px;
}

/* .brand-group replaces the old single <a class="brand"> wrapper
   (design-sweep punchlist, 2026-09-08 - "click on NatStat takes you to
   /, on the abbreviation to /{level}") - two independent link targets
   now, so the mark and the wordmark can't share one anchor any more.
   Same flex/gap layout the old .brand carried. */
.brand-group { display: flex; align-items: center; gap: 9px; }
.brand-mark-link { display: flex; }
.brand-name, .brand-name-level {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--fs-8);
  color: var(--text);
  letter-spacing: -.01em;
}
a.brand-name:hover { text-decoration: none; color: var(--level-color); }
/* Level-mode wordmark - "NatStat" in the page's normal text color,
   "{ABBREV}" in the Level's own light/highlight color (per the user) -
   --level-abbrev-color is set inline per-page since it's this specific
   Level's own Highlight/Color, not the generic --level-color token
   (which Level pages already override globally, but the punchlist
   asked for the LIGHT color specifically here, not necessarily the
   same shade every other accent use on the page reaches for). */
/* REVISED (design-sweep punchlist, 2026-09-08 second round, per the
   user) - "NatStat" gets no hover color at all (was shifting to
   --level-color like every other nav link - the user wants it inert);
   the abbreviation's own RESTING color now matches the five primary-nav
   links exactly (same color-mix formula, just above), with the
   Level's own light/highlight color reserved for its hover state only -
   --level-highlight is already a real global custom property (set
   inline per-page in this file's own <head>, same block that sets
   --level-color), no separate per-element inline color needed any
   more. */
.brand-natstat { color: var(--text); }
.brand-natstat:hover { text-decoration: none; }
.brand-level-abbrev { color: color-mix(in srgb, var(--level-color) 55%, var(--text-dim)); }
.brand-level-abbrev:hover { color: var(--level-highlight); text-decoration: none; }

/* Same mark+wordmark lockup as the mailing-campaign email header
   (FrontOffice\Mail\Templates::renderCampaign()) - 26px mark, wordmark in
   Dosis. Logo variant follows the theme the same way the email picks
   white/black by background contrast: black mark on the light header
   background, white mark on the dark one. */
.brand-mark { display: none; width: 26px; height: 26px; }
.brand-mark-black { display: block; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .brand-mark-black { display: none; }
  :root:not([data-theme="light"]) .brand-mark-white { display: block; }
}
:root[data-theme="dark"] .brand-mark-black { display: none; }
:root[data-theme="dark"] .brand-mark-white { display: block; }

/* "National Statistical" on widescreen (tablet/desktop), "NatStat" on
   phone - same 767px cutoff as the rest of the mobile-only tweaks below. */
.brand-short { display: none; }

.primary-nav {
  display: flex;
  align-items: center;
  gap: 18px;
  flex: 1;
}

.primary-nav a,
.primary-nav .more-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 600;
  /* Tinted toward --level-color rather than a flat --text-dim (per the
     user, 2026-08-30) - on a Level page this shifts the resting nav color
     toward that Level's own accent instead of looking identical to every
     other page until hover; on non-Level pages --level-color is itself
     the same slate default, so the mix reads unchanged. */
  color: color-mix(in srgb, var(--level-color) 55%, var(--text-dim));
  background: none;
  border: none;
  font-family: var(--font-body);
  font-size: var(--fs-5);
  cursor: pointer;
  padding: 6px 0;
}
/* Shared icon sizing for every nav/dropdown svg (header links, More/sport
   dropdown items, footer sport headers) - per the user, 2026-08-29. */
.primary-nav svg, .more-menu svg, .footer-sport-col summary svg {
  width: 16px;
  height: 16px;
  flex: none;
}
/* Sport icons are Font Awesome <i> tags, not inline svg (per the user,
   2026-08-29 - hand-drawn ones weren't reading clearly) - sized via
   font-size instead of width/height. */
.sport-icon { font-size: .85em; }
/* Fixed-width Font Awesome glyphs, site-wide (design-sweep punchlist,
   2026-09-08 - "Font Awesome icon widths"). Every fa-solid glyph on the
   site leads a text label (a nav tab, a dropdown row, a section
   subhead), but the glyphs themselves aren't equal-width - fa-users-cog
   is visibly wider than fa-trophy, for one real example already on
   this site (team-nav.php's Players vs. Superlatives tabs) - so labels
   in the same vertical list/row set start at slightly different x
   offsets depending purely on which icon happens to lead them. Font
   Awesome ships its own `fa-fw` utility for exactly this (1.25em,
   centered) in the kit already self-hosted at /assets/fontawesome/ -
   applying its rule to every `.fa-solid` directly here covers every
   call site at once rather than adding the `fa-fw` class by hand to
   every icon tag across two dozen partials. */
.fa-solid { width: 1.25em; text-align: center; }
.primary-nav a:hover,
.primary-nav a.active,
.primary-nav .more-toggle:hover {
  color: var(--level-color);
  text-decoration: none;
}

.more-wrap { position: relative; }

.more-menu {
  position: absolute;
  top: calc(100% + 10px);
  left: 0;
  /* Without this, a stat grid or any other positioned/z-indexed content
     further down the SAME page could paint over this dropdown (found
     live, 2026-08-30, on the Boxscore tab's stat grid) - explicit and
     comfortably above ordinary page content (.site-header itself is 50). */
  z-index: 55;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 8px;
  display: flex;
  flex-direction: column;
  min-width: 160px;
}
/* [hidden] and .more-menu are equal-specificity, same-origin selectors, so
   without this the later .more-menu { display: flex } rule wins the
   cascade tie and silently overrides the hidden attribute's display:none -
   found live (2026-08-29), the dropdown rendered open by default. */
.more-menu[hidden] { display: none; }
.more-menu a {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--text);
  font-weight: 500;
  padding: 8px 10px;
  border-radius: 6px;
}
/* :not(.account-menu-pill) (2026-09-10, real bug caught live by the user -
   "mouseover makes entire pills black on #111111") - this hover tint reads
   var(--level-color), the shared per-Level nav accent (blue by default),
   not each pill's own --chip-color, so on hover a League pill's real chip
   color was replaced by a near-black tint instead. Per the user, no
   mouseover at all here, matching Overview's own .level-chip pills (which
   already have no hover-background rule) - simplest fix is just excluding
   the pill from this rule rather than giving it a second, different hover
   effect. */
.more-menu a:not(.account-menu-pill):hover { background: color-mix(in srgb, var(--level-color) 12%, transparent); text-decoration: none; }
/* "Leaves this structure" indicator (design sweep, 2026-09-11, per the
   user) - a small, faded, flush-right box-with-arrow icon for a
   .more-menu row that navigates OUT of the current context entirely
   (a Level's own Analysis tab -> the Account area, e.g. Watchlist
   today, API stuff soon) rather than to another tab within the same
   structure. .more-menu a is already `display: flex`, so margin-left:
   auto alone pushes this to the row's far right with no layout change
   needed elsewhere. */
.more-menu-escape { margin-left: auto; font-size: var(--fs-1); color: var(--text-dim); opacity: .5; }

/* Header account dropdown (2026-09-09, per the user) - reuses .more-menu
   a's own row styling for its own <a>s, plus a <button> reset for the
   Log Out row (that base rule was only ever written for <a> before,
   same gotcha .detail-nav .more-toggle already needed its own fix for
   elsewhere in this file). */
/* Right-aligned, not left (2026-09-09, per the user - "the account menu
   points right and takes it off the page"): the trigger is the
   rightmost element in the header, so .more-menu's default left: 0
   anchor pushed the whole panel off the right edge of the viewport
   (confirmed live: panel right edge at 1446px on a 1280px-wide page).
   Same fix, same shape, as .export-menu-panel's own right: 0; left:
   auto override just below - not the first .more-menu on this site
   that's needed this. */
.account-menu { right: 0; left: auto; min-width: 240px; }
.account-menu .account-menu-name { font-weight: 700; border-bottom: 1px solid var(--border); margin-bottom: 4px; padding-bottom: 10px; align-items: center; }
/* ID under the name, smaller (2026-09-09, per the user - "pin this for
   the support desk, since we've generally relied on a static ID"). */
.account-menu-id { display: block; font-weight: 400; font-size: var(--fs-2); color: var(--text-dim); }
.account-menu-leagues, .account-menu-key, .account-menu-watchlist { display: flex; align-items: center; gap: 8px; padding: 8px 10px; color: var(--text); }
/* Real bug (design sweep, 2026-09-11) - a logged-in user with an empty
   watchlist still saw this row: account-menu.js only sets [hidden] when
   watchlistCount is 0, but an author `display: flex` rule at ANY
   specificity beats the browser's own [hidden] UA-default rule, not
   just an equal-specificity author rule - same lesson already hit once
   on the Account Leagues confirm-modal ("[hidden] and this class's own
   display: flex have equal CSS specificity, and an author rule always
   wins over the browser's UA default"). Explicit override, same fix. */
.account-menu-watchlist[hidden] { display: none; }
.account-menu-leagues i, .account-menu-key i, .account-menu-watchlist i { color: var(--text-dim); }
/* :not(.account-menu-pill) (2026-09-09, real bug caught live by the
   user - "the pill styles are a little off - they have the right
   background color but no border-radius") - this reset was written for
   the plain "Leagues (N)"/API-Key text links but its bare `a` selector
   also matched .account-menu-pill (a real <a class="level-chip
   account-menu-pill">, nested inside .account-menu-leagues too) with
   higher specificity than .level-chip's own border-radius: 999px,
   silently squaring off every League pill in this menu. */
.account-menu-leagues a:not(.account-menu-pill), .account-menu-key a, .account-menu-watchlist a { padding: 0; border-radius: 0; }
.account-menu-loading { color: var(--text-dim); font-size: var(--fs-4); }
.account-menu-pills { display: flex; flex-wrap: wrap; gap: 4px; }
/* .level-chip.account-menu-pill (both classes, real specificity fix
   live-caught by the user - "the right background color but no
   border-radius") - .more-menu a's own border-radius: 6px (0,1,1) still
   beat .level-chip's 999px (0,1,0) even after excluding this pill from
   the OTHER conflicting reset just above; this compound selector (0,2,0)
   finally clears both. */
/* Smaller (2026-09-10, per the user - "make the pills smaller so up to
   5 can fit on one line") - was .78rem/1px 8px, tight enough at 5
   3-letter codes that .account-menu's own min-width also went up
   220px -> 240px alongside this for a real safety margin, not just the
   pills alone. */
.level-chip.account-menu-pill { font-size: var(--fs-1); padding: 1px 6px; border-radius: 999px; }
.account-menu-key-text { cursor: pointer; font-variant-numeric: tabular-nums; }
.account-menu-key-text:hover { color: var(--level-color); }
.account-menu form { margin: 0; }
.account-menu form button {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  color: var(--text);
  font-weight: 500;
  font-family: inherit;
  font-size: var(--fs-5);
  padding: 8px 10px;
  border-radius: 6px;
  border: none;
  background: none;
  cursor: pointer;
  text-align: left;
}
.account-menu form button:hover { background: color-mix(in srgb, var(--level-color) 12%, transparent); }
/* Greyed out (2026-09-09, per the user) - a lower-emphasis action than
   the account info above it. .account-menu form button (0,1,2 - one
   class, two elements) otherwise beats a plain .account-menu-logout
   button (0,1,1) on specificity alone; .account-menu .account-menu-
   logout button matches that shape exactly (0,2,2) to actually win. */
.account-menu .account-menu-logout button { color: var(--text-dim); }

/* Abbreviation + color dot + native tooltip instead of the full Level
   name (per the user, 2026-08-29) - a 2-column grid too, since Basketball
   alone has 16 Levels and a single-column list would make the dropdown
   very tall. */
.sport-menu {
  display: grid;
  grid-template-columns: repeat(2, auto);
  min-width: 220px;
}
.sport-menu-level {
  display: flex;
  align-items: center;
  gap: 7px;
  font-weight: 600;
  font-size: var(--fs-4);
}
.sport-menu-level::before {
  content: '';
  width: 9px;
  height: 9px;
  border-radius: 3px;
  background: var(--chip-color);
  flex: none;
}

/* margin-left: auto (not primary-nav's flex:1) is what actually keeps this
   flush right - found live (2026-08-29): primary-nav is display:none below
   1024px, so without this the toggle+account pair collapsed back against
   the brand instead of staying pinned to the edge on tablet/mobile. */
.header-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; }

/* Level-contextual quick-search (2026-09-11, per the user - top nav,
   left of the theme toggle, contextual: a search icon that "rolls out
   to the left on click" rather than a dropdown panel). The input's own
   TRACK animates width 0 -> real width while sitting BEFORE the toggle
   button in normal flex flow - the button never moves, so growing the
   track to its left is what produces the roll-out-leftward effect,
   with no position:absolute/right-edge math needed (robust regardless
   of the button's own exact width, unlike anchoring the panel via a
   hardcoded `right` offset). */
.level-search-wrap { position: relative; display: flex; align-items: center; }
.level-search-input-track { width: 0; overflow: hidden; transition: width .2s ease; }
.level-search-wrap.is-open .level-search-input-track { width: 190px; margin-right: 8px; }
.level-search-input {
  width: 190px; box-sizing: border-box; padding: 7px 10px; border: 1px solid var(--border); border-radius: 999px;
  background: var(--bg); color: var(--text); font-family: var(--font-body); font-size: var(--fs-4);
}
/* border-color, not box-shadow (round 2 fix, 2026-09-11 - the box-shadow
   glow from round 1 was STILL showing as clipped orange arcs per the
   user's own screenshot). Real cause: .level-search-input-track needs
   overflow:hidden for its own width-rolling animation, and a box-shadow
   paints outside the input's border box - so that ancestor's
   overflow:hidden clips it asymmetrically right at the pill's curved
   ends. border-color is painted entirely within the input's own box,
   so there is nothing left for any ancestor's overflow to clip. */
.level-search-input:focus { outline: none; border-color: var(--level-color); }
/* Same round-icon-button look theme-toggle/account-link already use in
   this exact row, not the smaller/differently-styled .icon-toggle used
   elsewhere on the site - this sits directly beside those two, so it
   should read as their sibling, not a mismatched import. */
.level-search-toggle {
  background: none; border: 1px solid var(--border); border-radius: 50%;
  width: 34px; height: 34px; flex: none; display: grid; place-items: center;
  cursor: pointer; color: var(--text-dim);
}
.level-search-toggle:hover, .level-search-wrap.is-open .level-search-toggle { color: var(--level-color); border-color: var(--level-color); }
/* Sized/weighted to match the Level Hub's own tabs dropdown
   (.detail-nav .more-menu/.more-menu a - real bug caught live,
   2026-09-11, "let's hew to the style of the tabs dropdown - smaller
   text, not as much padding"), not the site header's own roomier
   .more-menu (Account/Analysis/More). */
.level-search-results {
  position: absolute; top: calc(100% + 10px); right: 0; width: 240px;
  background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 4px; list-style: none; margin: 0; z-index: 60;
}
.level-search-results[hidden] { display: none; }
.level-search-result { display: flex; align-items: flex-start; gap: 8px; padding: 5px 6px; border-radius: 6px; color: var(--text); font-weight: 500; font-size: var(--fs-4); }
.level-search-result:hover,
.level-search-result.is-active { background: color-mix(in srgb, var(--level-color) 12%, transparent); text-decoration: none; }
/* Fixed-width media "column" (round 2 fix, 2026-09-11, per the user -
   "minilogos about 3-4px too big, blow out the table cells; make the
   columns fixed width, right now the team names are superindented
   compared to the FA icon players") - both the team-logo and
   player-icon branches sit inside this same wrapper now, sized once
   here, so a row's label always starts at the same x-position
   regardless of which one rendered. */
/* +5px on both the media column and the icon/logo inside it (round 4,
   2026-09-11, per the user - "now it's super small... would benefit
   from +5 h/w") - round 3's specificity fix finally made the CSS here
   apply for real, which made it obvious 13/11px had overcorrected. */
.level-search-result-media { width: 18px; height: 18px; flex: none; display: grid; place-items: center; margin-top: 2px; }
.level-search-result-icon { text-align: center; color: var(--level-color); font-size: var(--fs-5); }
/* Real bug found (round 3, 2026-09-11, per the user - "still the same
   size, busted cache to make sure it wasn't stale css" - correctly
   ruled out caching, the CSS itself was the problem): a bare
   `.level-search-result-logo` class selector has the SAME specificity
   as .detail-team-logo's own base rule (both single-class, 0-1-0), and
   .detail-team-logo's `width: 28px; height: 28px;` sits LATER in this
   file (line ~801) - so it silently won the cascade by source order
   every time, regardless of what size was set here. This never
   actually shrank anything in round 2 either, despite looking correct
   in the diff. Fixed the way every other .detail-team-logo override on
   this site already does it (.game-schedule-team .detail-team-logo,
   .stat-grid-team-abbrev .detail-team-logo, etc.) - a two-class
   descendant selector outranks the single-class base rule regardless
   of source order, so it can never be silently defeated like this
   again. */
.level-search-result-media .detail-team-logo { width: 16px; height: 16px; }
.level-search-result-text { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.level-search-result-label { display: flex; align-items: baseline; gap: 5px; min-width: 0; }
.level-search-result-label > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Current team abbreviation next to a player's name, grey (round 2,
   2026-09-11, per the user - "add current team abbreviation to players
   where available in grey"). */
.level-search-result-abbrev { flex: none; font-size: var(--fs-1); font-weight: 600; color: var(--text-dim); }
/* Small grey meta line under each result - team Record+ELO, or player
   Position/Number+PCR rank (round 2, 2026-09-11, per the user - "team
   records and ELO, like '45-52 - ELO 1394 #14'" / "player number/pos
   and PCR, like 'G #22 - PCR #423'"). */
.level-search-result-meta { font-size: var(--fs-1); font-weight: 500; color: var(--text-dim); }

/* /search - the global "Google-y" results page (2026-09-11), a real
   full-page list (not a dropdown, unlike .level-search-* above) - own
   class names throughout, but the same media-column sizing convention
   (.detail-team-logo trimmed to a fixed slot) carries over directly. */
.search-page-form { display: flex; gap: 8px; margin-bottom: 24px; }
.search-page-form input {
  /* border-radius 999px -> 8px (2026-09-12, per the user - "the full
     rounded border radius on the search bar is a little out of step
     with our design standards... most text inputs are like the ones in
     the Account section") - matches .auth-field input's own values
     exactly (8px radius, var(--bg) background), not just a rounder
     approximation of it. The round submit button below is unchanged -
     the user confirmed that one's fine as-is. */
  flex: 1; box-sizing: border-box; padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--bg); color: var(--text); font-family: var(--font-body); font-size: var(--fs-5);
}
.search-page-form input:focus { outline: none; border-color: var(--level-color, var(--accent)); }
.search-page-form button {
  background: none; border: 1px solid var(--border); border-radius: 50%;
  width: 42px; height: 42px; flex: none; display: grid; place-items: center;
  cursor: pointer; color: var(--text-dim);
}
.search-page-form button:hover { color: var(--accent); border-color: var(--accent); }

/* Filter bar (2026-09-12, REWRITTEN from a standalone pill design to
   real tabs - per the user, "like most search engines... All/Teams/
   Players/Games/Stories buttons can be tabs") - the markup's own
   .search-filter-nav class carries no rules of its own; it's
   .detail-nav itself (Team/Player/Game/Level-Hub's own tab row), reused
   wholesale rather than reinventing a second tab look. Still real <a>
   links per page - only the Time dropdown's open/close needs JS, and
   it's the same .more-toggle/.more-menu global wiring every other
   dropdown nav uses. */

/* Light-gray-bold inline date (2026-09-12, per the user - "anything with
   a date, make it light gray bold and put it inline") - a story result's
   Meta is a bare date, folded straight into its snippet line instead of
   its own row; a game result's Meta (the only other type that ever
   starts with one) keeps its own line, just with this same span wrapping
   the leading date part of it. var(--text-dim) is the same token
   .search-result-meta/.search-result-snippet already use for "light
   gray" elsewhere on this page - this only adds the bold weight on top. */
.search-result-date { color: var(--text-dim); font-weight: 700; }

/* Time dropdown's custom from/to calendars (2026-09-12, per the user -
   "two date range calendars should allow users to 'zero in' on a time
   period") - a divider above separates it from the preset links
   sharing the same dropdown.
   min-width 240px -> 200px (2026-09-12, per the user - "50%+ of the
   dropdown is EMPTY SPACE") - that 240px was sized for an input that
   stretched to fill it (width: 100%, since fixed); now the input has
   its own real 170px width, forcing the dropdown wider than 170px +
   2*8px padding = 186px just left dead space on the right of every
   row, presets included. */
/* Time dropdown's custom From/To range - REBUILT CLEAN A SECOND TIME
   (2026-09-12, per the user, explicit "START OVER AND DO IT AGAIN")
   after native <input type="date"> proved impossible to reliably
   theme: its internal digit segments and picker accent live in the
   browser's own UA shadow tree, not reachable consistently
   cross-browser no matter what was targeted at it (accent-color,
   ::-webkit-datetime-edit-* pseudo-elements, color-scheme - all
   correct, still not enough). Swapped to a plain <input type="text">
   with a YYYY-MM-DD placeholder (search-page.php) - the exact same
   control, styled the exact same way, every other input on this site
   already renders correctly (matches .auth-field input's own proven
   values below), so there is no native chrome left to fight at all.

   Also fixes a real, separate bug found in the same pass: this
   dropdown is `.detail-nav .more-menu` specifically, whose padding is
   4px on the container plus each link's OWN 6px horizontal padding
   (10px total inset) - .search-time-custom never added that same 6px,
   so the calendar section sat flush at 4px while the preset links
   above it sat at 10px. Matched here. */
/* Measured live, not guessed: input content width was 142px (menu
   154px content - form's own 12px padding). Target = 142 - 10 = 132px
   input (per the user, "SHRINK THE TEXT INPUTS BY 10PX"). Working
   backwards to the menu's own total width so the div is sized TO that,
   not left oversized by the shared .more-menu min-width: 132 + 12
   (form padding) + 8 (menu's own 4px+4px padding, SYMMETRIC - see
   below) + 2 (border) = 154px. min-width: 0 overrides the site-wide
   .more-menu { min-width: 160px } floor that was the actual reason
   every previous fixed input width still left slack after it.

   No padding-right override anymore (2026-09-12, per the user - "more
   left padding than right... shifted right 10px") - zeroing only the
   right side earlier (chasing an earlier complaint) left this box
   asymmetric against the base .detail-nav .more-menu { padding: 4px }
   (uniform), while .search-time-custom's own 6px/6px form padding
   below was already symmetric - the mismatch between those two read
   as a rightward shift. Back to symmetric on both, width bumped 4px
   (150 -> 154) to keep the input at the same measured 132px target. */
.search-time-menu { width: 154px; min-width: 0; max-width: calc(100vw - 24px); box-sizing: border-box; }
@media (max-width: 600px) {
  .search-time-menu { left: auto; right: 0; }
}
.search-time-custom {
  display: flex; flex-direction: column; gap: 8px;
  margin: 8px 0 0; padding: 8px 6px 0; border-top: 1px solid var(--border);
}
.search-time-custom label { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; font-size: var(--fs-2); color: var(--text-dim); }
/* Values match .auth-field input exactly (see that rule's own comment
   for why 8px/10px padding, 8px radius) - a proven, already-themed
   control, not a new one-off. font-size 1rem (16px), not smaller -
   iOS Safari auto-zooms the viewport on focusing any input under
   16px, confirmed live on the old date input ("clicking on the date
   picker ZOOMS IN ON IT") and just as true of a text input. */
.search-time-custom input[type="text"] {
  /* width: 100%, not a fixed px guess (2026-09-12, per the user,
     final - every fixed number tried here left a real gap between the
     input and the dropdown's own edge, because the dropdown's actual
     width is set by the PRESET LINKS above it (.more-menu's shared
     160px, wide enough for "This Month"/"All Time"), not by the date
     row - a narrower fixed-width input inside that wider box is
     structurally guaranteed to leave slack after it no matter what
     number is picked. Filling the container's real width instead
     means zero gap by construction, on any screen size, without
     guessing a pixel value ever again - this is also what actually
     fixes the reported mobile overflow, not a coincidence: dead space
     here was never inherent to a "calendar picker," it was this fixed
     width fighting its own container. text-align: right removed -
     that was answering "make it flush right" literally instead of
     the real ask, which was just "no gap." */
  width: 100%; box-sizing: border-box;
  padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--bg); color: var(--text); font-family: var(--font-body);
  /* 16px, not smaller - iOS Safari auto-zooms the viewport on focusing
     any input under 16px, confirmed live earlier on the old native
     date input and just as true of this text input. */
  font-size: var(--fs-5);
}
.search-time-custom input[type="text"]:focus { outline: none; border-color: var(--level-color, var(--accent)); }
.search-time-custom .btn { padding: 6px; font-size: var(--fs-3); }

.search-result-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
/* Fixed-width Level-pill column, LEFT of the row (round 2, 2026-09-11,
   per the user - "put the level pills on the left side and fixed-width
   them like we did on the Account/Watchlist page") - same real fix as
   .watchlist-item's own grid-template-columns: every row is its own
   independent CSS Grid, so an auto-sized pill column would size itself
   PER ROW (a "WNBA" pill vs an "MLB" pill), throwing off alignment
   between rows - a fixed px column plus justify-self: center on the
   pill itself is the established fix for this exact bug class on this
   site. */
.search-result-row {
  display: grid; grid-template-columns: 46px 22px 1fr; align-items: start; gap: 10px;
  padding: 10px 8px; border-radius: 8px; color: var(--text);
}
.search-result-row:hover { background: var(--bg-raised); text-decoration: none; }
.search-result-level-chip { font-size: var(--fs-1); padding: 1px 8px; margin-top: 2px; justify-self: center; }
.search-result-media { width: 22px; height: 22px; display: grid; place-items: center; margin-top: 2px; }
.search-result-icon { text-align: center; font-size: var(--fs-6); color: var(--text-dim); }
.search-result-logo { width: 20px; height: 20px; }
.search-result-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.search-result-name { font-weight: 600; font-size: var(--fs-4); color: var(--text); }
/* font-weight 700 (2026-09-12, per the user - "make... team records/elo
   and the player meta info" stand out, matching the inline dates'
   own bold treatment) - covers every entity type's meta line generically
   (team W-L/ELO, player height/weight/PCR, game date/score), not a
   per-type override. */
.search-result-meta { font-size: var(--fs-2); font-weight: 700; color: var(--text-dim); }
.search-result-snippet { font-size: var(--fs-3); line-height: 1.5; color: var(--text-dim); }
.search-result-snippet mark { background: color-mix(in srgb, var(--level-color, var(--accent)) 30%, transparent); color: var(--text); border-radius: 3px; padding: 0 2px; }

.theme-toggle,
.account-link {
  background: none;
  border: 1px solid var(--border);
  border-radius: 50%;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  cursor: pointer;
  color: var(--text-dim);
}
.theme-toggle:hover,
.account-link:hover { color: var(--level-color); border-color: var(--level-color); text-decoration: none; }

.account-link svg { width: 17px; height: 17px; }

/* Sun/moon toggle icons (design sweep, 2026-09-12) - explicitly sized to
   match .account-link svg's own 17px so this button reads as a sibling of
   the search/account icons on either side, not a smaller afterthought.
   Sun is the default/light-mode icon; the dark-mode overrides below swap
   to moon - same "system default stamps nothing, explicit choice stamps
   data-theme" 3-state pattern the rest of the site's CSS already follows. */
.theme-toggle-icon { font-size: var(--fs-5); }
.theme-toggle-icon-moon { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle-icon-sun { display: none; }
  :root:not([data-theme="light"]) .theme-toggle-icon-moon { display: inline; }
}
:root[data-theme="dark"] .theme-toggle-icon-sun { display: none; }
:root[data-theme="dark"] .theme-toggle-icon-moon { display: inline; }

/* ----------------------------------------------------------------- main */

.content {
  max-width: 1280px;
  margin: 0 auto;
  padding: 32px 20px 80px;
}

.placeholder-shell {
  max-width: 65ch;
}
.placeholder-shell p { color: var(--text-dim); }

/* ------------------------------------------------------------ detail pages */
/*
 * The shared system for every "inside page" - Game now, Team/Player once
 * those rounds start (per the user, 2026-08-30: "make a unified design
 * for inside pages now so we never have to talk about it again"). Reuse
 * these classes directly rather than inventing team-page/player-page
 * equivalents.
 *
 * Ground rule, learned the hard way (3 wrong attempts on the Game round
 * before this): .detail-page NEVER gets a max-width or any other
 * per-page/per-tab sizing. It fills .page-main completely, at every
 * viewport width, on every tab, exactly like the front page's own content
 * does - full-width for cards was right, a per-page 65ch/800px cap keeps
 * *seeming* right in isolation but each version broke something worse:
 * a fixed grid track made .page-sidebar visibly jump between tabs, and a
 * fixed max-width nested inside the still-growing 1fr column opened an
 * UNBOUNDED gap that widened as the viewport widened. .page-sidebar's
 * position must stay a pure function of .page-layout's own grid, never of
 * what any one tab contains. Prose content (an AI story's full text) gets
 * its OWN max-width: 65ch scoped to the text elements themselves (see
 * .detail-prose below) - natural whitespace next to a capped text column
 * is normal and expected; a *container* full of that whitespace, boxed in
 * a visible border, is what read as an ugly, broken gap.
 */
.detail-page { max-width: none; }
/* /search (2026-09-13) - was wrongly wearing .static-page's 700px cap
   (built for /about, /legal, /privacy - plain narrow prose), which left
   its .detail-nav tabs squeezed into ~650px and wrapping to a second row
   well before the viewport actually ran out of room. Same ground rule as
   .detail-page just above: fills .page-main, no page-level max-width. */
.search-page { padding-bottom: 32px; }

/* Score lives in the H1 now, present on every tab (per the user,
   2026-08-30: the old date/status-only header "has the result but not
   the score, which is useless on other tabs"). */
.detail-header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
.detail-result { font-family: var(--font-display); font-size: var(--fs-9); font-weight: 700; margin: 0; }
.detail-when { margin-left: auto; text-align: right; color: var(--text-dim); font-size: var(--fs-4); white-space: nowrap; }
/* Watchlist +/checkmark toggle (2026-09-10, per the user - "a + icon
   flush right on team/player pages"). margin-left: auto here too, not
   just relying on .detail-when's own - Player's own .detail-when is
   conditional (only renders with a current team), so when it's absent
   this needs to push itself right on its own; when .detail-when IS
   present this is a harmless no-op (no free space left to consume,
   .detail-header's own `gap` already spaces the two apart). */
.watchlist-toggle { margin-left: auto; border: none; background: none; color: var(--text-dim); cursor: pointer; padding: 6px; font-size: var(--fs-5); opacity: .55; transition: opacity .15s, color .15s; }
.watchlist-toggle:hover, .watchlist-toggle:focus-visible { opacity: 1; color: var(--level-color); }
.watchlist-toggle.is-watching { opacity: 1; color: var(--level-color); }
/* Team header's own record/streak/ELO line (2026-09-03, per the user:
   "increase text size 2px") - a modifier, not a change to .detail-when
   itself, since that class is shared with the Game header's date/status
   line, which wasn't asked to change. */
.team-header-record { font-size: var(--fs-5); }
/* Player header's own flush-right team/#/position line, matched to
   the exact same size as Team's own header record line (2026-09-06,
   per the user - "make the flush right team/num/position the same
   size as the same area on the Team pages"), rather than inheriting
   .detail-when's smaller default. */
.player-header-team { font-size: var(--fs-5); }
/* Game header's own flush-right date/playoffs/status line, matched to
   the same 15.6px size as Team/Player's own header modifiers above
   (2026-09-06, per the user - "make the top right date/playoffs/status
   the same size as we've made the teams/players"). */
.game-header-when { font-size: var(--fs-5); }
/* Season pulldown - REVISED 2026-09-03 (per the user, second pass): "on
   the year pulldown, let's remove the border; increase the size of the
   year so it's a normal-text version of the team name and make it grey.
   the chevron will be enough of a hint that it's a menu." No border/
   background box at all now - reads as a plain grey label next to the
   H1, with only the chevron signaling it's interactive. */
.detail-season-picker {
  appearance: none;
  background: none;
  border: none;
  color: var(--text-dim);
  font-family: var(--font-display);
  font-size: var(--fs-7);
  font-weight: 400;
  padding: 0 20px 0 0;
  cursor: pointer;
  background-image: linear-gradient(45deg, transparent 50%, var(--text-dim) 50%), linear-gradient(135deg, var(--text-dim) 50%, transparent 50%);
  background-position: calc(100% - 10px) 55%, calc(100% - 5px) 55%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
.detail-season-picker:hover { color: var(--text); }
/* The pill's own padding pushes it visibly low against the H1's text
   baseline (per the user, 2026-08-30) - nudged up 3px, scoped to this
   header only so .level-chip's baseline elsewhere (story cards, the
   sidebar) is untouched. */
.detail-header .level-chip { position: relative; top: -3px; }
/* REAL BUG FIXED (2026-09-05, per the user: "the logo... sits about
   5px above the top of the level pill and the H1") - .detail-header
   uses align-items: baseline for its text siblings (level pill, H1),
   but the team logo is an empty <span> with no text of its own to set
   a real baseline - the browser falls back to its bottom margin edge,
   which reads as floating high relative to the surrounding text.
   align-self: center overrides baseline for this one child only. */
.detail-header .detail-team-logo { align-self: center; }

/* The constellation nav - made visible instead of hidden behind a <select>
   like the legacy site (per the user, 2026-08-30 - the biggest
   discoverability problem with the site being cloned: "going to a game
   page is a portal to a whole constellation of subscriber-only data").
   Large groups (an AI-story set, later a Team/Player "Stats" group) get
   collapsed into a labelled pulldown rather than flattened into this row -
   reuses the header's own .more-toggle/.more-menu pattern (theme.js), no
   new JS needed. */
.detail-nav { display: flex; align-items: center; gap: 4px; margin-bottom: 20px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.detail-nav .more-wrap { position: relative; }
.detail-nav-tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  /* +2px over the old .85rem (13.6px) - found live, 2026-08-30: the
     pulldown's own item text was rendering noticeably bigger than this
     trigger label (16px, inherited default, vs 13.6px here) - raising
     the label and lowering the pulldown text (below) closes that gap
     from both sides instead of just one. */
  font-size: var(--fs-5);
  font-weight: 600;
  color: var(--text-dim);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.detail-nav-tab i { font-size: .85em; }
/* .more-toggle's own base styling is scoped to .primary-nav .more-toggle
   (the header's own dropdown) - this one lives in .detail-nav instead, so
   it needs its own <button> reset (.detail-nav-tab alone never had to
   cover background/border/cursor, since it was only ever used on <a>
   tags before this). */
.detail-nav .more-toggle { background: none; border: none; cursor: pointer; font-family: inherit; }
.detail-nav-tab:hover { color: var(--text); text-decoration: none; }
.detail-nav-tab.is-active { color: var(--level-color); border-bottom-color: var(--level-color); }
/* Padding reduced 4px (8px 10px -> 4px 6px) and text reduced 2px (16px,
   inherited, -> 14px) - found live, 2026-08-30: this pulldown's own items
   were rendering noticeably larger than the trigger label above them. */
.detail-nav .more-menu { padding: 4px; }
.detail-nav .more-menu a { padding: 4px 6px; font-size: var(--fs-4); }
.detail-nav .more-menu a.is-active { color: var(--level-color); font-weight: 600; }

/* The one card primitive every bordered box on a detail page shares -
   score/matchup summaries, meta panels, promo teasers, stat-highlight
   rows. Fills the full column width on purpose (see the ground rule
   above) - never given its own max-width. */
.detail-card {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
  margin-bottom: 24px;
}
a.detail-card { display: block; color: var(--text); }
a.detail-card:hover { border-color: var(--level-color); text-decoration: none; }

/* The one prose primitive for real article/body text on a detail page
   (an AI story's full text today; any other long-form copy later) - the
   TEXT gets a readable max-width, not its container (see the ground rule
   above for why that distinction matters). Deliberately not wrapped in
   .detail-card - a bordered box around a narrower-than-its-container text
   block is exactly what read as an ugly, broken gap; plain body copy with
   ordinary whitespace to its right doesn't. */
.detail-prose { max-width: 65ch; }
.detail-prose > * { margin: 0 0 14px; line-height: 1.6; color: var(--text-dim); }
.detail-prose > *:last-child { margin-bottom: 0; }

/* A team logo recolored to the Level's own accent, regardless of the
   source PNG's real colors (per the user, 2026-08-30: "css-transformed to
   be black/whitened and tinted the Level accent color").
   *
   * Rebuilt as a grayscale+blend "duotone" (per the user, 2026-08-31: the
   * Houston Astros logo "renders as a round dot") - the original version
   * masked purely on the source PNG's ALPHA channel, which only recovers
   * the logo's outer SILHOUETTE. A logo that's fully opaque within its own
   * outline (no internal transparency - the Astros' star-in-circle is
   * drawn in solid color, not cut out) has no alpha variation for a mask
   * to find, so it collapsed to "a solid circle" - correct outer shape,
   * zero internal detail. This version keeps that same alpha mask for the
   * SILHOUETTE (::after, still needed so the tint never bleeds past the
   * logo's real edges) but layers it over a grayscale copy of the actual
   * artwork (::before) using mix-blend-mode: color - blend:color takes
   * hue/saturation from the top layer (--chip-color) and LUMINANCE from
   * the layer beneath it, so internal light/dark detail from the source
   * image (the star, lettering, shading) survives the tint instead of
   * being flattened to one flat shape. isolate confines that blending to
   * these two layers, not the page behind them. */
.detail-team-logo {
  display: inline-block;
  position: relative;
  width: 28px;
  height: 28px;
  flex: none;
  isolation: isolate;
}
.detail-team-logo::before,
.detail-team-logo::after {
  content: '';
  position: absolute;
  inset: 0;
  background-image: var(--logo-url);
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}
/* mix-blend-mode: color takes hue/saturation from the tint layer but
   LUMINANCE from this grayscale layer beneath it - a logo that's
   naturally very dark (per the user, 2026-08-31: the Yankees' navy
   interlocking NY "was medium-green, now almost black") stays dark no
   matter how bright --chip-color is, since blend:color can't lighten a
   near-black backdrop pixel - and the inverse is just as real: a logo
   with a naturally light average (found live, 2026-08-31: the Reds/Cubs
   logos rendered with no visible tint at all, not even washed-out grey)
   stays washed toward white, since blend:color can't darken a near-white
   backdrop either. The original fix here (contrast(.7) then
   brightness(1.5)) only protected the dark floor - the extra brightness
   multiply, applied uniformly after contrast already did its job, pushed
   already-light logos straight past the point blend:color has anything
   left to work with. contrast() alone is symmetric around mid-grey (a
   true-black source and a true-white source get pulled toward the same
   center by the same amount), so a single lower contrast value protects
   both extremes at once without needing a brightness correction that can
   only ever help one side while hurting the other. */
.detail-team-logo::before {
  filter: grayscale(1) contrast(.4);
}
.detail-team-logo::after {
  background-image: none;
  background-color: var(--chip-color);
  mix-blend-mode: color;
  -webkit-mask-image: var(--logo-url);
  mask-image: var(--logo-url);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}

/* Missing-logo fallback (design-sweep punchlist, 2026-09-08 - "every
   missing team logo falls back to a colored white NatStat logo,
   matching the site header"). logo-fallback.js adds this class once a
   background Image() probe confirms --logo-url's own PNG 404s.
   REVISED same day, second round - "why isn't the NatStat fallback
   logo colored like the other logos": the first version dropped the
   tint/mask layers entirely (reasoning that a 404 gives them nothing to
   mask against) and fell back to a single FLAT, untinted background-
   image - real per the "matching the site header" framing (the header's
   own brand mark is genuinely flat/uncolored) but inconsistent with
   every REAL team logo on the page, which IS tinted. Fixed by pointing
   the SAME ::before/::after mask+tint pair real logos already use
   (this file's own docblock above) at the brand mark file directly
   instead of --logo-url - a real, valid PNG with its own alpha channel
   (confirmed live: a genuine circular cutout, not a solid rectangle),
   so the mask technique works on it exactly like it does on a real
   team logo, just masking the brand mark's shape instead of a team's. */
/* REAL bug fixed (design sweep, 2026-09-11 - "the icon-not-found
   fallback icon... isn't color-tinted to the Level color"): the
   original rule set `background-image` on BOTH ::before AND ::after via
   one combined selector. Real team logos deliberately give ::after
   `background-image: none` (this file's own base .detail-team-logo::after
   rule) - it's meant to be a PURE color+mask layer, tinted only via
   background-color + mask-image, so mix-blend-mode:color has something
   achromatic-vs-colored to actually blend. Giving ::after a real
   background-image too painted the flat brand-mark PNG pixels (white/
   black, no hue) OVER --chip-color (background-image always layers
   above background-color), completely hiding the tint - blend:color
   then had only an achromatic image to blend against an achromatic
   ::before layer, so the result was always grey/white, on every
   Level, regardless of --chip-color. Confirmed live by forcing
   mix-blend-mode:normal on ::after and seeing a flat WHITE circle, not
   orange - proof ::after's own painted content was the plain image,
   not the color. Fixed by giving ::before the image and ::after ONLY
   the mask (::after keeps background-image: none from the base rule,
   never overridden here) - the exact same shape every real team logo
   already uses. */
.detail-team-logo.logo-fallback::before {
  background-image: url('/assets/img/logo-mark-black.png');
}
.detail-team-logo.logo-fallback::after {
  -webkit-mask-image: url('/assets/img/logo-mark-black.png');
  mask-image: url('/assets/img/logo-mark-black.png');
}
.detail-team-logo.logo-fallback { opacity: .55; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .detail-team-logo.logo-fallback::before { background-image: url('/assets/img/logo-mark-white.png'); }
  :root:not([data-theme="light"]) .detail-team-logo.logo-fallback::after {
    -webkit-mask-image: url('/assets/img/logo-mark-white.png');
    mask-image: url('/assets/img/logo-mark-white.png');
  }
}
:root[data-theme="dark"] .detail-team-logo.logo-fallback::before { background-image: url('/assets/img/logo-mark-white.png'); }
:root[data-theme="dark"] .detail-team-logo.logo-fallback::after {
  -webkit-mask-image: url('/assets/img/logo-mark-white.png');
  mask-image: url('/assets/img/logo-mark-white.png');
}

/* Logo/name, then the record/streak/ELO line, then the linescore - all
   ONE container (per the user, 2026-08-30: "put the linescore in the
   container div"), not a separate bare table floating below the card. */
.game-matchup { margin-bottom: 12px; padding: 16px; }
.game-team { display: flex; align-items: center; gap: 10px; padding: 6px 0; }
.game-team-name { font-family: var(--font-display); font-size: var(--fs-6); font-weight: 700; }

/* Team/player name links, site-wide (2026-09-03, per the user's design
   pass). Two deliberately different treatments: `.team-link-accent` is
   ALWAYS level-colored (the Overview linescore's own team names);
   `.team-link`/`.player-link` stay whatever color they already are
   (white on a dark boxscore heading, inherited text color elsewhere) and
   only pick up the level color on hover. Neither ever gets an
   underline or forced bold - the point is to carry through the existing
   look, not decorate it. */
a.team-link-accent { color: var(--chip-color); text-decoration: none; }
a.team-link-accent:hover { text-decoration: none; }
a.team-link, a.player-link { color: inherit; text-decoration: none; font-weight: inherit; }
a.team-link:hover, a.player-link:hover { color: var(--chip-color); text-decoration: none; }
/* The final score, for a game with no LineVis/LineHome to build a real
   per-inning grid from (per the user, 2026-08-31, on a real AmerBB game
   showing team names/records with no score anywhere: "if there is no
   linevis/linehome, the score doesn't seem to show up") - pushed to the
   row's own right edge, same weight/tabular-nums as the linescore grid's
   own "R" total for the games that DO have one. */
.game-team-score { margin-left: auto; font-size: calc(.85rem + 4px); font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
/* "85-51 · W4 · #2 (1377 ELO +3)" - below the team name, indented under
   it (38px = logo width + gap) when a logo is actually there, not
   flush-right beside it (per the user, 2026-08-30). */
.game-team-info { margin: 0 0 8px 38px; font-size: var(--fs-3); color: var(--text-dim); font-variant-numeric: tabular-nums; }
/* No logo (common for lower levels like MiLB - teams.LogoURL is
   genuinely blank for them, not a broken path) means nothing for the
   38px to align under, so it just reads as an unexplained indent - flush
   left instead (per the user, 2026-08-31). */
.game-team-info-no-logo { margin-left: 0; }

/* Real per-inning/period linescore (LineVis/LineHome), not just the final
   score - per the user, 2026-08-30: "line scores... in grey, total in
   white, hits and errors for baseball". ONE CSS Grid, identity cell +
   score cells sharing the same row per team (per the user, 2026-08-30:
   "put the linescore in the container div... NOT as a duplicated set of
   lines" - an earlier version repeated the team name as its own table
   row label, right under the identity block that already named it).
   grid-template-columns is set inline per-game (period count varies), so
   the header row and both team rows always share the same column widths. */
.game-linescore-grid { display: grid; row-gap: 4px; column-gap: 6px; align-items: center; margin-top: 4px; }
/* +4px and bold (per the user, 2026-08-30) - .game-linescore-head below
   overrides size/weight back down for the header row (inning numbers,
   R/H/E labels), so this only actually lands on the real score digits. */
.game-linescore-cell { text-align: center; font-size: calc(.85rem + 4px); font-weight: 700; color: var(--text-dim); font-variant-numeric: tabular-nums; opacity: .8; }
.game-linescore-head { font-weight: 600; font-size: var(--fs-2); }
/* Hits/errors distinguished from the plain per-inning digits by real
   COLOR now, not just opacity (per the user, 2026-08-31: on a real dark-
   mode screenshot, --text-dim at .8 vs 1.0 opacity turned out to render
   as visually identical grey against the card's own dark background -
   opacity alone wasn't a strong enough signal). var(--text) (not bold)
   sits as a genuine middle step between the dim inning digits and the
   bold-white total. */
.game-linescore-he { opacity: 1; color: var(--text); font-weight: 600; }
/* Full opacity - only the per-inning digits grey out further, not the
   real total (per the user, 2026-08-30: "grey out the line score more"). */
.game-linescore-total { color: var(--text); font-weight: 700; opacity: 1; }
.game-linescore-total-head { color: var(--text-dim); }
/* Shares the header row's otherwise-empty identity cell (per the user,
   2026-08-31: playoff series/game "in the space above the teams on the
   same line as the period/inning indicators, in light gray bold") -
   overrides .game-linescore-cell's big centered score-digit sizing back
   down to a small left-aligned label, since this cell sits above the
   team names, not above a column of numbers. */
.game-linescore-series { text-align: left; font-size: var(--fs-2); font-weight: 700; color: var(--text-dim); opacity: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* League/sublevel fallback (2026-09-13) - same cell/size as the playoff series line above, but normal weight per the user, since this is the everyday case rather than the rarer, more specific series info. */
.game-linescore-series-league { font-weight: 400; }
.game-linescore-identity { grid-row: span 1; }

/* Lets the linescore grid scroll horizontally on narrow viewports rather
   than forcing the whole .game-matchup card wider than its column. */
.table-wrap { overflow-x: auto; }

/* Top Statlines/Forecasting subheads (per the user, 2026-08-31: both
   "smaller than the summary headline") - deliberately h3, not h2, so
   they sit visually below the AI recap's real h2 without needing a
   one-off font-size override on every use. */
/* 1.3rem, not the earlier +2px bump (per the user, 2026-08-31: they
   were reading the same size as .game-venue's 1.05rem, both bold, and
   need real separation without reaching all the way up to h2's clamp). */
.section-subhead { display: flex; align-items: center; gap: 8px; font-size: var(--fs-7); font-weight: 700; margin: 0 0 10px; color: var(--text); }
.section-subhead i { color: var(--text-dim); font-size: .85em; }
/* Design sweep, 2026-09-11, real bug caught live: Team Overview's own
   "Schedule" subhead is the one section-subhead site-wide that's wrapped
   in a real <a> (team-page.php) - the global `a { color: var(--level-
   color) }` rule was winning over .section-subhead's own white, so this
   one subhead rendered level-colored while every sibling ("Season",
   "Forecasting", "Team Leaders", etc.) stayed white. Same inherit-at-
   rest/level-color-on-hover convention .team-link/.player-link already
   use, so it still reads as clickable without being permanently tinted. */
.section-subhead a { color: inherit; text-decoration: none; }
.section-subhead a:hover { color: var(--level-color); text-decoration: none; }

/* A grid, not a single dense text line (per the user, 2026-08-31: "we
   can stretch this out more on a wider canvas") - same card look as
   .game-statlines li, but items flow across the column's full width
   instead of stacking one per row. */
.game-forecast-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 8px; margin: 0 0 24px; }
.game-forecast-item { display: flex; flex-direction: column; gap: 4px; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius); }
/* Record/ELO/ELO Rank boxes link out on the Team Overview (2026-09-04,
   per the user) - same box, just also a link; hover borrows the
   level-color-on-hover convention .team-link/.player-link already use
   elsewhere rather than inventing a new hover treatment. */
a.game-forecast-item-link { text-decoration: none; color: inherit; }
a.game-forecast-item-link:hover { border-color: var(--level-color); text-decoration: none; }
a.game-forecast-item-link:hover .game-forecast-value { color: var(--level-color); }
.game-forecast-label { font-size: var(--fs-2); text-transform: uppercase; letter-spacing: .04em; color: var(--text-dim); font-weight: 600; }
.game-forecast-value { display: flex; align-items: center; gap: 6px; font-size: var(--fs-5); font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
/* Win pct in parens next to a Forecasting W-L record (2026-09-06, per
   the user - "grey normal in parens") - a lighter weight/dimmer color
   than the bold record it sits next to, not a second equally-loud
   number. */
.game-forecast-value-sub { color: var(--text-dim); font-weight: 400; }
.game-forecast-correct { color: #22c55e; }
.game-forecast-incorrect { color: #ef4444; }
/* Movement line, in the level's own accent so it reads as "this game's
   own history" rather than a generic chart color. */
.game-forecast-spark { width: 60px; height: 20px; margin-top: 2px; color: var(--level-color); }

/* Team Schedules - two boxes side by side, one per team (per the user,
   2026-08-31: "two boxes, two teams, seven games each"), each a compact
   7-row window paged across a wider hidden set via the up/down carets
   (game-schedule-pager.js). Stacks to one column narrow. */
.game-schedule-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 0 0 24px; }
.game-schedule-box { border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; }
.game-schedule-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 0 0 8px; }
.game-schedule-team { display: flex; align-items: center; gap: 8px; margin: 0; font-size: var(--fs-5); }
.game-schedule-team .detail-team-logo { width: 20px; height: 20px; }
/* Carets flush right of the team name (per the user, 2026-08-31: "up
   and down carets flush right that go back and forward 7 games"). */
.game-schedule-carets { display: flex; gap: 2px; }
.game-schedule-caret { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; border: 1px solid var(--border); border-radius: 6px; background: none; color: var(--text-dim); font-size: var(--fs-1); cursor: pointer; }
.game-schedule-caret:hover:not(:disabled) { color: var(--text); border-color: var(--text-dim); }
.game-schedule-caret:disabled { opacity: .35; cursor: default; }
.game-schedule-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.game-schedule-list li { padding: 0; font-size: var(--fs-4); border-bottom: 1px solid var(--border); }
/* The 7-row window is a real class from first paint (per the user,
   2026-08-31: it "flashes all the pre-fetched games on load" - JS
   toggling inline display: none only after running left every one of
   the up-to-43 pre-fetched rows visible for a moment first). The server
   already decides which rows start visible (game-page.php); this class
   is just what both the initial HTML and game-schedule-pager.js's later
   clicks toggle. */
.game-schedule-hidden { display: none; }
/* One shared layout class for every row - link or current - so they're
   guaranteed identical padding/gap (per the user, 2026-08-31: the
   current row was rendering "completely different... thinner, no
   padding" - a same-looking-but-lower-specificity modifier class was
   silently losing to the base `li` rule on padding). .game-schedule-
   current adds ONLY a background tint on top of this, nothing layout-
   related, so there's no specificity fight to lose. 5px (not 6px)
   vertical padding, per the user: "1px too much top/bottom". */
.game-schedule-row { display: flex; align-items: center; gap: 8px; padding: 4px 4px; color: inherit; text-decoration: none; border-radius: 6px; }
/* Matches the site's existing clickable-list-row hover (.elo-list li a,
   .news-card, .scoreboard-card, .upcoming-card all use this same 10%
   level-tint) - per the user, 2026-08-31: "the tabs have it, these
   should too". var(--bg) was invisible here since it's the exact same
   token the surrounding page background already uses. */
a.game-schedule-row:hover { background: color-mix(in srgb, var(--level-color) 10%, transparent); text-decoration: none; }
.game-schedule-date { color: var(--text-dim); width: 72px; flex: none; font-variant-numeric: tabular-nums; }
.game-schedule-time { color: var(--text-dim); width: 66px; flex: none; font-variant-numeric: tabular-nums; }
.game-schedule-opp { flex: 0 1 auto; min-width: 0; color: var(--text); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Opponent mini-icon (2026-09-04, per the user's team-sweep feedback:
   "opponent mini-icons") - reuses .detail-team-logo's own real
   colored-tint treatment (grayscale base + --chip-color mask, see that
   class's own docblock) instead of a flat, uncolored background-image -
   a plain background-image left these looking washed-out/monochrome,
   per the user: "logos on schedule/overview aren't colored". Also used
   inside the .game-stat-grid table cells below now - vertical-align
   keeps it on the same baseline as the opponent text next to it
   (a table cell isn't a flex container the way the old row was). */
.game-schedule-opp-logo { width: 16px; height: 16px; vertical-align: middle; margin-right: 2px; }
/* Score value flush left, W/L/T badge flush right, both inside one
   fixed-width box (2026-09-06, per the user - "the style you created
   for the schedule page (score flush left, result flush right)... can
   [it] be added to... the Team Schedules on game as well, they're
   currently white scores and all over the place horizontally"). Same
   trick .game-schedule-grid-score already uses on the real <table>
   version of this list - a fixed width here does the equivalent job a
   table's own column-width auto-sizing does for free, so "3-2" and
   "22-0" both land the result badge at the exact same x position down
   the whole list instead of drifting with the score's own text width. */
/* margin-left: auto (design-sweep punchlist, 2026-09-09 - "the floating
   broken score/indicator div... should have the score/WLT indicator
   column on the far right, double-flushed like the team schedule page")
   - none of this row's other children (.game-schedule-opp included) has
   a flex-grow, so with no explicit push the score sat wherever the
   preceding content happened to end (short opponent names left a big
   gap to the row's real right edge) instead of pinned there - the
   classic single-flex-item "push to the end" fix. */
.game-schedule-score { display: flex; align-items: center; justify-content: space-between; width: 64px; flex: none; gap: 4px; font-variant-numeric: tabular-nums; color: var(--text); margin-left: auto; }
.game-schedule-result { width: 18px; flex: none; text-align: center; font-weight: 700; border-radius: 4px; font-size: var(--fs-2); }
.game-schedule-result-w { color: #22c55e; }
.game-schedule-result-l { color: #ef4444; }
/* Tie/draw (2026-09-06, per the user's UI-conventions pass - "D/T...
   grey") - var(--text-dim), not a literal grey hex, so it stays correct
   in both themes. */
.game-schedule-result-t, .game-schedule-result-d { color: var(--text-dim); }
/* Team Overview's Schedule box + the Schedule tab REVISED 2026-09-05,
   per the user: a real `.game-stat-grid` <table> now, not the flexbox
   row list above (still used by game-page.php's own simpler Team
   Schedules strip, unaffected by this) - a flex row can't give real
   column alignment once content length (top statline, running record)
   varies row to row; a table can, for free, and it matches every
   other stat screen on the site instead of being its own bespoke
   layout. Whole-row click-through via a plain `onclick` on `<tr>`
   (same inline-navigation convention team-header.php's own season
   `<select onchange="...">` already uses), not a `<tr>` wrapped in an
   `<a>`, which isn't valid HTML. */
.game-schedule-grid-row { cursor: pointer; }
.game-schedule-grid-row:hover { background: color-mix(in srgb, var(--level-color) 10%, transparent); }
.game-schedule-grid-top { text-align: left !important; color: var(--text-dim); }
/* "White normal" (2026-09-05, per the user: "all other pages have
   statlines as white normal name... apply the universal styles we've
   been working on with this") - matches .game-meetings-top-performer-
   name's own treatment exactly (full --text brightness, weight 400,
   NOT bold) rather than the bolder convention a couple of other
   statline spots on the site still use. */
.game-schedule-top-inline-name { color: var(--text); font-weight: 400; }
/* Flushed left (2026-09-05, per the user: "flush that column left
   since it's not numeric" - "5-3 W" is a compound string, center-
   aligning it like a real number column reads oddly) with real gap
   before the W/L badge ("add some more space between the score and
   the W/L/T indicator" - a single text-node space read as cramped). */
/* REVISED 2026-09-05, per the user: "flush left the score and flush
   right the w/l/t indicator in that column so it doesn't float around
   with different sized scores" - the score value and the result badge
   are two flex children with space-between now, pinned to opposite
   edges of the column regardless of how many digits the score has,
   rather than sitting side-by-side with a fixed gap that let the whole
   pair drift depending on the score's own width. */
/* Explicit width (2026-09-06, found live: future/empty games were
   collapsing this column to near-zero) - overriding a <td>'s display to
   flex drops it out of the table's own shared column-width computation
   in at least one real browser engine, so an empty cell with no content
   to size against shrank independently of its populated siblings even
   though every column in this table is supposed to share one width.
   A hard width sidesteps that entirely, same fix already applied to
   .game-schedule-score just above for the same underlying reason. */
/* Playoff series status, e.g. "@ Philadelphia 4-2" (design-sweep
   punchlist, 2026-09-10) - small text in the current Level's own accent
   color, right after the opponent name. */
.game-schedule-series-status { color: var(--level-color); font-size: var(--fs-2); font-weight: 400; }
/* Widened 64px -> 90px (design-sweep punchlist, 2026-09-10 - "for
   basketball scores the double-flush score column is too small, a score
   like 108-100 W smashes together") - baseball's own shorter scores
   still fit fine with the extra room, this isn't a sport-conditional
   fix. */
.game-schedule-grid-score { display: flex; align-items: center; justify-content: space-between; width: 90px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.game-schedule-grid-score-value { text-align: left; }
.game-schedule-grid-score .game-schedule-result { display: inline-block; width: auto; }
/* The current game reads as a real anchor point, not just another row
   (per the user, 2026-08-31: "a slight shade behind it instead of bold
   stuff") - background only, every row keeps the exact same weight.
   --bg-raised, a real distinct surface color, not --bg (per the user,
   2026-08-31: "not seeing the shade" - --bg IS the page's own
   background, so it was blending in completely). */
.game-schedule-current { background: var(--bg-raised); }
@media (max-width: 640px) {
  .game-schedule-grid { grid-template-columns: 1fr; }
}

.game-meta { margin-bottom: 24px; color: var(--text-dim); font-size: var(--fs-4); }
.game-meta p { margin: 0 0 4px; }
/* +2px (.9rem inherited from .game-meta -> 1.05rem) and bold white - per
   the user, 2026-08-30. */
.game-venue { font-size: var(--fs-5); font-weight: 700; color: var(--text); }
.game-attendance-num { color: var(--text); font-weight: 700; }
/* No separate font-size override - inherits .game-meta's .9rem, same
   size as the Attendance line right above it (per the user, 2026-08-30:
   this was rendering noticeably smaller at .8rem). */

/* Full page width (not .detail-prose's 65ch prose cap) - the excerpt was
   reading "2/3 width" next to the sidebar (per the user, 2026-08-30). */
/* Tightened ~5px off the default h1/h2/h3 margin (2026-09-06, per the
   user: "the padding-bottom on the recap headline is a little
   extreme... so it's imbalanced") - the headline should sit close to
   its own body text below it, not read as evenly boxed between the nav
   line above and the recap text below. calc() keeps this "5px less
   than normal" rather than a flat value, since the base rule's .5em is
   itself relative to h2's own clamped, viewport-dependent font-size.
   ROOT CAUSE of this taking 3 rounds to actually land: no `<p>` margin
   reset exists anywhere on the site, so `.game-recap`'s own inner <p>
   was rendering with the browser's default ~1em top margin - which,
   since `.game-recap` itself has no padding/border, collapses straight
   through the div and merges with this h2's margin-bottom (to the
   LARGER of the two, not additive). Every prior edit here was tuning a
   value the paragraph's own default margin was already dwarfing -
   the real fix is neutralizing that default via `.game-recap p` below,
   so this margin is finally the only one in play. */
.game-recap-headline { margin-bottom: calc(.5em - 5px); }
/* "2 days ago" eyebrow, inline before the excerpt's own journalistic
   dateline ("MILWAUKEE (Interstat) - ...") - not the headline (2026-09-06,
   per the user, after an earlier attempt put it in the headline instead;
   Team Overview only, since the Game page's own recap already sits under
   a full dateline in its header and doesn't need this). Same grey as
   every other subheader (see the global h2 rule's own note), bold to
   read as a real label, not body text. */
.game-recap-when { font-weight: 700; color: color-mix(in srgb, var(--text-dim) 50%, var(--text) 50%); margin-right: 8px; }
.game-recap { color: var(--text-dim); line-height: 1.6; margin: 0 0 8px; }
.game-recap p { margin-top: 0; }
/* Inline with the excerpt, not a separate line below it (per the user,
   2026-08-31: the old standalone "Read the full {type} ->" line read as
   its own section header, confusing next to Top Statlines right under
   it). */
.game-recap-more { font-weight: 600; white-space: nowrap; }
/* Separates the editorial recap from the data sections below it (per the
   user, 2026-08-31: "cinch up the Overview design... a thin HR below the
   recap preview"). */
.game-section-divider { border: none; border-top: 1px solid var(--border); margin: 20px 0 24px; }

/* Player flush-left, statline flush-right - no colon between them (per
   the user, 2026-08-30), same row shape as .game-boxscore-list. */
.game-statlines { list-style: none; margin: 0 0 24px; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.game-statlines li { display: flex; justify-content: space-between; gap: 12px; font-size: var(--fs-4); padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--radius); }
/* Team logo left of the name, abbrev right of it (per the user,
   2026-08-30). */
.game-statlines-name { display: flex; align-items: center; gap: 8px; }
.game-statlines-name .detail-team-logo { width: 20px; height: 20px; }
.game-statlines-player { font-weight: 600; }
.game-statlines-team { color: var(--text-dim); font-size: var(--fs-2); font-weight: 600; }
.game-statlines-line { color: var(--text-dim); font-variant-numeric: tabular-nums; text-align: right; }

.detail-card.game-story-teaser h2 { font-family: var(--font-display); font-size: var(--fs-7); margin: 0 0 8px; max-width: 65ch; }
.detail-card.game-story-teaser p { margin: 0; color: var(--text-dim); max-width: 65ch; }

/* Centered as ONE block (image+headline+prose+credit together), not
   left-aligned - found live, 2026-08-30: a narrow text column flush
   against the left edge of a much wider column reads as a lopsided,
   broken gap; the same narrow column centered within that space reads as
   an intentional, ordinary article layout. Doesn't touch .page-main/
   .page-layout, so .page-sidebar's position is unaffected - same fix
   shape as the "CMS-style container" the user asked for, just scoped to
   this one article block instead of the whole grid. Widened +100px over
   the base 65ch (per the user, 2026-08-30) - a fixed px value here
   instead of a "+100px" ch-relative hack, since ch isn't meant to be
   added to like this. */
/* +50px again (per the user, 2026-08-30: "add 50px to the width... line
   length is still not too long on wider screens") - on top of the
   original +100px from the round before. */
.game-story-full { max-width: calc(65ch + 150px); margin: 0 auto; }
.game-story-full { position: relative; }
.ai-wand-tab { margin-left: auto; width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--panel-border); background: var(--panel); color: var(--accent); font-size: var(--fs-4); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: transform .15s, background .15s; }
.ai-wand-tab:hover { transform: scale(1.08); background: var(--border); }
.ai-wand-tab:disabled { opacity: .5; cursor: default; transform: none; }
.ai-wand-btn {
  position: absolute; top: 0; right: 0; z-index: 2;
  width: 36px; height: 36px; border-radius: 50%;
  border: 1px solid var(--panel-border); background: var(--panel);
  color: var(--accent); font-size: var(--fs-5); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: transform .15s, background .15s;
}
.ai-wand-btn:hover { transform: scale(1.08); background: var(--border); }
.ai-wand-btn:disabled { opacity: .5; cursor: default; transform: none; }

.game-story-full .story-headline { font-size: var(--fs-8); margin-bottom: 16px; }
/* NOT .detail-prose - no second max-width here. .game-story-full above is
   already this article's one deliberate width; the body copy should fill
   it edge to edge, not get re-narrowed by an independent 65ch cap (per
   the user, 2026-08-30 - see the partial's own note). */
.game-story-body > * { margin: 0 0 14px; line-height: 1.6; color: var(--text-dim); }
.game-story-body > *:last-child { margin-bottom: 0; }
.game-story-image { aspect-ratio: 16 / 9; border-radius: var(--radius); overflow: hidden; margin-bottom: 16px; background: var(--border); }
.game-story-image img { width: 100%; height: 100%; object-fit: cover; display: block; }

.game-boxscore {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--fs-2);
  line-height: 1.5;
  /* pre-wrap, not pre (found live, 2026-08-30: the fixed-width text
     rendered ~5000px wide with half the content effectively invisible
     off-screen instead of wrapping) - breaks the source's fixed-column
     character alignment on narrow viewports, but per the user, that's
     the right tradeoff over unreadable overflow. */
  white-space: pre-wrap;
  overflow-x: auto;
  background: color-mix(in srgb, var(--text) 4%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
  margin-bottom: 24px;
}

/* The Boxscore tab's real per-sport stat grid (performances{Level}'s own
   Stat/StatLabel columns, not the flat BoxscoreText nor a single squashed
   "Player: Statline" list - per the user, 2026-08-30: "these are
   statlines, not a boxscore. create a grid for each sport" - see
   Game::boxscoreColumns() for the column source). One real HTML table per
   team per PlayerType group (Batting/Pitching for baseball, one group for
   sports with no such split). */
/* One size for the body, header row excepted (per the user, 2026-08-31:
   first "just giving up and saying 'make it all one size' is the right
   play" - then, once live, "one-text-size on the headers is blowing open
   the table columns and causing vertical scroll... one size except for
   the headers, -2 and normal text" - a header at full body size needs
   more width than its own short column of digits, so the column grows to
   fit the header instead of the data). This had drifted through
   .72/.78/.85rem across th/td/position before settling here. */
.game-stat-grid { width: 100%; border-collapse: collapse; margin-bottom: 20px; font-size: var(--fs-4); }
/* 7px, not 14px, horizontal padding (per the user, 2026-08-31: "the
   number column widths are pretty big, they can be de-widthed by 15px as
   the whole box isn't visible until 1400px wide") - ~15px narrower per
   column (7px shaved off both sides). Header text -2px and normal
   weight, not the grid's own .9rem/600 (per the user, 2026-08-31, just
   above) - dim color alone still marks it as a header. */
/* Header vs. number contrast (2026-09-03, per the user's sweep -
   "darken the header text slightly and lighten the stat numbers," also
   applies to boxscores since this class already serves both) - no
   third, dimmer-than---text-dim token exists, so headers blend
   further toward transparent instead; numbers go to the full --text
   brightness rather than --text-dim, since the actual data deserves
   more visual weight than its own label. */
.game-stat-grid th { color: color-mix(in srgb, var(--text-dim) 65%, transparent); font-weight: 400; text-align: center; padding: 4px 7px; font-size: calc(.9rem - 2px); text-transform: uppercase; letter-spacing: .02em; border-bottom: 1px solid var(--border); }
/* REVISED 2026-09-04, per the user: stat numbers at full --text
   brightness read the same weight as player names (.game-stat-grid-
   player forces --text !important), no contrast between the row's
   real anchor (the name) and its supporting data - dialed back to
   --text-dim so names stay the visual lead. The sorted column
   (.is-sorted-col below) still goes full-bright+bold on top of this,
   that's deliberate active-sort emphasis, not the same thing. */
.game-stat-grid td { text-align: center; padding: 4px 7px; color: var(--text-dim); font-variant-numeric: tabular-nums; border-bottom: 1px solid var(--border); }
.game-stat-grid-player { text-align: left !important; font-weight: 600; color: var(--text) !important; white-space: nowrap; }
/* Team abbreviation + minilogo (design-sweep punchlist, 2026-09-08 -
   "team abbreviations need to have minilogos and grey bold abbrevs
   like the game php page") - same recipe as .game-pbp-team, generalized
   under its own name since it's no longer pbp-specific once the Stats
   tab's roster-scope tables reuse it too. */
.stat-grid-team-abbrev { display: inline-flex; align-items: center; gap: 5px; font-weight: 600; color: var(--text-dim); }
.stat-grid-team-abbrev .detail-team-logo { width: 14px; height: 14px; }
/* Level-colored variant (design-sweep punchlist, 2026-09-08 - PCR's own
   Team column was plain unstyled text, "not in site style: minilogo,
   bold abbreviation in level color") - same minilogo+layout recipe as
   the dim default above, `.team-link-accent`'s own "ALWAYS level-
   colored" treatment for the text color instead of --text-dim. */
.stat-grid-team-abbrev-accent { color: var(--chip-color); }
/* White, normal weight (not bold like .game-stat-grid-player) - Stat
   Rankings' own friendly-name column (2026-09-06, per the user - "Stat
   name... first column in white normal text"). */
.game-stat-grid-name { text-align: left !important; font-weight: 400; color: var(--text) !important; white-space: nowrap; }
/* Keeps a short "-"-joined combo (e.g. "CF-LF") from wrapping onto two
   lines (2026-09-06, per the user - Career Glance's Position column). */
.game-stat-grid-nowrap { white-space: nowrap; }
/* Performance Score - always grey/bold regardless of value (2026-09-06,
   per the user), same as every other "this is a computed summary
   metric" treatment on the site, not a raw box-score number. */
.game-stat-grid-ps { font-weight: 700; color: var(--text-dim); }
/* Generic left-align for any non-numeric, non-sortable-by-magnitude
   column (2026-09-05, per the user: "flush the 'held by' column left,
   it's text - goes for anything that isn't specifically numeric or
   math-sortable") - .game-stat-grid-player already does this but also
   forces bold/full-bright text, appropriate for a player NAME
   specifically, not for prose-y text like a list of record-holders. No
   font-weight/color override here, just the alignment. */
.game-stat-grid-text { text-align: left !important; }
/* Superlatives range chevron (2026-09-05, per the user - replaces a
   repeated "(High)"/"(Low)" text suffix). */
.game-superlatives-range-icon { color: var(--text-dim); font-size: .78em; padding-right: .35em; }
/* Uniform number, left of the name (per the user, 2026-08-30) - blank
   when this player has none rather than a placeholder dash. */
.game-stat-grid-no { text-align: center !important; color: var(--text-dim); font-variant-numeric: tabular-nums; width: 1%; white-space: nowrap; }
/* Position, grey, right of the name (per the user, 2026-08-30). */
.game-stat-grid-position { margin-left: 6px; color: var(--text-dim); font-weight: 400; text-transform: none; }
/* Non-starters indented (per the user, 2026-08-30: baseball only,
   Starter <> 'Y') - a real lineup card shows who started at a glance. */
.game-stat-grid-player-sub { padding-left: 26px !important; }
/* Non-starters faded, not indented (per the user, 2026-08-31, on a
   basketball boxscore: "slightly fade non-starters" - then, once live,
   "just their names, not their stats") - any sport but baseball that
   tracks Starter (game-boxscore-tab-page.php's own $groupHasStarterData
   gate). A real color change on just the name cell, not row opacity -
   the stat digits stay full-strength var(--text-dim) either way, only
   the player name itself dims to mark who didn't start. */
.game-stat-grid-player-faded { color: var(--text-dim) !important; }
.game-boxscore-team-heading { display: flex; align-items: center; gap: 10px; }
.game-boxscore-team-heading .detail-team-logo { width: 24px; height: 24px; }
/* Projected score badge on the "Projected Box" team headings (per the
   user, 2026-09-01) - same weight/color/opacity as the Overview
   linescore's own per-period digits (.game-linescore-cell), not the bold
   full-color final-score treatment, so a real recorded run always reads
   as more authoritative than a projected one. */
.game-boxscore-projected-score { font-weight: 700; color: var(--text-dim); opacity: .8; font-variant-numeric: tabular-nums; }
/* Totals row (per the user, 2026-08-30: "a boxscore should total its
   columns") - bold/white and a heavier top border to read as a real sum,
   not just another player row. */
.game-stat-grid tfoot td { font-weight: 700; color: var(--text); border-top: 2px solid var(--border); border-bottom: none; }
/* Sortable columns (stat-grid.js) - click once for descending, again for
   ascending (per the user, 2026-08-30). Every sortable header reserves
   space for the arrow at all times (opacity, not content/visibility) -
   only showing/hiding it on click was letting the column change width on
   every click, which nudged the whole grid (per the user, 2026-08-30:
   "the arrow won't blow open its column and change the size of the whole
   grid every time one's clicked on"). Reusing the arrow's always-reserved
   space as a faint "ghost" hint that the column IS sortable (per the
   user, 2026-08-30) - var(--text) is near-black in light mode and
   near-white in dark, so a low opacity of it reads faintly against
   either theme's background without hardcoding a color per theme. */
.game-stat-grid th.is-sortable { cursor: pointer; user-select: none; }
.game-stat-grid th.is-sortable:hover { color: var(--text); }
.game-stat-grid th.is-sortable::after { content: '▾'; margin-left: 4px; color: var(--text); opacity: .2; }
.game-stat-grid th[data-sort-dir]::after { opacity: .8; }
.game-stat-grid th[data-sort-dir="asc"]::after { content: '▴'; }
/* The currently-sorted column reads white/bold all the way down, header
   included (per the user, 2026-08-30: "show currently sorted column in
   white bold") - stat-grid.js applies .is-sorted-col to that column's
   own <td>s on every sort. */
.game-stat-grid th[data-sort-dir] { color: var(--text); }
.game-stat-grid td.is-sorted-col { color: var(--text); font-weight: 700; }


/* The Lineup column's header cell IS the search bar - no separate
   "Lineup" label alongside it (per the user, 2026-08-31: first a
   standalone block above the table, then inline next to the label,
   then "replace the lineup header with the instasearch - nobody would
   sort them by first initial"). */
.game-stat-grid-lineup-head { display: flex; align-items: center; }
/* Sized/weighted to match the dense grid it lives inside (per the user,
   2026-08-31: the first standalone-block pass was "HUGE and doesn't
   match anything else in the UI") - fills its now-fixed-width column
   rather than a separate compact block floating in spare header space. */
/* REVISED 2026-09-05, per the user: "the bottom of the instasearch
   overlaps with the top of the table, needs a balanced vert gap" -
   this box had no margin-bottom at all, so it sat flush against
   .table-wrap's own top edge. "instasearch doesn't have to go the full
   width" - capped instead of width: 100%; no other, narrower
   search-input convention was found already defined elsewhere in this
   codebase to match against, so this is a reasonable standalone cap,
   not a reuse of a pre-existing standard. */
.lineups-search { display: flex; align-items: center; gap: 6px; width: 100%; max-width: 280px; margin-bottom: 12px; padding: 3px 7px; border: 1px solid var(--border); border-radius: var(--radius); color: var(--text-dim); font-size: var(--fs-2); font-weight: 400; text-transform: none; letter-spacing: normal; }
.lineups-search i { font-size: .85em; }
.lineups-search input { flex: 1; min-width: 0; border: none; background: none; color: var(--text); font: inherit; padding: 0; }
.lineups-search input:focus { outline: none; }
.lineups-search input::placeholder { color: var(--text-dim); }
/* Reset (design-sweep punchlist, 2026-09-08, second round - Player
   Directory search had "no button or mechanism to reset the form") -
   only rendered when a search is active, so it reads as a clear
   affordance next to the input, not a decorative icon. */
.lineups-search-clear { display: flex; align-items: center; color: var(--text-dim); flex-shrink: 0; }
.lineups-search-clear:hover { color: var(--text); }

.game-pbp-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
/* .9rem, matching .game-stat-grid/.game-meetings - one size across every
   grid/table/list on this page (per the user, 2026-08-31: "just giving up
   and saying 'make it all one size' is the right play"). */
.game-pbp-list li { display: flex; gap: 10px; align-items: baseline; padding: 4px 0; border-bottom: 1px solid var(--border); font-size: var(--fs-4); }
/* Non-scoring plays dimmed slightly so scoring plays stand out more (per
   the user, 2026-08-30) - deliberately subtle (.9), not a heavy fade. */
.game-pbp-list li:not(.is-scoring-play) { opacity: .9; }
/* Scoring Plays mode (pbp-filter.js). */
.game-pbp-list.scoring-only li:not(.is-scoring-play) { display: none; }
/* Player PBP reuses Game PBP's list wholesale, but its "period" slot
   holds a full YYYY-mm-dd date (season-spanning plays need to say
   which game each is from) instead of a short in-game clock label -
   wider than the 60px that fits "Top 1"/"Q1 8:45" (2026-09-06). */
/* Leftmost date column (2026-09-06, per the user - a season-spanning
   feed needs to say which game each play is from, unlike Game's own
   single-game list; "period" here goes back to its real inning/time
   meaning instead of being repurposed for this). */
.player-pbp-list .game-pbp-date { flex: none; width: 85px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
/* Each play links back to its own game (Game PBP's own list doesn't -
   it's already on the one game its plays belong to). */
.player-pbp-list li { cursor: pointer; }
/* Low-opacity sequence number, far left (per the user, 2026-08-30) - a
   quiet ordinal, not meant to compete with the actual play content.
   One size for every element in this row (per the user, 2026-08-31: "make
   all text in a grid/table one size" - the description was inheriting the
   row's own font-size while seq/period/team/lead each pinned their own
   smaller size, so the description alone jumped when the row's base size
   grew) - hierarchy now comes from color/opacity/weight only, not size. */
.game-pbp-seq { flex: none; width: 24px; color: var(--text-dim); opacity: .4; font-variant-numeric: tabular-nums; }
.game-pbp-period { flex: none; width: 60px; color: var(--text-dim); text-transform: uppercase; }
/* Team logo + abbreviation between period and description (per the user,
   2026-08-30: "teams aren't noted so it's pretty hard to follow"). Logo
   shrunk 18px -> 14px and the column narrowed to match (per the user,
   2026-08-30: "shrink logo, it's blowing open the row vertically" - on a
   narrow viewport the fixed period/team columns were squeezing the
   description down to a sliver, wrapping it across many lines). */
.game-pbp-team { flex: none; width: 46px; display: flex; align-items: center; gap: 5px; font-weight: 600; color: var(--text-dim); }
.game-pbp-team .detail-team-logo { width: 14px; height: 14px; }
.game-pbp-desc { flex: 1; }
.game-pbp-score { flex: none; font-variant-numeric: tabular-nums; display: flex; align-items: baseline; gap: 4px; }
/* Which team is ahead, before the score itself (per the user, 2026-08-30:
   "so it's more clear") - blank when tied. Grey/normal weight - the
   actual score digits carry the emphasis instead (per the user,
   2026-08-30: "switch the emphasis... team abbreviation in grey normal
   text, score in white bold"). */
.game-pbp-lead { font-weight: 400; color: var(--text-dim); }
.game-pbp-score-digits { font-weight: 700; color: var(--text); }
/* Scoring plays' description bolded (ScoringPlay flag on the playbyplay
   row itself - per the user, 2026-08-30) - the score itself (.game-pbp-
   score-digits) is always bold/white regardless of scoring status now,
   per the emphasis swap above. */
.game-pbp-list li.is-scoring-play .game-pbp-desc { font-weight: 700; color: var(--text); }

/* .toggle-group - a reusable N-option segmented control (per the user,
   2026-08-31: the first version was a binary checkbox switch - "this
   should handle more than 2, the design will be reused a lot later").
   First used by the Play-by-Play All Plays/Scoring Plays filter, built
   generically so any future page can drop in the same markup with as
   many .toggle-group-option buttons as it needs. toggle-group.js owns
   ONLY the generic behavior (which option is .is-active, dispatching a
   `toggle-group:change` event) - a page wires its own specific effect
   (like pbp-filter.js's list filtering) by listening for that event,
   keeping the reusable piece free of any page-specific knowledge. */
/* 8px top margin, not 16px (per the user, 2026-08-31, on the GameFlo-to-
   toggle gap: "reduce to ~20" - paired with .game-gameflo-card's own
   trimmed margin-bottom below). */
.toggle-group { display: inline-flex; align-items: center; gap: 2px; padding: 3px; border-radius: 999px; background: var(--border); margin: 8px 0 20px; }
.toggle-group-option {
  border: none;
  background: none;
  cursor: pointer;
  padding: 6px 14px;
  border-radius: 999px;
  font-family: inherit;
  font-size: var(--fs-4);
  font-weight: 600;
  color: var(--text-dim);
}
.toggle-group-option:hover { color: var(--text); }
.toggle-group-option.is-active { background: var(--bg-raised); color: var(--text); box-shadow: var(--shadow); }
/* Darker active pill in dark mode (per the user, 2026-08-31: "great on
   light mode, the selected bubble is nearly indistinguishable from the
   overall toggle on dark. darken a little") - var(--bg-raised) (#1d2127)
   against the track's var(--border) (#2a2f37) was only a ~13-per-channel
   gap; var(--bg) (#14171c) is the page's own darkest token and reads as
   a clear sunken pill instead. Light mode (white pill on a light-grey
   track) untouched - the user called that one "great" already. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .toggle-group-option.is-active { background: var(--bg); }
}
:root[data-theme="dark"] .toggle-group-option.is-active { background: var(--bg); }

/* .radio-pills - a real <input type="radio"> group styled as the exact
   same segmented-pill track as .toggle-group above (2026-09-11, per the
   user - the export page's Custom Export Format field, "the first of
   those on the site"), not that component's own JS-driven button+
   .is-active version. A plain small fixed choice set doesn't need JS at
   all - hiding the native input (not display:none, which would drop it
   from the tab order/form submission entirely) and styling its sibling
   <span> via :checked is the standard accessible way to reskin a radio
   without losing native keyboard/form behavior. */
.radio-pills { display: inline-flex; align-items: center; gap: 2px; padding: 3px; border-radius: 999px; background: var(--border); }
.radio-pill { position: relative; cursor: pointer; }
.radio-pill input {
  position: absolute; width: 1px; height: 1px; margin: 0; padding: 0;
  opacity: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.radio-pill span { display: inline-block; padding: 6px 14px; border-radius: 999px; font-size: var(--fs-4); font-weight: 600; color: var(--text-dim); }
.radio-pill:hover span { color: var(--text); }
.radio-pill input:checked + span { background: var(--bg-raised); color: var(--text); box-shadow: var(--shadow); }
.radio-pill input:focus-visible + span { outline: 2px solid var(--level-color); outline-offset: 1px; }
/* Same darker-active-pill-in-dark-mode fix as .toggle-group-option.is-active
   above, same reason (var(--bg-raised) barely read against the track's
   var(--border) in dark mode). */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .radio-pill input:checked + span { background: var(--bg); }
}
:root[data-theme="dark"] .radio-pill input:checked + span { background: var(--bg); }

/* GameFlo - score differential over the game (per the user, 2026-08-30:
   "don't stub the GameFlo, make it"). One filled polygon (the line,
   closed back along the zero baseline) clipped into top/bottom halves is
   what lets a single shape render as two different colors above/below
   zero without per-point zero-crossing math - see the partial's own
   docblock. */
/* 12px margin-bottom, not .detail-card's shared 24px default (per the
   user, 2026-08-31, on the gap to the All Plays/Scoring Plays toggle
   right below: "reduce to ~20" - paired with .toggle-group's own trimmed
   top margin above). */
.game-gameflo-card { padding: 20px 16px 12px; margin-bottom: 12px; }
.game-gameflo-chart-wrap { position: relative; }
.game-gameflo-chart { display: block; width: 100%; height: auto; }
.game-gameflo-zero { stroke: var(--border); stroke-width: 1.5; }
.game-gameflo-period-line { stroke: var(--border); stroke-width: 1; opacity: .5; }
.game-gameflo-area-home { fill: var(--level-color); opacity: .18; }
.game-gameflo-area-visitor { fill: var(--text-dim); opacity: .16; }
.game-gameflo-line { fill: none; stroke: var(--text); stroke-width: 1.75; stroke-linejoin: round; }
/* Invisible hit-areas carrying each play's <title> tooltip (per the user,
   2026-08-30: "add playbyplay descriptions as tooltips along the line") -
   a faint dot on hover is feedback that something's there, not a
   permanent mark cluttering the line. `pointer-events: all` is required -
   a `transparent` fill doesn't reliably count as "painted" for SVG hit-
   testing on its own (found live, 2026-08-30: tooltips silently never
   fired without it), unlike a real opaque fill. */
.game-gameflo-point { fill: transparent; stroke: none; cursor: default; pointer-events: all; }
.game-gameflo-point:hover { fill: var(--text); opacity: .5; }
/* Always-visible scoring-play markers (per the user, 2026-08-30:
   "highlight scoring plays with permanent dots") - distinct from the
   invisible hover circles above (which exist on every play). */
.game-gameflo-scoring-dot { fill: var(--level-color); stroke: var(--bg-raised); stroke-width: 1.5; pointer-events: none; }
/* Score scale on the right edge, opposite the team identifiers on the
   left (per the user, 2026-08-30: "so it's clear what the score is"). */
.game-gameflo-axis-tick { fill: var(--text-dim); font-size: 11px; text-anchor: end; font-variant-numeric: tabular-nums; }
/* Team logo + abbrev flanking the zero line on the chart's left edge,
   instead of a text "leading" label (per the user, 2026-08-30) - the
   abbrev always shows even when a team has no logo. */
.game-gameflo-legend { position: absolute; left: 4px; display: flex; align-items: center; gap: 4px; }
.game-gameflo-legend .detail-team-logo { width: 22px; height: 22px; }
.game-gameflo-legend-abbrev { font-size: var(--fs-2); font-weight: 700; color: var(--text-dim); }
.game-gameflo-legend-home { top: 8%; }
.game-gameflo-legend-visitor { bottom: 8%; }
.game-gameflo-periods { position: relative; height: 16px; margin-top: 2px; font-size: var(--fs-1); color: var(--text-dim); }
.game-gameflo-periods span { position: absolute; transform: translateX(-50%); }

.game-meetings-list { list-style: none; margin: 0 0 20px; padding: 0; display: flex; flex-direction: column; gap: 2px; }
/* 8px, not 10px (per the user, 2026-08-31: "more top/bottom padding on
   this page than elsewhere in the UI") - matches .game-pbp-list li's own
   row padding, the closest equivalent list elsewhere in the
   constellation. */
/* .9rem on the row itself, not per-element (per the user, 2026-08-31:
   "just giving up and saying 'make it all one size' is the right play,
   the text size hierarchies seem random right now") - date/relative/
   score/top-performer had each drifted to their own size across earlier
   rounds; all four now inherit this one instead of pinning their own. */
.game-meetings-list a { display: flex; align-items: center; gap: 12px; padding: 4px 0; border-bottom: 1px solid var(--border); color: var(--text); font-size: var(--fs-4); }
.game-meetings-list a:hover { text-decoration: none; color: var(--level-color); }
.game-meetings-date { flex: none; width: 90px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
/* 20px, not 60px (per the user, 2026-08-31: "has a lot of extra room") -
   the text itself never exceeds ~4 chars ("2mo", "15y"). No "ago" suffix
   either (Score::relativeDate() itself drops it now). */
/* 40px, not 20px (per the user, 2026-08-31: "too narrow and longer items
   are smashing against the logos, future items are breaking (in\n2w),
   add ~20px") - a future meeting's "in Xw" ran longer than a past one's
   bare "Xd"/"Xy" and wrapped inside the 20px column. */
.game-meetings-relative { flex: none; width: 40px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.game-meetings-score { flex: 1; display: flex; align-items: center; gap: 8px; }
.game-meetings-winner-logo { width: 20px; height: 20px; }
.game-meetings-winner { font-weight: 700; }
/* A real color change, not opacity (per the user, 2026-08-31: "still
   looks the same opacity as the loser score, was this treated?") - the
   loser's score span carries no dimming at all, so opacity .9 against
   full-opacity var(--text) at 16px was never going to read as different
   on this dark theme. Same lesson as the linescore H/E-vs-inning fix:
   opacity deltas this small don't survive real rendering here. */
.game-meetings-at { color: var(--text-dim); }
/* Name+abbrev and statline on ONE line, no colon between them (per the
   user, 2026-08-30 and 2026-08-31). */
.game-meetings-top-performer { flex: none; display: flex; align-items: baseline; gap: 8px; color: var(--text-dim); }
/* Normal weight, not 600 (per the user, 2026-08-31: "unbold the top star
   name") - the winner's own score/name already carries the bold weight
   for this row; the performer line is a secondary caption. */
.game-meetings-top-performer-name { font-weight: 400; color: var(--text); white-space: nowrap; }
.game-meetings-top-performer-line { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* News tab (per the user, 2026-08-31) - same flat-list/one-size pattern
   as .game-pbp-list/.game-meetings-list, not the sidebar's own carousel
   .news-card treatment (a different context: a full tab, not a rotating
   widget). */
/* One straight line per item - date/relative/source/headline (per the
   user, 2026-08-31: "let's also straighten that out into a straight
   line") - was a two-line stack (meta row above, headline below). Same
   fixed-width-columns-plus-flex shape as .game-meetings-list. */
.game-news-list { list-style: none; margin: 0 0 20px; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.game-news-list a { display: flex; align-items: center; gap: 12px; padding: 4px 0; border-bottom: 1px solid var(--border); color: var(--text); font-size: var(--fs-4); }
.game-news-list a:hover { text-decoration: none; color: var(--level-color); }
/* REVISED 2026-09-08 (design-sweep punchlist, round 2 - "let's make
   this less blindingly white... Source: grey normal text,
   text-transform:uppercase. News headline: normal white text") -
   the 2026-08-31 bold-white-source version above read as too much
   emphasis on the LEAST important column; the headline (the thing
   worth actually reading) now carries the only real visual weight
   left on the row, just via color contrast (still var(--text), full
   white) rather than font-weight, not bold either. */
.game-news-source { flex: none; width: 160px; color: var(--text-dim); font-weight: 400; text-transform: uppercase; letter-spacing: .02em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.game-news-date { flex: none; width: 90px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
/* Widened + right padding (design-sweep punchlist, 2026-09-08 -
   "relative date has very little space and smashes against the
   headline") - 20px was too narrow for its own longest realistic
   value (Story::agoFromTimestamp() can return up to "23h"/"59m", 3
   characters), so the text routinely overflowed its own box and ate
   into the row's shared 12px gap before ever reaching the headline.
   Shared by every .game-news-list table site-wide (Headlines, Game
   News, Player News, Stories), so this one rule fixes all of them. */
.game-news-relative { flex: none; width: 34px; padding-right: 6px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.game-news-headline { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 400; color: var(--text); }
/* Stories tab's own matchup column (design-sweep punchlist, 2026-09-08
   - "no context on what games these stories are for... [logo] CLE @
   ATH") - winner's minilogo + "VIS @ HOME", the winning side bolded
   white via .game-schedule-top-inline-name (already the site's own
   "real, not-dimmed" text convention, reused rather than inventing a
   new one). */
/* Bold, full headline size (design-sweep punchlist, 2026-09-08, second
   round - "make matchup bold and the same size as headline") - was
   dimmed/shrunk like a metadata column; the matchup is real content
   here, same visual weight as the headline text next to it. */
.game-news-matchup { flex: none; width: 100px; display: flex; align-items: center; gap: 4px; color: var(--text); font-weight: 700; font-variant-numeric: tabular-nums; overflow: hidden; }

/* Injuries (2026-09-03, per the user) - own list, not .game-news-list's
   truncated-single-line shape, since injury descriptions run long and
   shouldn't be clipped - wraps instead. Logo carries the team-identity
   styling through the same way every other list on this page does. */
.game-injury-list { list-style: none; margin: 0 0 20px; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.game-injury-list a { display: flex; align-items: flex-start; gap: 12px; padding: 4px 0; border-bottom: 1px solid var(--border); color: var(--text); }
.game-injury-list a:hover { text-decoration: none; }
.game-injury-logo { flex: none; width: 20px; height: 20px; margin-top: 2px; }
.game-injury-text { flex: 1; min-width: 0; font-size: var(--fs-4); line-height: 1.5; }
.game-injury-name { color: var(--text); font-weight: 700; }
.game-injury-status { color: var(--text); font-weight: 400; }
.game-injury-desc { color: var(--text-dim); font-weight: 400; }

/* Team Transactions (2026-09-05, per the user: "match existing
   transactions/injuries styles from games' News tab") - same visual
   language as .game-injury-list just above (bordered rows, bold-then-
   dim hierarchy, identical padding/gap), a new class since the
   content shape itself differs (a date label + a full digest
   paragraph, not a name + short status). */
.team-transactions-list { list-style: none; margin: 0 0 20px; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.team-transactions-list li { display: flex; align-items: flex-start; gap: 12px; padding: 4px 0; border-bottom: 1px solid var(--border); }
/* Date was rendering at the page's base font-size, not the .9rem every
   sibling here uses (2026-09-06, per the user - "the dates are really
   big... mirror the consistent text sizes from the injury page") -
   .game-injury-name/-status/-desc above all inherit .9rem from their
   shared .game-injury-text wrapper, but .team-transactions-date sits
   as a direct <li> child with no such wrapper, so it never got the
   same explicit sizing its own sibling .team-transactions-text has.
   Fixed to match directly rather than adding a wrapper element. */
/* Grey, not bold white (design-sweep punchlist, 2026-09-08, second
   round - "the date should be YYYY-mm-dd and grey" - matches every
   other date column site-wide, e.g. .game-news-date). */
.team-transactions-date { flex: none; width: 90px; font-size: var(--fs-4); color: var(--text-dim); font-variant-numeric: tabular-nums; }
.team-transactions-text { flex: 1; min-width: 0; font-size: var(--fs-4); line-height: 1.5; color: var(--text-dim); }
/* Team name + minilogo, non-team-scoped panels only (design-sweep
   punchlist, 2026-09-08, second round - "if it's not a team page the
   team name and minilogo need to be there") - same .stat-grid-team-
   abbrev recipe used everywhere else on the site, just with the real
   Team name instead of the bare Abbrev (a digest of team moves reads
   better named in full than abbreviated). */
.team-transactions-team { flex: none; width: 140px; display: flex; align-items: center; gap: 5px; font-size: var(--fs-4); font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.team-transactions-team .detail-team-logo { width: 16px; height: 16px; }

/* ------------------------------------------------------------- stories */

/* level-header.php's identity dot (2026-09-08, design-sweep punchlist,
   round 2 - "the HEADER needs to be the color chip dot (no abbreviation)
   and level name... make this header the same size as the game, team
   and player headers"). Sits inside .detail-header/.detail-result
   exactly like Game/Team's own level-chip does, just a bare dot instead
   of a pill with abbreviation text - see level-header.php's own
   docblock for why. Corrects the previous round's own misread of "no
   repetitive tab name" as "add a second, smaller identity line" instead
   of "make the real H1 the identity" - that second line (the old
   .detail-level-identity/.detail-level-name pair) is gone now that the
   real header carries FrontName directly. */
.level-header-dot {
  width: 14px;
  height: 14px;
  border-radius: 4px;
  background: var(--chip-color, var(--level-color));
  flex: none;
  align-self: center;
}

.story-feed {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 24px;
}

.story-card { display: block; }
.story-card-link {
  display: flex;
  flex-direction: column;
  color: var(--text);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  height: 100%;
  transition: border-color .15s, box-shadow .15s;
}
.story-card-link:hover {
  text-decoration: none;
  border-color: var(--level-color);
  box-shadow: var(--shadow);
}

.story-image { aspect-ratio: 16 / 9; overflow: hidden; background: var(--border); }
.story-image img { width: 100%; height: 100%; object-fit: cover; display: block; }

.story-body { padding: 16px; display: flex; flex-direction: column; gap: 8px; flex: 1; }

.story-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fs-2);
  font-weight: 600;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: .03em;
}
.level-chip {
  background: var(--chip-color);
  color: #fff; /* light-mode default - dark mode overrides to #000 just below (2026-09-09, per the user) */
  border-radius: 999px;
  padding: 2px 10px;
  text-transform: none;
  letter-spacing: 0;
  /* Explicit rather than inherited (per the user, 2026-08-30) - it had no
     weight of its own, so it read as 600 inside .story-meta but ~400 in
     the sidebar (.scoreboard-card-head/.news-meta/.elo-list set none at
     all) - same component, two different weights depending on context. */
  font-weight: 700;
  /* Every prior use was a <span> (game-header/sidebar/level-games-page),
     never a real link - account-page.php's "Your Leagues" chips
     (2026-09-09) are the first <a class="level-chip">, and the base
     `a:hover { text-decoration: underline }` rule reached it since
     nothing here said otherwise. A colored pill shouldn't underline on
     hover any more than it should at rest. */
  text-decoration: none;
}
.level-chip:hover { text-decoration: none; }
/* Mode-dependent pill text (2026-09-09, per the user - corrected once:
   "dark mode: pill text black; light mode: pill text white"). Same
   dual-selector pattern .captcha-letter already uses (prefers-color-
   scheme for the unset/"system" case, [data-theme] for an explicit
   toggle in either direction) - shared with .btn just below, since both
   are the same "colored pill" component family. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .level-chip, :root:not([data-theme="light"]) .btn:not(.btn-outline), :root:not([data-theme="light"]) .qb-format-option.is-active { color: #000; }
}
:root[data-theme="dark"] .level-chip, :root[data-theme="dark"] .btn:not(.btn-outline), :root[data-theme="dark"] .qb-format-option.is-active { color: #000; }
.story-ago { margin-left: auto; text-transform: none; font-weight: 400; }

.story-headline {
  font-family: var(--font-display);
  font-size: var(--fs-6);
  font-weight: 700;
  margin: 0;
  /* Root cause found live, 2026-08-30: removing text-wrap: balance here
     did nothing because it was never the source - the base `h1, h2, h3`
     rule near the top of this file sets text-wrap: balance for every
     heading, and .story-headline is an <h3>, so it was inheriting
     balance from THAT rule regardless of what this block said. Every
     earlier edit to this block was a no-op because of that. Explicit
     override back to the real default (wrap) - per the user, "just a
     natural wrap like most of the internet". Text::noWrapScores() still
     keeps a score like "9-7" from splitting mid-number - that part
     was real and unrelated to this bug. */
  text-wrap: wrap;
}

.story-excerpt {
  color: var(--text-dim);
  font-size: var(--fs-4);
  line-height: 1.5;
  margin: 0;
  flex: 1;
}

.story-credit {
  color: var(--text-dim);
  font-size: var(--fs-2);
  font-style: italic;
  margin: 0;
}

.load-more {
  display: block;
  margin: 32px auto 0;
  padding: 10px 28px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg-raised);
  color: var(--text);
  font-family: var(--font-body);
  font-weight: 700;
  cursor: pointer;
}
.load-more:hover { border-color: var(--level-color); color: var(--level-color); }
.load-more:disabled { opacity: .6; cursor: default; }

/* -------------------------------------------------------------- sidebar */
/* Per the doc's own "SIDEBAR" section - scoreboard, ELO/standings, a
   recent video, a subscription CTA. Not always two/three across once this
   is here (per the user, 2026-08-30) - .story-feed's auto-fill grid
   already adapts to the narrower .page-main on its own, no separate rule
   needed. This layout is scoped to the front page/Level pages only
   (index.php wraps them in .page-layout) - a future full-width page (a
   News index, say) can reuse .story-feed directly without it. */

.page-layout {
  display: grid;
  grid-template-columns: 1fr 300px;
  gap: 32px;
  align-items: start;
}
/* Found live, 2026-08-30, on the Classic Boxscore tab: a grid item's
   default min-width is `auto`, meaning it refuses to shrink below its
   OWN content's min-content size - a <pre> block (no text wrapping)
   reported a min-content width equal to its full unwrapped text
   (~3800px on a real box score), so .page-main grew to fit it instead of
   respecting the 1fr track, blowing out the entire grid (body went to
   ~4400px wide, well past the viewport, with .page-sidebar dragged along
   with it). .game-boxscore's own overflow-x: auto never got a chance to
   apply, because nothing constrained the width it was overflowing
   *within* first. The standard fix for this well-known CSS Grid/Flexbox
   gotcha: override the item's min-width explicitly. */
.page-main { min-width: 0; }

.page-sidebar {
  display: flex;
  flex-direction: column;
  gap: 20px;
  position: sticky;
  top: 84px;
}

.sidebar-widget {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
}
.sidebar-widget-title {
  margin: 0 0 10px;
  font-family: var(--font-display);
  font-size: var(--fs-6);
  font-weight: 700;
  opacity: .7;
}

.elo-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.elo-list li a {
  display: block;
  padding: 1px 4px;
  border-radius: 8px;
  color: var(--text);
}
.elo-list li a:hover {
  background: color-mix(in srgb, var(--level-color) 10%, transparent);
  text-decoration: none;
}

/* News carousel: each panel holds 3 headlines and is the fading unit
   (same grid-stack/fade pattern as .scoreboard-card/.video-card) - the
   individual headline rows inside a panel are just plain rows. */
.news-panel {
  grid-row: 1;
  grid-column: 1;
  display: flex;
  flex-direction: column;
  gap: 8px;
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s ease;
}
.news-panel.is-active { opacity: 1; pointer-events: auto; }
.news-card { display: block; padding: 4px; border-radius: 8px; color: var(--text); }
.news-card:hover { background: color-mix(in srgb, var(--level-color) 10%, transparent); text-decoration: none; }
.news-meta { display: flex; align-items: center; gap: 6px; font-size: var(--fs-3); color: var(--text-dim); text-transform: uppercase; letter-spacing: .02em; margin-bottom: 4px; }
/* Normal weight, not bold (design-sweep punchlist, 2026-09-08 -
   "change the source text in the sidebar to grey uppercase normal") -
   color/uppercase already came from .news-meta, just the weight was
   still shouting. */
.news-source { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 400; }
.news-ago { flex: none; text-transform: none; font-weight: 400; }
.news-headline { margin: 0; font-size: var(--fs-5); font-weight: 600; line-height: 1.3; }

.scoreboard-row { display: flex; justify-content: space-between; align-items: center; font-size: var(--fs-5); margin-top: 3px; }
.scoreboard-team { font-weight: 600; }
.scoreboard-score { font-variant-numeric: tabular-nums; color: var(--text-dim); font-weight: 700; }

/* Single-score carousel (per the user, 2026-08-30): one score visible at
   a time, fade between them (auto-advancing - see scoreboard.js), subtle
   left/right nav sitting on the dot-indicator row rather than eating a
   third of the sidebar's width alongside the card itself. Cards are
   stacked in the same grid cell (grid-area/row/column all 1/1) and only
   .is-active is opaque/interactive - scoreboard.js just toggles that
   class, no layout reflow between cards of different heights (a
   "Final · 3OT" status can be longer than "7:05 PM ET"). */
.scoreboard-carousel { display: flex; flex-direction: column; }
.scoreboard-viewport { display: grid; min-width: 0; }
/* Team Schedule tab (2026-09-03, per the user: "it can mirror the front
   page format" - reuses .scoreboard-card-head/.scoreboard-row/-team/
   -score directly, all generic enough already, but NOT .scoreboard-card
   itself - that class is tightly coupled to the carousel's stacking/
   fade mechanism (grid-row/column:1, opacity toggled by JS), not a
   plain always-visible grid of cards. A real auto-fill grid instead of
   one narrow column - per the user, this is "a widescreen box with a
   lot of empty space" otherwise. */
/* Games tab toolbar (design-sweep punchlist, 2026-09-08): a light-grey
   full-date subhead flush left, the date-nav (prev/calendar/next) flush
   right - folds to its own line below the date on narrow viewports
   rather than the two fighting for space. */
.games-toolbar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-bottom: 16px; }
/* A real <h2> now, not small dim text (design-sweep punchlist,
   2026-09-08 - "should show up as a SUBHEADER NOT SMALL TEXT. Same
   subheader as... /mlb/rankings/elo") - that reference page's own
   subhead is a plain unstyled <h2>, so this only needs to drop its
   former font-size/color override and zero the base h2 rule's own
   margin (this sits inside .games-toolbar's flex row alongside the
   date-nav controls, which already carries its own margin-bottom). */
.games-day-subhead { margin: 0; }

/* Round icon-only button - same shape as .theme-toggle/.account-link in
   the site header, generalized into its own reusable class since the
   Games tab's date-nav needed three of them in a row. */
.icon-toggle {
  background: none;
  border: 1px solid var(--border);
  border-radius: 50%;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  cursor: pointer;
  color: var(--text-dim);
  font-size: var(--fs-3);
}
.icon-toggle:hover { color: var(--level-color); border-color: var(--level-color); }

.scoreboard-cal-wrap { display: inline-flex; align-items: center; gap: 6px; }
/* Widens/positions .more-menu for this specific popover's own grid
   content - .more-menu itself (background/border/shadow/z-index) is
   untouched, reused exactly as every other dropdown on the site uses it. */
.scoreboard-cal-popover { top: calc(100% + 8px); left: auto; right: 0; width: 240px; padding: 10px; }
.scoreboard-cal-header { display: flex; align-items: center; justify-content: space-between; gap: 4px; margin-bottom: 8px; }
.scoreboard-cal-header select {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--fs-2);
  min-height: auto;
  padding: 2px 3px;
  border: 1px solid transparent;
  background: none;
  color: var(--text);
  border-radius: 4px;
}
.scoreboard-cal-header select:hover { border-color: var(--border); }
.scoreboard-cal-dow { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; color: var(--text-dim); font-size: var(--fs-1); font-weight: 600; margin-bottom: 2px; }
.scoreboard-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.scoreboard-cal-day {
  aspect-ratio: 1;
  border: 1px solid transparent;
  background: none;
  color: var(--text);
  border-radius: 6px;
  font-size: var(--fs-2);
  cursor: pointer;
}
.scoreboard-cal-day:hover { border-color: var(--level-color); }
.scoreboard-cal-day.has-games { background: color-mix(in srgb, var(--level-color) 14%, transparent); color: var(--level-color); font-weight: 700; }
.scoreboard-cal-day.selected { border-color: var(--level-color); }

.team-schedule-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; margin: 0 0 24px; }
.team-schedule-card { display: block; padding: 12px 14px; border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); }
.team-schedule-card:hover { border-color: var(--text-dim); text-decoration: none; }
.team-schedule-top-performer { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border); font-size: var(--fs-2); color: var(--text-dim); }
.team-schedule-top-performer-name { color: var(--text); font-weight: 600; }

.scoreboard-card {
  grid-row: 1;
  grid-column: 1;
  display: block;
  padding: 4px 4px 2px;
  border-radius: 8px;
  color: var(--text);
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s ease;
}
.scoreboard-card.is-active { opacity: 1; pointer-events: auto; }
.scoreboard-card:hover { background: color-mix(in srgb, var(--level-color) 10%, transparent); text-decoration: none; }
.scoreboard-card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; font-size: var(--fs-2); }
.scoreboard-status { font-size: var(--fs-3); color: var(--text-dim); text-transform: uppercase; font-weight: 400; letter-spacing: .02em; }

.scoreboard-controls { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 8px; }
.scoreboard-nav {
  flex: none;
  width: 18px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  padding: 0;
  color: var(--text-dim);
  cursor: pointer;
}
.scoreboard-nav svg { width: 16px; height: 16px; }
.scoreboard-nav:hover { color: var(--level-color); }

.scoreboard-dots { display: flex; align-items: center; gap: 5px; }
.scoreboard-dot { width: 7px; height: 7px; border-radius: 999px; background: var(--border); }
.scoreboard-dot.is-active { background: var(--level-color); }

.elo-list li a { display: flex; align-items: center; gap: 8px; font-size: var(--fs-1); }
.elo-list .level-chip { flex: none; width: 44px; text-align: center; }
.elo-rank { flex: none; width: 18px; text-align: center; font-size: var(--fs-5); font-weight: 700; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.elo-team { flex: 1; font-size: var(--fs-5); font-weight: 600; display: flex; align-items: center; gap: 5px; min-width: 0; }
.elo-value { font-size: var(--fs-4); color: var(--text-dim); font-variant-numeric: tabular-nums; }
/* Minilogo, both Top ELO and stat-leaders (design sweep, 2026-09-12, per
   the user, ROUND 2 for stat-leaders - the first attempt reused
   .stat-grid-team-abbrev, a wider-table "chip" component the user flagged
   as "not like anything else on the site" in this compact list; the real
   spec put the logo BEFORE the name, not a separate boxed unit after it).
   The bare .detail-team-logo default (28px) is sized for wider contexts
   than this tight sidebar row, same lesson already learned elsewhere on
   this site (Tempo-Free Aerial's own "minilogo too big, blowing out row"
   fix). */
.elo-team .detail-team-logo { width: 14px; height: 14px; }
/* .elo-team-name wraps the free-flowing text (name + inline abbrev) as
   ONE flex item alongside the logo - without this wrapper, the bare text
   node and .elo-team-abbrev span would each become their own anonymous
   flex item under .elo-team's flex layout, putting an unwanted gap
   between the name and its abbrev instead of a plain single space. */
.elo-team-name { min-width: 0; }
/* Team abbrev, inline right after the player's name (design sweep,
   2026-09-12, per the user - "DORD in *SLIGHTLY* smaller grey, inline,
   that's barely readable" - explicitly not a separate boxed/chip unit
   like .stat-grid-team-abbrev, which is for wider stat-grid tables, not
   this compact sidebar list). */
.elo-team-abbrev { font-size: .85em; font-weight: 400; color: var(--text-dim); }

.video-embed { aspect-ratio: 16 / 9; border-radius: 0; overflow: hidden; }
.video-embed iframe { width: 100%; height: 100%; border: 0; display: block; }

/* Videos carousel card - same grid-stack/fade pattern as .scoreboard-card. */
.video-card { grid-row: 1; grid-column: 1; opacity: 0; pointer-events: none; transition: opacity .25s ease; }
.video-card.is-active { opacity: 1; pointer-events: auto; }

/* Upcoming-games carousel card - same grid-stack/fade pattern, arrows
   only (no dots - per the user, 2026-08-30). */
.upcoming-card {
  grid-row: 1;
  grid-column: 1;
  display: block;
  padding: 4px 4px 2px;
  border-radius: 8px;
  color: var(--text);
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s ease;
}
.upcoming-card.is-active { opacity: 1; pointer-events: auto; }
.upcoming-card:hover { background: color-mix(in srgb, var(--level-color) 10%, transparent); text-decoration: none; }
/* Normal weight, lowercase m/h - per the user, unlike .scoreboard-status
   (which the scoreboard card's own head reuses uppercase/bold for
   "FINAL" etc.) this is just a plain countdown, not a status badge. */
.upcoming-when { flex: none; font-size: var(--fs-3); color: var(--text-dim); font-weight: 400; text-transform: uppercase; letter-spacing: .02em; }
.upcoming-record { font-weight: 400; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.upcoming-forecast { margin: 6px 0 0; font-size: var(--fs-3); color: var(--text-dim); font-variant-numeric: tabular-nums; }

.sidebar-cta { text-align: center; position: relative; overflow: hidden; }
.sidebar-cta p { color: var(--text-dim); font-size: var(--fs-4); margin: 0 0 12px; }
/* Optional per-ad graphic background (2026-09-15, per the user) - an <img>,
   not a CSS background-image, so it participates in normal image loading;
   object-fit: cover crops to fill the card regardless of its own aspect
   ratio. Falls back to the plain --bg-raised card color when no
   BackgroundURL is set - most ads still will be, until real graphics exist. */
.sidebar-cta-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0; }
.sidebar-cta-content { position: relative; z-index: 1; }
/* House ads with a background image (the images are dark): white headline and light gray text in either theme - the theme's
   own text colors read gray-on-dark and vanish in light mode. Only when an image is actually there, so a plain ad card keeps
   the theme colors. */
.sidebar-cta-bg + .sidebar-cta-content .sidebar-widget-title { color: #fff; }
.sidebar-cta-bg + .sidebar-cta-content p { color: #d3d8df; }
.sidebar-cta-btn {
  display: inline-block;
  padding: 10px 24px;
  border-radius: 999px;
  background: var(--level-color);
  color: var(--sidebar-cta-text);
  font-weight: 700;
}
.sidebar-cta-btn:hover { text-decoration: none; opacity: .9; }
.sidebar-cta form { margin: 0; }
button.sidebar-cta-btn { border: none; cursor: pointer; font-family: var(--font-body); font-size: var(--fs-5); }

@media (max-width: 1024px) {
  .page-layout { grid-template-columns: 1fr; }
  .page-sidebar { position: static; }
}

/* --------------------------------------------------------------- footer */

.site-footer {
  border-top: 1px solid var(--border);
  margin-top: 40px;
}
.site-footer-inner {
  max-width: 1280px;
  margin: 0 auto;
  padding: 28px 20px 100px;
  display: flex;
  flex-direction: column;
  gap: 24px;
}
.site-footer nav {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}
/* Same --level-color tint as .primary-nav - see that rule's comment. */
.site-footer a { color: color-mix(in srgb, var(--level-color) 55%, var(--text-dim)); font-size: var(--fs-4); font-weight: 500; }
/* The Network Updates link's own trailing "(vX.X)" (2026-09-11, per the
   user - "slightly greyed out") - a plain var(--text-dim), not the
   link's own tinted color, so it reads as metadata rather than more
   link text. */
.footer-updates-version { color: var(--text-dim); font-weight: 400; }
/* Second link row (products) sits directly under the first, not a full flex-gap below it. */
.site-footer nav.footer-products { margin-top: -12px; }
.footer-dot { color: var(--text-dim); font-size: var(--fs-4); }

.footer-drilldown {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--border);
}
/* Collapsed by default (native <details>) - Basketball alone has 16
   Levels, a flat always-expanded column blew the footer's height open. */
.footer-sport-col {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  min-width: 160px;
}
.footer-sport-col summary {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 8px 14px;
  cursor: pointer;
  /* Matches .site-footer a (the About/System Info links) rather than a
     heavier distinct treatment - per the user, 2026-08-29. */
  font-size: var(--fs-4);
  font-weight: 500;
  color: var(--text);
  list-style: none;
}
.footer-sport-col summary::-webkit-details-marker { display: none; }
.footer-sport-count { color: var(--text-dim); font-weight: 500; text-transform: none; letter-spacing: 0; }
.footer-sport-col[open] summary { border-bottom: 1px solid var(--border); }
.footer-sport-links {
  display: grid;
  grid-template-columns: repeat(2, auto);
  gap: 2px 16px;
  padding: 8px 14px 12px;
  max-height: 260px;
  overflow-y: auto;
}
/* .sport-menu-level (abbreviation + color dot + tooltip) is shared with
   the header's sport dropdown - same treatment, per the user. */
.footer-sport-links .sport-menu-level { padding: 3px 0; }

/* Mobile "Levels" drilldown groups the same way as the footer, inside the
   more-menu-mobile panel. */
.sport-menu-mobile { flex-direction: column; max-height: 70vh; overflow-y: auto; }
.sport-group { display: flex; flex-direction: column; }
.sport-group-label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-2);
  font-weight: 700;
  color: var(--text-dim);
  padding: 10px 10px 2px;
}
.sport-group-label .sport-icon { font-size: 1.1em; }
.site-footer a:hover { color: var(--level-color); }
.copyright { color: var(--text-dim); font-size: var(--fs-4); margin: 0; }

/* ---------------------------------------------------- mobile/tablet nav */

.mobile-island { display: none; }
.more-menu-mobile { display: none; }

@media (max-width: 1024px) {
  .primary-nav { display: none; }

  .mobile-island {
    display: flex;
    position: fixed;
    left: 50%;
    bottom: 16px;
    transform: translateX(-50%);
    z-index: 50;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: 999px;
    box-shadow: var(--shadow);
    padding: 6px;
    gap: 2px;
  }
  /* Big icon on top, small label underneath - per the user, 2026-08-29 -
     rather than the plain text-only tab bar this started as. */
  .mobile-island a,
  .mobile-island .more-toggle {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    padding: 8px 14px 6px;
    border-radius: 20px;
    /* Same --level-color tint as .primary-nav - see that rule's comment. */
    color: color-mix(in srgb, var(--level-color) 55%, var(--text-dim));
    font-weight: 600;
    font-size: var(--fs-4);
    background: none;
    border: none;
    font-family: var(--font-body);
    cursor: pointer;
  }
  .mobile-island svg { width: 21px; height: 21px; }
  .mobile-island .sport-icon { font-size: var(--fs-7); }
  .island-label { font-size: .62rem; text-transform: uppercase; letter-spacing: .02em; }
  .mobile-island a:hover,
  .mobile-island a.active,
  .mobile-island .more-toggle:hover {
    background: color-mix(in srgb, var(--level-color) 14%, transparent);
    color: var(--level-color);
    text-decoration: none;
  }

  .more-menu-mobile:not([hidden]) {
    display: flex;
    position: fixed;
    top: auto; /* .more-menu's base `top` otherwise wins over `bottom` below - found live (2026-08-29), it pushed this panel off-screen */
    left: 50%;
    bottom: 76px;
    transform: translateX(-50%);
    z-index: 50;
  }
}

@media (max-width: 767px) {
  .site-header-inner { padding: 10px 16px; }
  .content { padding: 20px 16px 100px; }
  .brand-full { display: none; }
  .brand-short { display: inline; }

  /* Swipe replaces these on touch - carousel.js wires up swipe
     unconditionally, so hiding the controls here doesn't lose any
     functionality, just the scroll height they were adding. */
  .scoreboard-controls { display: none; }

  /* REAL BUG FIXED (2026-09-15, per the user: "clicking the search
     button and unraveling the search bar blows out the page -
     everything moves to the right and it opens up a horiz scroll").
     Root cause, confirmed live via getBoundingClientRect(): .header-actions
     is a nowrap flex child with no min-width:0, so when
     .level-search-input-track grows to its fixed 190px open width, the
     row has nowhere to put it - .header-actions silently overflows
     .site-header-inner's own right edge instead of wrapping or
     shrinking (body.scrollWidth 511px vs a 375px viewport, confirmed).

     Fix: only .level-search-input-track becomes an overlay on mobile -
     .level-search-wrap itself and the search toggle button INSIDE it
     stay in normal flex flow, completely unmoved, so nothing else in
     the header reflows or jumps. The track expands leftward from the
     toggle button's own position (right: 0 relative to .level-search-wrap,
     which is already `position: relative` - unchanged) instead of
     pushing the row wider. .level-search-results (the dropdown) is a
     child of the track, so it now anchors to the track instead of the
     wrap - still `right: 0`, just relative to a box that sits ~42px
     further left (the track's own width) - verified live it still
     fits comfortably inside the viewport. */
  .level-search-wrap.is-open .level-search-input-track {
    position: absolute;
    top: 50%;
    right: 0;
    transform: translateY(-50%);
    /* Overrides the base rule's overflow:hidden (there only to hide
       the pre-full-width state during the roll-open animation) - now
       that this is a positioned overlay, it also became the containing
       block for .level-search-results (previously skipped past to
       .level-search-wrap), and overflow:hidden was silently clipping
       that whole dropdown away since it renders entirely below this
       box. Safe to override unconditionally here since .is-open only
       ever matches once the width is already at its 190px end state. */
    overflow: visible;
  }
}

/* Context Rating radar/spidogram (TCR/PCR, 2026-09-02) - shared by
   team-tcr-page.php and player-pcr-page.php. Plain SVG polygon, same
   "no JS charting library" house pattern as GameFlo above - the live
   legacy TCR-history page's own FusionCharts bundle was observed
   throwing a real JS parse error in the console the same day this was
   built, confirming that dependency isn't worth carrying forward. */
/* REAL BUG FIXED (2026-09-05, per the user: "smushed and unreadable...
   rendered in too small an area... worse as the screen gets smaller")
   - align-items: center (not stretch) meant .radar-chart-wrap never
   received a real width from its parent, so .radar-chart's own
   `width: 100%` had nothing meaningful to be 100% OF and fell back to
   a small intrinsic SVG default regardless of how much room the card
   actually had - confirmed live, the chart was rendering nowhere near
   its own max-width: 600px cap even on a wide desktop card. stretch +
   an explicit width: 100% on the wrap closes that chain. */
.radar-chart-wrap-outer { display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
.radar-chart-wrap { display: flex; justify-content: center; width: 100%; }
/* Cap raised 440->600 (2026-09-04, per the user: "rendering really
   small... on desktop") - at the old cap, the viewBox (440 wide) and
   the CSS max-width landed on the same number, so the chart never grew
   past its own native 1:1 size regardless of how much room the card
   actually had. Label font-size/stroke-widths below are also bumped
   in the SVG's own user-coordinate units (not just this CSS box) - an
   SVG's internal px sizes scale with the viewBox-to-rendered-size
   ratio, so that's the real fix for "fuzzy on mobile": a bigger
   viewBox-relative font/stroke stays legible even once a phone's
   narrower container shrinks the whole chart back down, where a raised
   CSS cap alone wouldn't help. */
.radar-chart { width: 100%; max-width: 600px; height: auto; }
.radar-chart-ring { fill: none; stroke: var(--border); stroke-width: 1.25; }
.radar-chart-spoke { stroke: var(--border); stroke-width: 1.25; opacity: .6; }
.radar-chart-data { fill: var(--level-color); fill-opacity: .22; stroke: var(--level-color); stroke-width: 2.5; stroke-linejoin: round; }
.radar-chart-vertex { fill: var(--level-color); stroke: var(--bg-raised); stroke-width: 1.5; }
.radar-chart-label { fill: var(--text-dim); font-size: 13px; }
/* Two-series overlay (Four Factors offense-vs-defense, 2026-09-02) -
   series 1 (own/offense) keeps the solid --level-color treatment above;
   series 2 (opponent/defense) gets a dashed outline and --text-dim fill
   so the two never read as ambiguous even without the legend. */
.radar-chart-data-2 { fill: var(--text-dim); fill-opacity: .14; stroke: var(--text-dim); stroke-width: 2.2; stroke-dasharray: 4 3; stroke-linejoin: round; }
.radar-chart-vertex-2 { fill: var(--text-dim); }
.radar-chart-legend { display: flex; gap: 16px; font-size: var(--fs-3); color: var(--text-dim); }
.radar-chart-legend-item { display: flex; align-items: center; gap: 6px; }
.radar-chart-legend-item::before { content: ''; display: inline-block; width: 10px; height: 10px; border-radius: 2px; }
.radar-chart-legend-series1::before { background: var(--level-color); }
.radar-chart-legend-series2::before { background: var(--text-dim); }

/* Season ELO / Historical ELO line chart (2026-09-02) - team-elo-page.php. */
.elo-chart-summary { display: flex; gap: 16px; font-size: var(--fs-4); color: var(--text-dim); margin-bottom: 8px; }
.elo-chart-summary strong { color: var(--text); font-variant-numeric: tabular-nums; }
.elo-chart { display: block; width: 100%; height: auto; }
.elo-chart-ref-line { stroke: var(--border); stroke-width: 1; stroke-dasharray: 3 3; }
.elo-chart-ref-label { fill: var(--text-dim); font-size: 10px; }
/* Real rating (Y)/date (X) axis ticks (2026-09-05, per the user: "ELO
   charts don't have rating #/date axes... so a lot of people won't
   know what they're looking at") - same treatment as game-gameflo-
   widget.php's own .game-gameflo-axis-tick. */
.elo-chart-axis-label { fill: var(--text-dim); font-size: 10px; font-variant-numeric: tabular-nums; }
.elo-chart-line { fill: none; stroke: var(--level-color); stroke-width: 1.75; stroke-linejoin: round; }
.elo-chart-point { fill: var(--level-color); opacity: 0; }
.elo-chart-point:hover { opacity: 1; }

/* REAL BUG FIXED (2026-09-04) - `.note` (the established bottom-of-
   screen explainer-text class, used across 15+ Team/Game partials) had
   no CSS rule of its own at all, so it silently rendered as full-size,
   full-brightness body text - not the "grey/small" treatment the
   user's own standing design direction called for from earlier in
   this session ("any and all expl text should go under the tables and
   be grey/small"). Confirmed live before this fix: computed color was
   the page's own --text token, not --text-dim. */
.note { font-size: var(--fs-4); color: var(--text-dim); }
/* Legend line below a stat table (2026-09-06, per the user - recreates
   the legacy site's own below-table column-code glossary). Each entry
   wraps as its own inline unit so a code+explanation pair never breaks
   across two lines. */
/* Recolored (2026-09-06, per the user - "the white labels don't match
   up with anything... this shouldn't stand out at all"): the code
   itself uses the same subdued grey the site's own h2 headers already
   use (see the global h2 rule's own note - a color-mix, not a literal
   hex, so it stays correct in both themes), not var(--text) (white) -
   it was reading as more prominent than the explanation next to it,
   backwards for a footnote. The explanation text drops a notch dimmer
   than .stat-legend's own baseline color, not the same weight as body
   copy. */
.stat-legend { font-size: var(--fs-3); color: color-mix(in srgb, var(--text-dim) 80%, transparent); margin: 8px 0 0; line-height: 1.7; }
.stat-legend-entry { display: inline-block; margin-right: 14px; white-space: nowrap; }
.stat-legend-entry b { font-weight: 600; color: color-mix(in srgb, var(--text-dim) 50%, var(--text) 50%); }

/* TCR rank-by-season trend (2026-09-02) - team-tcr-page.php. Y-axis is
   inverted (rank 1 plots near the top) - built into the point math in
   the partial itself, not a CSS transform. */
.tcr-summary { font-size: var(--fs-5); color: var(--text-dim); margin-bottom: 4px; }
.tcr-summary strong { color: var(--text); }
.tcr-trend-chart { display: block; width: 100%; height: auto; }
.tcr-trend-line { fill: none; stroke: var(--level-color); stroke-width: 1.75; stroke-linejoin: round; }
.tcr-trend-point { fill: var(--level-color); stroke: var(--bg-raised); stroke-width: 1.5; }

/* Advanced (sabermetrics) headline stat tiles - wOBA/wRC+/bWAR
   (2026-09-02). No equivalent "big number" tile pattern existed yet
   elsewhere in this app (.game-stat-grid is a dense comparison table,
   not a headline callout), so this is new, not reused. */
.advanced-stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 16px 0 20px; }
.advanced-stat-box { border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; text-align: center; }
.advanced-stat-value { font-size: var(--fs-9); font-weight: 700; color: var(--level-color); font-variant-numeric: tabular-nums; }
.advanced-stat-label { font-size: var(--fs-3); color: var(--text-dim); text-transform: uppercase; letter-spacing: .04em; margin-top: 4px; }
@media (max-width: 599px) {
  .advanced-stat-grid { grid-template-columns: 1fr; }
}

/* Roster injury flag (2026-09-02, per the user - "a little medical red
   cross for injured players on the roster page like Wikipedia has") - a
   generic medical-kit glyph (not the literal Red Cross emblem, which is
   a protected symbol under the Geneva Conventions in many
   jurisdictions), colored via a dedicated semantic token so it reads as
   a real status flag rather than borrowing --level-color's per-Level
   branding hue. */
.roster-injury-flag { color: #d64545; margin-left: 4px; font-size: .8em; }
.roster-injury-flag:hover { color: #b83333; }

/* Classic Boxscores (Level Hub News tab, 2026-09-06, per the user - "a
   scroll of all classic boxscores... like The Sporting News in 1985").
   The header row reuses .scoreboard-card-head/-row/-team/-score
   verbatim (Games tab's own cards); .game-boxscore reuses verbatim too
   (Game round's own Classic Boxscore tab) - this is only the spacing/
   divider wrapper tying one header to its box score block, and marking
   where one game ends and the next begins in a long endless scroll. */
/* border-bottom divider removed (design-sweep punchlist, 2026-09-08 -
   "remove <hr> between boxscores") - each entry's own generous padding
   plus the box score's monospace block already separate entries
   clearly enough without a literal rule line. */
/* Padding cut from 20px/side to 2px/side (design-sweep punchlist,
   2026-09-08, second round - "reduce vertical gap by about 40px") - the
   dense, stacked-boxscore feel the section's own docblock is going for
   ("like The Sporting News in 1985") wants entries snug, not airy. */
.classic-boxscore-entry { padding: 2px 0; }
.classic-boxscore-entry:first-child { padding-top: 0; }
/* Real subheader now, not small dim text (design-sweep punchlist,
   2026-09-08 - "scoreline needs to be a SUBHEADER NOT SMALL TEXT" - the
   prior revision reused .games-day-subhead's class name on a plain
   <span>, which never picks up that class's h1/h2/h3-only sizing) - a
   real <h2> (.classic-boxscore-result, styled by the base h2 rule) plus
   a flush-right date+status span reusing game-header.php's own
   .detail-when/.game-header-when exactly, "in the same format as game
   pages" per the user. */
.classic-boxscore-header { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; color: var(--text); margin-bottom: 10px; }
.classic-boxscore-header:hover { text-decoration: none; }
.classic-boxscore-header:hover .scoreboard-team, .classic-boxscore-header:hover .classic-boxscore-result { color: var(--level-color); }
/* -2px vs. the "Classic Boxscores" subhead's own base h2 size (design-
   sweep punchlist, 2026-09-08, second round - "so they aren't the same
   size as the 'Classic Boxscores' subhead"). */
.classic-boxscore-result { margin: 0; font-size: calc(clamp(1.2rem, 3vw, 1.625rem) - 4px); }

/* Real server-side sort links + pagination (Level Hub Stats tab,
   2026-09-06) - a plain <a> per header, not stat-grid.js's client
   click-handler (that file explicitly skips data-server-sort tables -
   see its own docblock for why a client re-sort would be wrong here,
   not just redundant). Same dimmed-until-active/hover language as
   .game-stat-grid th.is-sortable's own ▾ glyph, just driven by a real
   href + an inline ▲/▼ on whichever column is the current sort. */
.stat-grid-sort-link { display: inline-block; color: inherit; text-decoration: none; }
.stat-grid-sort-link:hover { color: var(--text); text-decoration: none; }
.stat-grid-sort-link.is-active { color: var(--text); font-weight: 700; }
/* Dim hint-arrow on every sortable header, not just the active column
   (design-sweep punchlist, 2026-09-08 - "header rows are missing
   direction arrows like all other grids on the site") - matches
   .game-stat-grid th.is-sortable's own client-sort ▾ exactly, just as a
   CSS pseudo-element here too so every server-sorted table (Level Hub's
   own leaderboards) gets it without touching each page's PHP. The
   active column keeps its own real ▲/▼ text (rendered server-side,
   correctly reflecting $sortDir) instead of a second arrow stacking on
   top of it. */
.stat-grid-sort-link:not(.is-active)::after { content: '▾'; margin-left: 4px; opacity: .2; }
.pagination-controls { display: flex; align-items: center; justify-content: center; gap: 18px; margin: 18px 0 4px; }
.pagination-link { font-weight: 600; color: var(--level-color); }
.pagination-link.is-disabled { color: var(--text-faint); pointer-events: none; }
.pagination-status { color: var(--text-dim); font-size: var(--fs-4); font-variant-numeric: tabular-nums; }

/* Accounts (Phase 4, first pass, 2026-09-06) - login/signup/account all
   share one centered-card shell rather than three bespoke layouts, same
   "one primitive, several callers" approach as .detail-card. Not a
   Level page (no sidebar, no level-color theming) - --level-color still
   drives the submit button so it doesn't look totally inert, but the
   card itself is neutral like the front page. */
/* padding reduced 48px -> 24px (2026-09-09, real bug caught live by the
   user - "~100px above the header... a scroll necessary to use this
   form") - applies to every page built on this shell (.auth-page/
   .subscribe-page/.levels-page all had the same 48px). */
/* padding-top removed entirely (2026-09-09, real bug caught live by the
   user - "what good does 60px of blank space above the header do?").
   The earlier 48px->24px pass only halved a redundancy it didn't
   actually find - <main class="content"> (header.php, every page on the
   site) already carries its own padding-top: 32px, and this class was
   stacking a second one on top of it. 0 here, relying entirely on
   .content's - no page-specific top padding at all needed. */
.auth-page { display: flex; justify-content: center; padding: 0 16px 24px; }
.auth-card {
  width: 100%;
  max-width: 380px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 32px;
}
.auth-card h1 { font-family: var(--font-display); font-size: var(--fs-8); margin: 0 0 4px; }
.auth-subtitle { color: var(--text-dim); margin: 0 0 24px; }
.auth-error {
  background: color-mix(in srgb, #d64545 15%, var(--bg-raised));
  border: 1px solid color-mix(in srgb, #d64545 40%, var(--border));
  color: #d64545;
  border-radius: var(--radius);
  padding: 10px 14px;
  margin-bottom: 20px;
  font-size: var(--fs-4);
}
.auth-field { margin-bottom: 16px; }
/* Direct-child combinator, not a bare descendant selector (real bug
   found live 2026-09-11, "odd bottom padding on the format selector
   that isn't on others" - the Format field's radio-pills are each a
   real <label class="radio-pill"> too, two levels deep inside
   `fieldset.auth-field` - the old descendant selector matched THOSE as
   well, silently adding this rule's own margin-bottom:6px under every
   pill and inflating the row's total height. Every real field label on
   this page (Data/From/To/Format's own <legend>, the signup checkbox's
   <label class="auth-checkbox-field">) is already a direct child of its
   .auth-field, so scoping to `>` changes nothing for any of them. */
.auth-field > label { display: block; font-weight: 700; font-size: var(--fs-4); margin-bottom: 6px; }
/* A <fieldset class="auth-field"> (the radio-pills Format field, 2026-09-11)
   needs its own browser-default padding/border/margin reset, plus its
   <legend> matching .auth-field label's own look exactly - same weight/
   size/spacing, just a different element. */
fieldset.auth-field { border: none; padding: 0; margin: 0 0 16px; min-width: 0; }
.auth-field legend { display: block; font-weight: 700; font-size: var(--fs-4); margin-bottom: 6px; padding: 0; }
.auth-field input {
  width: 100%;
  /* 10px 12px -> 8px 10px (2026-09-09, per the user - "the text inputs
     have a little too much spacing," checked against .lineups-search
     (the instasearch fields elsewhere on the site) for a tighter
     reference point - that one's a bordered wrapper around a
     padding: 0 input rather than a directly-comparable bare field, so
     this isn't a literal match, just closer to it than the original
     value was). Shared by every .auth-field on the site (login/signup/
     forgot-password/reset-password/Profile/Security), not one-off to
     Profile - "unify the amount of text input padding across the site,"
     per the user. */
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--fs-5);
}
.auth-field input:focus { outline: 2px solid var(--level-color); outline-offset: 1px; }
/* The site-wide `select` base rule (above) has no opinion on width -
   every sibling .auth-field input already stretches full-width, so a
   bare select (support-page.php's Sport/League picker, the export
   page's Data/Format pickers) needs the same explicit width or it sits
   at its own native, much-narrower size next to full-width fields. */
.auth-field select { width: 100%; }
.auth-submit {
  width: 100%;
  padding: 11px;
  border: none;
  border-radius: 999px;
  background: var(--level-color);
  color: #fff;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: var(--fs-5);
  cursor: pointer;
  margin-top: 4px;
}
.auth-submit:hover { opacity: .9; }

/* .btn (2026-09-09, per the user - "is there anywhere else that uses the
   full-width buttons? they're giant... there has to be a button style
   that's more sane"). Checked first: .auth-submit's width: 100% is the
   ONLY submit-button component this app has - correct for the narrow
   380px .auth-card login/signup/forgot-password/reset-password forms it
   was built for (still used there, unchanged), but stretched absurdly
   wide once reused inside the ~700px .account-page's .detail-cards.
   Nothing existing to borrow, so this is the sane-sized alternative -
   auto width, sized to its own label, for every button/button-like link
   on Overview/Profile/Security. .btn-outline mirrors the secondary
   (transparent/bordered) look those pages were hand-rolling via inline
   styles before this. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 8px 24px;
  border: none;
  border-radius: 999px;
  background: var(--level-color);
  /* White - light-mode default, dark mode overrides to #000 (2026-09-09,
     per the user, corrected once - same mode-dependent rule as
     .level-chip, see that class's own comment). */
  color: #fff;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: var(--fs-5);
  cursor: pointer;
  text-decoration: none;
}
.btn:hover { opacity: .9; text-decoration: none; }
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
.btn-outline:hover { opacity: 1; border-color: var(--level-color); color: var(--level-color); }
/* Social login (2026-09-10, per the user - "social logins") - .btn-google
   just adds icon+label gap on top of .btn-outline; full-width only
   inside .auth-card's own narrow login/signup form (Profile's own
   Connect/Disconnect Google buttons stay auto-width, matching every
   other button on that page - see that page's own round 3 note on why
   .btn went auto-width there). .auth-divider is the "or" rule between
   it and the email/password form below, same size/color role as
   .account-hint. */
.btn-google { gap: 8px; }
.auth-card .btn-google { width: 100%; margin-top: 4px; }
.auth-divider { display: flex; align-items: center; gap: 10px; margin: 16px 0; color: var(--text-dim); font-size: var(--fs-3); }
.auth-divider::before, .auth-divider::after { content: ''; flex: 1; height: 1px; background: var(--border); }
/* Centers the submit .btn to match .auth-switch's own already-centered
   bottom text (2026-09-10, per the user - "bottom text is centered,
   buttons are flush left. they should be the same"). The button is a
   direct child of <form> (not wrapped in .auth-field), so text-align:
   center on the form alone would center it correctly - but it would also
   inherit down into .auth-field's own label/input, which need to stay
   left-aligned, so that's set back explicitly right below. */
.auth-card form { text-align: center; }
.auth-card .auth-field { text-align: left; }
.auth-switch { text-align: center; color: var(--text-dim); margin: 20px 0 0; font-size: var(--fs-4); }
.account-row { display: flex; justify-content: space-between; gap: 16px; padding: 12px 0; border-bottom: 1px solid var(--border); }
.account-row:last-of-type { border-bottom: none; }
.account-row-label { color: var(--text-dim); font-weight: 700; font-size: var(--fs-4); }
.account-row-value { font-variant-numeric: tabular-nums; }
/* Greyed out (2026-09-10, per the user) - User ID/Member Since are
   fixed facts, not editable/changing values like Name/Email/API Key. */
.account-row-value--static { color: var(--text-dim); }

/* Account nav (2026-09-09) - reuses .detail-nav/.detail-nav-tab (Team/
   Player/Game's own constellation nav) so every /account/* page shares
   one tab strip - fixes a real gap the user caught: no way back to the
   main account page from My Leagues. Tucked into the narrow .auth-card
   here instead of a wide .detail-page, so it gets its own small
   margin/border override rather than the wider component's defaults. */
.account-nav { margin: 0 0 20px; }
.account-section-empty { color: var(--text-dim); font-size: var(--fs-4); margin: 0 0 10px; }
/* Security > Recent Logins' own "All times Eastern." caption (design-
   sweep punchlist, 2026-09-10 - "~10px top padding for balance"). */
.account-login-caption { margin-top: 10px; }
/* Profile > Notifications' own privacy disclaimer (round 7, 2026-09-10) -
   same size/color role as .account-section-empty above, own class since
   this one sits under a real heading rather than standing in for empty
   content. */
.account-hint { color: var(--text-dim); font-size: var(--fs-4); margin: -4px 0 14px; }

/* Two related .detail-cards side by side (2026-09-09, per the user) -
   Profile's Name/Organization + Email forms, and Security's Change
   Password + Recent Logins (not both forms, but paired the same way -
   renamed from .account-form-grid once it stopped being form-only). Each
   .detail-card filled the whole ~700px .account-page width on its own,
   and .auth-field input's width: 100% made for absurdly wide text
   inputs. Same 600px collapse breakpoint as .subscribe-grid--fixed. */
.account-grid-2up { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; margin-bottom: 24px; }
.account-grid-2up .detail-card { margin-bottom: 0; }
/* -2px off the base h2 clamp (2026-09-09, per the user - "let's -2px on
   the subheaders, they're kinda big at 2-up"). Scoped to real
   .detail-cards inside .account-page specifically, not a bare
   ".detail-card h2" rule - that would silently resize every Team/
   Player/Game detail-card header sitewide, which was never asked for.
   .levels-group deliberately isn't a .detail-card (see its own CSS
   comment - "the grey card... stands out"), so its own bespoke 1.05rem
   h2 is untouched by this. */
.account-page .detail-card h2 { font-size: calc(clamp(1.2rem, 3vw, 1.625rem) - 2px); }
/* Financials accordion (2026-09-10) - native <details>, same convention
   as footer.php's own per-sport columns; the summary is styled to match
   this page's own h2 (font/weight/color) since it stands in for the
   card's title bar here, not a literal <h2>. */
.account-financials summary {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  list-style: none;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: calc(clamp(1.2rem, 3vw, 1.625rem) - 2px);
  color: color-mix(in srgb, var(--text-dim) 50%, var(--text) 50%);
}
.account-financials summary::-webkit-details-marker { display: none; }
/* Shared dimmed/normal-weight count number (2026-09-10, per the user -
   "i prefer the slightly less opaque normal-text number (Financials
   (17)) ... extend that to Leagues and Add-Ons too") - was Financials-
   only (.account-financials-count); now shared across every Overview
   card heading's own "(N)" count (Leagues/Add-ons/Financials), each of
   which now wraps just the count in this span rather than letting the
   whole heading (name + count) render as one uniform color/weight. */
.account-count { color: var(--text-dim); font-weight: 400; }
/* Three columns (2026-09-10, per the user - "can we put them in three
   columns") - 17 rows read as a long single-file list; a real grid reads
   as one compact block instead. Column-major (2026-09-10, per the user -
   "2024 under 2025, then the second column starts with 2019") -
   grid-auto-flow: column fills DOWN each column first; --financials-rows
   (set inline by account-page.php, computed from the real item count)
   tells it where to wrap into the next column, since Grid can't infer
   that from item count alone. Drops to a real single column on narrow
   viewports (grid-auto-flow back to row too - with only one explicit
   column left, "column" flow would otherwise still try to honor
   --financials-rows and spill into extra implicit columns instead of
   just stacking), same breakpoint the account-page's own 2-up grid uses. */
.account-financials-list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(var(--financials-rows, 6), auto);
  grid-auto-flow: column;
  gap: 6px 20px;
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
}
.account-financials-list a { display: flex; align-items: center; gap: 8px; color: var(--text); text-decoration: none; white-space: nowrap; }
.account-financials-list a:hover { color: var(--level-color); text-decoration: underline; }
.account-financials-list i { color: var(--text-dim); }
@media (max-width: 600px) {
  .account-financials-list { grid-template-columns: 1fr; grid-auto-flow: row; }
}
@media (max-width: 600px) { .account-grid-2up { grid-template-columns: 1fr; } }
/* Recent Logins is a real .game-stat-grid table now (2026-09-10, per
   the user) - no extra rules needed beyond that shared component. */
.account-login-table { margin-bottom: 0; }
.account-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.account-status-active { display: inline-flex; align-items: center; gap: 6px; color: #3a9a5c; }
.account-coming-soon { text-align: center; padding: 48px 16px; color: var(--text-dim); }
.account-coming-soon i { font-size: var(--fs-10); color: var(--level-color); margin-bottom: 12px; display: block; }
.account-coming-soon h2 { margin: 0 0 4px; }

/* Overview round 3 (2026-09-09, per the user) */
/* Faded until hover/focus (2026-09-09, per the user) - a secondary
   control shouldn't compete visually with the row's own content. */
.account-edit-pencil { float: right; padding: 4px 6px; color: var(--text-dim); font-size: .82em; background: none; border: none; cursor: pointer; opacity: .45; transition: opacity .15s; }
.account-edit-pencil:hover, .account-edit-pencil:focus-visible { color: var(--level-color); text-decoration: none; opacity: 1; }
.account-api-key { cursor: pointer; }
.account-api-key { font-family: var(--font-body); font-variant-numeric: tabular-nums; letter-spacing: .02em; }

/* Real grid, not flex (2026-09-10, per the user - "put in its own
   column") - icon | title | a dedicated IP column | a Documentation
   column | expiry, so Interstat's IP field and API+'s (IP-less) row
   still line up in the same five columns. Docs column is a fixed
   width, not auto/1fr - "Documentation" is the same word on every row
   regardless of content width either side of it. */
/* Documentation moved to the second column, right after the title
   (2026-09-10, per the user) - icon | title | Documentation | IP | expiry.
   IP column fixed at 230px, not auto (2026-09-10, real bug caught live by
   the user - "documentation column should be fixed width, so both
   documentation links show up cleanly over one another"). Each
   .account-addon-row is its own independent CSS Grid (one per addon), so
   an "auto" column sizes to THAT row's own content only - Interstat's IP
   column holds a real label+input+button form, API+'s is an empty <div>,
   so an auto-width IP column left the two rows with different actual
   widths for it, which pushed the 1fr title column (and everything after
   it, including the already-fixed-110px Documentation column) to two
   different positions. Every non-1fr column now has an explicit width, so
   1fr resolves to the same remainder on both rows and Documentation lines
   up. 230px covers "Authorized IP" + a 16ch input + the save button with
   a little room to spare. */
.account-addon-row { display: grid; grid-template-columns: auto 1fr 110px 230px auto; align-items: center; gap: 16px; padding: 12px 0; border-bottom: 1px solid var(--border); }
.account-addon-row:last-child { border-bottom: none; }
.account-addon-icon { font-size: var(--fs-7); text-align: center; }
.account-addon-icon--interstat { color: #f5c518; }
.account-addon-icon--apiplus { color: #d64545; }
.account-addon-title { color: var(--text); font-weight: 700; }
/* Inline label, not stacked above the field (2026-09-10, real bug
   caught live by the user - the earlier block-level label pushed the
   row taller whenever it wrapped to its own line). Normal text now,
   not the small-caps eyebrow style a stacked label could carry off. */
.account-addon-ip-form { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
.account-addon-ip-form label { color: var(--text-dim); font-size: var(--fs-4); font-weight: 400; }
/* 16ch (2026-09-10, per the user - "shrink IP text area, it's 2x+ as
   wide as an IP") - "255.255.255.255" (IPv4's own longest real form)
   is 15 characters; ch scales with the field's own font rather than a
   flat px guess. */
.account-addon-ip-form input { width: 16ch; padding: 4px 8px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text); font: inherit; font-size: var(--fs-4); }
.account-addon-ip-form button { border: none; background: none; color: var(--text-dim); cursor: pointer; padding: 4px; }
.account-addon-ip-form button:hover { color: #3a9a5c; }
/* Blocked out (2026-09-10, per the user) - no real href, styled
   visibly inert rather than a fake #-link. */
/* No font-size override any more (2026-09-10, per the user - "make them
   the same size as the until date") - inherits the page's plain 1rem
   default, same as .account-addon-expiry's own (also unset) size. */
.account-addon-docs { color: var(--text-dim); opacity: .5; cursor: default; }
/* Same size as .account-row-value (no override - that class has none of
   its own either, just inherits the page's 1rem default), grey via
   color alone (2026-09-10, per the user - "the 'Until' label is half
   the size of other text on the page, make the same size as the right
   text in the top section but grey"). Left padding (2026-09-10, real
   bug caught live - the IP form's own input sat flush against this
   column with nothing between them). */
.account-addon-expiry { color: var(--text-dim); text-align: right; padding-left: 12px; }
/* Real mobile reflow (design-sweep punchlist, 2026-09-10, real bug
   caught live - "it's not responsive, the table blows out the right
   side of the screen on mobile") - the desktop grid's fixed-px columns
   (110px + 230px alone, before the rest) simply can't fit a phone
   viewport, and had no breakpoint at all before this. Same real named-
   area reflow pattern Watchlist's own rows already use - icon/title/
   expiry share the top row, the IP form and Documentation link each get
   their own full-width row below instead of being squeezed into a fixed
   column. */
@media (max-width: 600px) {
  .account-addon-row {
    grid-template-columns: auto 1fr auto;
    grid-template-areas:
      "icon title expiry"
      "ip ip ip"
      "docs docs docs";
    row-gap: 8px;
  }
  .account-addon-icon { grid-area: icon; }
  .account-addon-title { grid-area: title; }
  .account-addon-expiry { grid-area: expiry; }
  .account-addon-ip-col { grid-area: ip; }
  .account-addon-docs { grid-area: docs; }
  .account-addon-ip-form { white-space: normal; }
}

.account-suggestions { position: relative; }
.account-suggestions h2 { margin: 0 0 14px; }
.account-suggestions-dismiss { position: absolute; top: 14px; right: 16px; border: none; background: none; color: var(--text-dim); font-size: var(--fs-7); line-height: 1; cursor: pointer; padding: 4px; opacity: .45; transition: opacity .15s; }
.account-suggestions-dismiss:hover, .account-suggestions-dismiss:focus-visible { color: var(--text); opacity: 1; }
.js-clickable-card { cursor: pointer; }
.account-suggestions-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
@media (max-width: 600px) { .account-suggestions-grid { grid-template-columns: 1fr; } }
.account-suggestion-card { border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; text-align: center; }
.account-suggestion-card h3 { font-family: var(--font-display); font-size: var(--fs-5); margin: 0 0 4px; }
.account-suggestion-card p { color: var(--text-dim); font-size: var(--fs-4); margin: 0 0 14px; }

/* Generic confirm popover (2026-09-09) - same visual pattern as the
   Level-picker's own .levels-confirm-overlay (including the same
   [hidden]-vs-author-rule specificity fix that one needed), kept as a
   separate component rather than reused directly so nothing here can
   regress that already-tested one. Used by Overview's API Key
   regenerate and Profile's email-change confirm. */
.confirm-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, .6); display: flex; align-items: center; justify-content: center; z-index: 100; padding: 16px; }
.confirm-overlay[hidden] { display: none; }
.confirm-card { background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); padding: 32px; max-width: 340px; width: 100%; text-align: center; }
/* Every prior .confirm-card use was a bare confirm question with no
   heading of its own - the export error popover (2026-09-11) is the
   first to need a real title distinguishing "here's what happened" from
   a plain yes/no confirm, so this rule is new, not previously missing. */
.confirm-card h3 { font-family: var(--font-display); font-size: var(--fs-7); margin: 0 0 8px; }
.confirm-card p { color: var(--text-dim); font-size: var(--fs-4); margin: 0 0 20px; }
.confirm-actions { display: flex; gap: 10px; }
.confirm-actions .btn { flex: 1; margin: 0; }
.confirm-cancel { flex: 1; padding: 10px; border: 1px solid var(--border); border-radius: 999px; background: transparent; color: var(--text-dim); font-family: inherit; font-weight: 700; font-size: var(--fs-5); cursor: pointer; }
.confirm-cancel:hover { border-color: var(--text-dim); color: var(--text); }
.account-leagues-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
/* /account/watchlist, round 2 (2026-09-10, per the user - real
   feedback on the first pass):
   - "take out the level headers and teams/player subs. two groups,
     teams and players" - just two .detail-cards (Teams, Players), no
     per-Level grouping - each row carries its own Level pill instead.
   - "the entire text blocks are [a] line and have text-decoration" -
     ONLY .watchlist-name-link is a real <a> now (not the whole row),
     with its own explicit color/text-decoration reset.
   - Round 3 (2026-09-10) - real compaction: "make these no more than
     two lines each; people might have a lot of these. compact... less
     padding. make the columns fixed width." Every column is a real
     fixed px width now (was auto/minmax) - fixes a real bug caught
     live: since each .watchlist-item is its OWN independent CSS Grid
     (not shared rows of one grid), an "auto" Level-pill column sized
     itself PER ROW ("WNBA" vs "MLB"), so a WNBA team's logo didn't
     line up with an MLB team's in the same list - the exact same class
     of bug already found and fixed once before this session (Account
     Overview's own Add-ons rows). Record moved onto the name's own
     line (.watchlist-line1); the old separate "recent headlines"
     block collapsed into a second, optional line under that
     (.watchlist-line2) instead of its own full-width row - the whole
     row is 2 lines now, not 3-4.
   - .watchlist-drag-handle + [draggable] (assets/watchlist-reorder.js)
     - "allow users to change the sort order by dragging up/down." */
.watchlist-list { display: flex; flex-direction: column; }
.watchlist-item {
  display: grid;
  /* fr, not px, for the name/game columns (2026-09-10, round 4, real bug
     caught live - "it's not using the full width of the div and is
     truncating statlines... the X is only about 75% across the screen
     at 1000px width") - fr divides the CONTAINER's own remaining width
     (same for every row in one .watchlist-list), not the row's own
     content, so this doesn't reintroduce the earlier per-row alignment
     bug (that was specifically caused by an auto-sized column, which
     DOES size off each row's own content - a genuinely different
     mechanism from fr). Handle/pill/logo/remove stay real fixed px
     widths - those genuinely need a constant size regardless of
     container width. */
  /* round 6 (2026-09-10) - name column narrowed (1.3fr -> 1fr), game
     column widened (1fr -> 1.3fr), per the user: "the team name can be
     shorter so the last/next/stats cell can have a little more room and
     decrease the potential for truncates" - now-short team Names (round
     5) don't need as much of the row's own width as the fuller last/
     next/statline text in the game column does. */
  grid-template-columns: 16px 42px 28px 1fr 1.3fr 20px;
  align-items: start;
  gap: 10px;
  padding: 7px 0;
  border-bottom: 1px solid var(--border);
}
.watchlist-item:last-child { border-bottom: none; }
.watchlist-item.is-dragging { opacity: .4; }
.watchlist-drag-handle { color: var(--text-dim); cursor: grab; text-align: center; opacity: .5; margin-top: 4px; }
.watchlist-drag-handle:hover { opacity: 1; }
/* Smaller pill, CENTERED in its column (2026-09-10, round 3, per the
   user - "the pills are kinda big"... "pills are flush left awkwardly,
   that doesn't match anything on the site... fix the width of that
   column and center the pills - WNBA will be longer than MLB, but
   they'll be centered"). justify-self centers this ONE grid item
   within its own fixed-width cell without needing to touch the column
   width or any other item's alignment. margin-top bumped 3px -> 11px
   (round 6, per the user - "drop pills down ~8px so they horizontally
   align with the dragger and logo"), then back down to 8px (round 7,
   per the user - "pills dropped down too much - up by ~3px"). */
.watchlist-item .level-chip { font-size: var(--fs-1); padding: 1px 7px; margin-top: 8px; justify-self: center; }
/* round 5 (2026-09-10) - the logo is now wrapped in <a class="watchlist-logo-link">
   so its Team/Player page link works ("link up... logos to team pages"). The
   grid item is the <a>, not the inner .detail-team-logo span - justify-self
   only affects an element that IS a direct grid child, so it moved here. */
.watchlist-item .watchlist-logo-link,
.watchlist-item .watchlist-logo-placeholder { margin-top: 2px; justify-self: center; display: block; }
.watchlist-col { display: flex; flex-direction: column; gap: 1px; min-width: 0; overflow: hidden; }
/* Round 7 (2026-09-10, per the user - "if there's no headline,
   vertically center the name/rating so it lines up with the dragger/
   pill/logo") - .watchlist-item's grid uses align-items: start, so a
   name-col with only line1 (no headline's line2) sits flush at the
   row's top instead of centered in the row's real height (set by the
   game column's own steady 2 lines). The template adds this class only
   when there's no headline - align-self overrides align-items for just
   this one grid item, centering it in the row without touching any
   other cell's own top alignment. */
.watchlist-name-col.watchlist-col-centered { align-self: center; }
/* Plain flex-start, not space-between (2026-09-10, round 4, real UX
   issue caught live - "the PCR number floats awkwardly between the
   columns, which doesn't match team treatment" - PCR now sits right
   after the name/#/position text, same inline placement Team's own
   record already uses, not pushed out to the edge of a column that
   isn't a meaningful visual boundary on its own). */
.watchlist-line1 { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.watchlist-name-link { color: var(--text); font-weight: 700; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.watchlist-name-link:hover { color: var(--level-color); text-decoration: none; }
/* Team record (round 6, 2026-09-10, per the user - "make record normal
   white, leave ELO grey") - Watchlist::teamStatus() now returns record
   ("66-75") and elo (" - ELO 1263 #25") as two separate fields; this is
   the record half, full-brightness text like the name next to it. The
   ELO half stays on the existing grey .watchlist-sub. font-weight
   corrected 600 -> 400 (round 7 - "make the team records white normal",
   the same "normal" weight already used for player's own #/position
   meta, not the semi-bold this originally shipped with). */
.watchlist-record { color: var(--text); font-size: var(--fs-2); font-weight: 400; white-space: nowrap; }
/* Player #/position (round 6, 2026-09-10, per the user - "drop the
   #/position to same size as the PCR, turn white normal, remove
   interpunct between num and position") - same .78rem size as the
   PCR/.watchlist-sub text next to it, but full-brightness (not grey)
   and NOT inside .watchlist-name-link (so it doesn't inherit that
   class's bold weight) - a plain space joins "#24"/"LF" now, not the
   middot .watchlist-sub-style meta lines use elsewhere. */
.watchlist-meta { color: var(--text); font-size: var(--fs-2); font-weight: 400; white-space: nowrap; }
.watchlist-sub { color: var(--text-dim); font-size: var(--fs-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The game column's Last/Next lines are real links now (round 6, per
   the user - "link game scores to their games on web") - reset the
   default <a> underline/color a bare .watchlist-sub span never had,
   scoped to the anchor specifically so PCR/ELO's own plain-span uses of
   .watchlist-sub are untouched. */
a.watchlist-sub { text-decoration: none; }
a.watchlist-sub:hover { color: var(--level-color); text-decoration: underline; }
.watchlist-line2 { color: var(--text-dim); font-size: var(--fs-2); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.watchlist-line2:hover { color: var(--level-color); text-decoration: underline; }

/* Real mobile layout, not just a smaller version of the desktop grid
   (2026-09-10, round 3, real bug caught live by the user - "it's also
   not responsive - the table stretches off the right side of the
   screen on mobile and creates a huge v-scroll"). The desktop grid's
   fixed pixel columns (16+42+28+254+214+20 + 5x10px gaps = ~624px)
   simply don't fit a phone-width viewport - no amount of shrinking
   individual pieces fixes a fundamentally fixed-width row on a
   fundamentally variable-width screen. Reflows to a real 2-row named
   grid instead: handle/pill/logo run down the left as their own
   column, name+remove share the top row, game info gets the FULL
   width on row 2 - genuinely more room than the cramped desktop game
   column has, so text wraps naturally here instead of needing the
   ellipsis+title fallback desktop still needs for a long statline. */
@media (max-width: 600px) {
  .watchlist-item {
    grid-template-columns: 16px 36px 28px 1fr 20px;
    grid-template-areas:
      "handle pill logo name remove"
      "handle pill logo game game";
    row-gap: 4px;
    column-gap: 8px;
  }
  .watchlist-drag-handle { grid-area: handle; }
  .watchlist-item .level-chip { grid-area: pill; }
  .watchlist-item .watchlist-logo-link,
  .watchlist-item .watchlist-logo-placeholder { grid-area: logo; }
  .watchlist-name-col { grid-area: name; }
  .watchlist-game-col { grid-area: game; }
  .account-watchlist-remove { grid-area: remove; }
  .watchlist-name-link, .watchlist-record, .watchlist-meta, .watchlist-sub, .watchlist-line2 {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
  }
  .watchlist-line1 { flex-wrap: wrap; }
}
/* Profile's Watchlist digest opt-in checkbox (2026-09-10) - plain
   checkbox + label text, no custom-styled toggle switch needed for one
   low-stakes preference. */
.account-checkbox-field { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; cursor: pointer; color: var(--text); }
.account-checkbox-field input { margin: 0; }
.account-success {
  background: color-mix(in srgb, #3a9a5c 15%, var(--bg-raised));
  border: 1px solid color-mix(in srgb, #3a9a5c 40%, var(--border));
  color: #3a9a5c;
  border-radius: var(--radius);
  padding: 10px 14px;
  margin-bottom: 16px;
  font-size: var(--fs-4);
}

/* Captcha (2026-09-09, per the user - "a *simple* five-letter captcha...
   no cloudflare or google crap, just a local solution"). Plain text, not
   an image - deliberately: it stays fully readable to screen readers
   (no alt-text workaround needed), and the per-letter rotation/hue is
   just enough visual noise to discourage a trivial copy-paste script,
   not real bot-defense. */
/* Redesigned (2026-09-09, per the user) - the bordered/backgrounded box
   read as a text input itself, easy to confuse with the real one right
   below it ("the captcha has the same background as the text inputs").
   No more box: no border/background/border-radius, minimal vertical
   padding instead of a full 12px all around. One shared --hue per
   render (moved from each <span> up to this container, set once in PHP)
   so every character is the same color instead of each its own random
   hue ("make the characters the same color") - children read it via
   the ordinary CSS custom-property inheritance, no JS needed. The
   ::after line is the "thin... line through them to semi-obscure them"
   - a single horizontal rule crossing the whole character row, not
   real bot-defense (this class never was one - see the docblock
   above), just enough visual noise to discourage a trivial screen-
   scrape. */
/* inline-flex, not flex (2026-09-10, real bug caught live by the user -
   "the captcha line goes all the way across, can it just affect the
   text") - a block-level flex container fills the whole .auth-field
   width, so the ::after line (left:0;right:0, relative to THIS element)
   spanned the empty space past the real letters too. Shrink-to-fit
   fixes that with no change to the line's own positioning rule. */
/* .captcha-row wraps .captcha-display + the reload button (2026-09-10,
   per the user - "put a small reload button to the right of the
   captcha") - the bottom margin moved here from .captcha-display itself
   so the row reads as one unit. */
.captcha-row { display: inline-flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.captcha-reload { border: none; background: none; color: var(--text-dim); cursor: pointer; padding: 4px; font-size: var(--fs-5); }
.captcha-reload:hover { color: var(--level-color); }
.captcha-display {
  display: inline-flex;
  gap: 6px;
  padding: 4px 0;
  position: relative;
  user-select: none;
}
.captcha-display::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  background: var(--text-dim);
  opacity: .6;
  pointer-events: none;
}
/* One fixed color, not a random hue (2026-09-10, per the user - "can
   the text be the same slate blue/primary site color each time, the
   different colors don't make it any more secure") - var(--level-color)
   is already the site's own primary accent and already theme-correct by
   definition, so the whole --hue custom-property/dark-mode-override
   mechanism this used to need is gone with it. */
.captcha-letter {
  display: inline-block;
  font-family: var(--font-display);
  font-size: var(--fs-8);
  font-weight: 700;
  color: var(--level-color);
  transform: rotate(var(--rot));
  position: relative;
}

/* Table export dropdown (Cross-cutting: table exports, 2026-09-06) -
   the trigger reuses .more-toggle's positioning contract (it's inside a
   .more-wrap, so .more-menu's own absolute/z-index/background rules -
   see that block's own comment about painting over a stat grid - apply
   unchanged) but gets its own compact pill styling rather than
   .primary-nav .more-toggle's nav-bar-specific one, since this sits
   inside a .detail-card, not the header. */
/* +5px top padding (design-sweep punchlist, 2026-09-08 - "the export
   button doesn't smash into the top of the labels") - the row was
   flush with the card's own top edge, same line as the H2 baseline,
   with nothing to keep the pill button from crowding it. */
/* margin-bottom added (design-sweep punchlist, 2026-09-08 - "the
   vertical gap between subheads, instasearches and tables is way too
   small and varies from page to page... reference a roster page") -
   the h2 inside here has its own bottom margin zeroed (right below,
   unchanged - a flex row's own child shouldn't carry layout spacing,
   or its export-menu sibling would sit misaligned), so without this
   the head row sat completely flush against whatever followed (a
   search box or the table itself - confirmed live, 0px gap on the
   Stats tab). Roster's own plain, unwrapped <h2> (no .detail-card-head
   - it has no export menu) relies on the base h2 rule's own `margin: 0
   0 .5em` for this same gap, which measured 9.6px live at its actual
   rendered font-size - 10px here matches that, not a value invented
   fresh, so every table on the site opens with the same visual
   rhythm regardless of which of the two head patterns a given page
   uses. */
.detail-card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding-top: 5px; margin-bottom: 10px; }
.detail-card-head h2 { margin: 0; }
/* League/sublevel filter dropdown (2026-09-13) - shares this wrapper
   with the export menu so .detail-card-head's own space-between still
   only ever sees two children (the h2, and this whole actions
   cluster), keeping both controls together on the right rather than
   spread across the row. */
.detail-card-head-actions { display: flex; align-items: center; gap: 10px; }
.detail-league-picker {
  appearance: none;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--bg-raised); color: var(--text);
  font-family: var(--font-body); font-weight: 700; font-size: var(--fs-2); cursor: pointer;
  padding: 4px 24px 4px 12px;
  background-image: linear-gradient(45deg, transparent 50%, var(--text-dim) 50%), linear-gradient(135deg, var(--text-dim) 50%, transparent 50%);
  background-position: calc(100% - 14px) 55%, calc(100% - 9px) 55%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
.detail-league-picker:hover { border-color: var(--level-color); color: var(--level-color); }
/* 2px smaller all around (design-sweep punchlist - "the entire export
   button is too prominent") - was competing with the H2 next to it for
   attention; a real utility action, not a second heading. */
.export-menu-toggle {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 4px 12px; border: 1px solid var(--border); border-radius: 999px;
  background: var(--bg-raised); color: var(--text);
  font-family: var(--font-body); font-weight: 700; font-size: var(--fs-2); cursor: pointer;
}
.export-menu-toggle:hover { border-color: var(--level-color); color: var(--level-color); }
.export-menu-panel { right: 0; left: auto; }
/* Visible/total record count, flush left of the Export button
   (design-sweep punchlist - "put the indicator of visible records/
   total records... tooltip with 'visible/total'. remove indicator from
   below the table") - replaces each page's own "N teams/players..."
   sentence that used to sit under the table. */
.export-count-indicator { color: var(--text-dim); font-size: var(--fs-3); font-variant-numeric: tabular-nums; cursor: default; }
.export-menu-row { display: flex; align-items: center; gap: 10px; }
/* .more-menu a's own rules don't reach a <button> (Copy isn't a link,
   it fetches then writes to the clipboard) - same visual treatment,
   reset to a plain block first since browsers give <button> its own
   default chrome .more-menu a never had to begin with. */
.export-menu-copy {
  all: unset; box-sizing: border-box; width: 100%; cursor: pointer;
  display: flex; align-items: center; gap: 8px; color: var(--text);
  font-weight: 500; font-family: var(--font-body); font-size: var(--fs-5);
  padding: 8px 10px; border-radius: 6px;
}
.export-menu-copy:hover { background: color-mix(in srgb, var(--level-color) 12%, transparent); }

/* Stat Rankings picker (2026-09-06) - a real <select>, not a
   .more-menu link list, since the choice set (~70-85 stats) is too
   large for that pattern (same reasoning Tempo-Free Aerial's own
   conference picker used for a much smaller list). */
.stat-rankings-picker { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
.stat-rankings-picker label { font-weight: 700; font-size: var(--fs-4); }

/* League Statlines' own Date column (2026-09-06) - a plain "YYYY-MM-DD"
   .mono cell was wrapping onto two lines at the panel's narrower
   widths; every other .mono cell on this table is short enough that
   this never came up before. */
#stat-grid-league-statlines td.mono:first-child,
[id^="stat-grid-league-statlines-"] td.mono:first-child { white-space: nowrap; }

/* Venue Map (2026-09-07) - Leaflet needs an explicit height on its
   container to render at all (a 0-height div silently shows nothing,
   not an error) - a real height, not a padding-hack aspect ratio,
   since the map itself is the whole point of this card. */
/* position:relative + a real z-index (design-sweep punchlist, 2026-09-08
   - "the map is z-indexed over the tab dropdowns") - Leaflet's own
   internal panes use z-index values in the hundreds, and with no
   stacking context of its own here that escaped clean above
   level-nav.php's .more-menu dropdowns (z-index 55). Establishing one
   right at the map container caps every Leaflet-internal z-index inside
   it, same fix as .more-menu's own docblock note about needing an
   explicit z-index to paint over ordinary page content further down -
   just capping in the other direction here. */
.venue-map { position: relative; z-index: 0; width: 100%; height: 480px; border-radius: var(--radius); overflow: hidden; }
.venue-map .leaflet-popup-content-wrapper,
.venue-map .leaflet-popup-tip { background: var(--bg-raised); color: var(--text); }
.venue-map .leaflet-popup-content { font-family: var(--font-body); font-size: var(--fs-4); line-height: 1.5; }
/* Marker icon (design-sweep punchlist, 2026-09-08 - "a real team logo...
   instead of Leaflet's default oversized pin") - a small round badge,
   white/raised surface so a team's own logo (never re-tinted here,
   unlike .detail-team-logo elsewhere on the site - a map benefits from
   each team's real color for fast visual distinction) sits on a
   consistent, readable backing regardless of the tile basemap color
   beneath it. venue-map.js sets iconSize inline (26px logo / 14px dot);
   this class only owns the visual chrome, not the sizing math Leaflet
   itself needs to position the icon's anchor correctly. */
.venue-marker {
  background: var(--bg-raised);
  border: 2px solid var(--level-color);
  border-radius: 50%;
  box-shadow: var(--shadow);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.venue-marker img { width: 100%; height: 100%; object-fit: contain; padding: 2px; }
/* No logo resolved for this venue (e.g. a neutral-site venue with no
   real home team this season) - a plain filled dot instead of an empty
   ring, so it still reads as a marker rather than a rendering glitch. */
.venue-marker-dot { background: var(--level-color); border-width: 0; }
/* Popup redesign (design-sweep punchlist, 2026-09-08) - top 3 lines
   (name, city/state, "Home of...") at the base popup size, last 2
   (capacity/games, home record) 2px smaller, plus a real team logo
   next to the venue name instead of Leaflet's default oversized pin. */
.venue-popup-name { display: flex; align-items: center; gap: 6px; font-weight: 700; }
.venue-popup-small { font-size: calc(.9rem - 2px); color: var(--text-dim); }

/* Vidiprinter (2026-09-07) - a live-updating event feed, built and tested
   against real KHL games as they happened. The live indicator uses a
   literal green, not --level-color - semantic "this is live" status,
   same reasoning as any status pill, kept separate from the page's own
   accent hue. */
.vidi-live-indicator { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-3); font-weight: 600; color: #22a55e; }
.vidi-live-indicator i { font-size: .5rem; animation: vidi-pulse 2s ease-in-out infinite; }
.vidi-live-indicator.is-stale { color: var(--text-dim); }
.vidi-live-indicator.is-stale i { animation: none; }
@keyframes vidi-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* REBUILT (design-sweep punchlist, 2026-09-08, second round - "looks
   absolutely nothing like anything else on the site... the model
   should be the Game play-by-play screen") - same list/row rhythm as
   .game-pbp-list (flex column, 2px gap; each row a flex line, baseline-
   aligned, bottom border, .9rem body size) with columns reordered for
   what this feed actually has: relative time, a level-colored (not a
   hardcoded per-type green/gold - found live on KHL, whose own real
   Level color is a light grey, so the old green read as completely
   disconnected) type icon+abbreviation, a scoreline/matchup, the
   game's own status text, then the description - matching that
   screen's own grey-label/white-description weight split, not
   inventing a new one. */
.vidi-feed { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.vidi-row { border-bottom: 1px solid var(--border); }
.vidi-row:last-child { border-bottom: none; }
.vidi-row-link {
  display: flex; align-items: baseline; gap: 10px; padding: 4px 4px;
  color: inherit; text-decoration: none; font-size: var(--fs-4);
}
.vidi-row-link:hover { text-decoration: none; background: color-mix(in srgb, var(--level-color) 6%, transparent); }
.vidi-row-time { flex: none; width: 32px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
/* Type/status/desc all share the row's own base font-size now (design-
   sweep punchlist, 2026-09-09 - "the text sizes aren't consistent: the
   play type and inning are smaller than anything else") - neither
   column sets its own font-size anymore, both inherit .vidi-row-link's
   .9rem. The circle-icon dot is gone too (same punchlist - "remove the
   color dot"), replaced by .vidi-row-logo's real per-team minilogo. */
.vidi-row-type {
  flex: none; width: 78px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .02em;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.vidi-row-score { flex: none; width: 130px; color: var(--text-dim); font-variant-numeric: tabular-nums; font-weight: 600; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.vidi-row-status { flex: none; width: 56px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.vidi-row-logo { flex: none; width: 18px; display: flex; align-items: center; }
.vidi-row-logo .detail-team-logo { width: 16px; height: 16px; }
.vidi-row-desc { flex: 1; min-width: 0; color: var(--text); font-weight: 400; }
.vidi-row[data-priority='1'] .vidi-row-desc { font-weight: 700; }
.vidi-row-secondary { color: var(--text-dim); font-weight: 400; } /* light grey, normal weight - not bold (per the user, 2026-09-20) */
/* FC only (per the user, 2026-09-19): its country-prefixed team codes ("ESP-RACI 0 ESP-CELT 2") were cut off at 130px, and the
   minute column ("43'") has room to spare - 30px moves from one to the other. */
.vidi-feed[data-level="FC"] .vidi-row-score { width: 160px; }
.vidi-feed[data-level="FC"] .vidi-row-status { width: 26px; }
@media (max-width: 640px) {
  .vidi-row-type, .vidi-row-status { display: none; }
  .vidi-row-score, .vidi-feed[data-level="FC"] .vidi-row-score { width: auto; }
}

/* Shared "simple menu" grid (nav wireup, design-sweep punchlist,
   2026-09-08) - Stats/Ratings/Seasons menu pages, each reached only
   from the global top nav's More dropdown, not from within a Level
   Hub tab. Plain clickable cards, same visual weight as a stat-box
   (.game-forecast-item) rather than a dropdown-style flat list, since
   this IS the whole page's content, not a small popup. */
.level-menu-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }
.level-menu-link {
  display: flex; align-items: center; gap: 10px; padding: 14px 16px;
  border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums;
}
a.level-menu-link:hover { border-color: var(--level-color); color: var(--level-color); text-decoration: none; }
.level-menu-link i { color: var(--text-dim); font-size: 1.1em; }
/* Seasons' own entries are plain labels, not links (see that page's own
   docblock for why) - a lighter, non-interactive treatment so they
   don't read as clickable when they aren't. */
.level-menu-static { justify-content: space-between; color: var(--text-dim); font-weight: 400; cursor: default; }
.level-menu-static.is-current { color: var(--text); font-weight: 600; border-color: var(--level-color); }
/* A season with real played games IS a real link now (design-sweep
   punchlist, 2026-09-08, second round) - .level-menu-static's own
   cursor:default (right, for the still-unplayed-season <span> case)
   would read as broken on a genuinely clickable <a>, so the anchor
   variant gets a real pointer back; a.level-menu-link:hover (already
   defined above) still applies its usual border/color change on top
   of this class's own muted resting look. */
a.level-menu-static { cursor: pointer; }
.level-menu-badge { font-size: var(--fs-2); font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--level-color); }

/* /subscribe (Phase 4 punchlist step 2) - a wider sibling of .auth-page's
   centered-card shell for a multi-tier grid, reusing the same tokens
   (.auth-submit/.auth-error/.auth-subtitle unchanged) rather than
   inventing a parallel button/error system. */
/* padding-top removed - see .auth-page's own comment on why (double
   padding stacked on top of <main class="content">'s own 32px). */
.subscribe-page { max-width: 900px; margin: 0 auto; padding: 0 16px 24px; }
/* /league-menu (2026-09-10) - same shell shape as .subscribe-page, a
   plain public content page, not the account area. */
.league-menu-page { max-width: 700px; margin: 0 auto; padding: 0 16px 24px; }
.subscribe-intro { text-align: center; margin-bottom: 32px; }
.subscribe-intro h1 { font-family: var(--font-display); font-size: var(--fs-10); margin: 0 0 16px; }
.subscribe-copy { text-align: left; max-width: 640px; margin: 0 auto; color: var(--text-dim); font-size: var(--fs-4); line-height: 1.55; }
.subscribe-copy p { margin: 0 0 14px; }
.subscribe-copy p:last-child { margin-bottom: 0; }
.subscribe-copy a { color: var(--level-color); }
.subscribe-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 16px;
}
/* Monthly/Annual only (per the user) - a fixed 4-up/2-up grid rather
   than auto-fit, since their card count is stable now that sale items
   have their own section instead of living here. */
.subscribe-grid--fixed { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 600px) { .subscribe-grid--fixed { grid-template-columns: repeat(2, 1fr); } }
.subscribe-section { margin-bottom: 36px; }
.subscribe-empty {
  color: var(--text-dim);
  font-size: var(--fs-4);
  font-style: italic;
  padding: 20px 0;
}
/* .subscribe-card--featured - the "highlighted box" treatment for a real
   sale ticket, once the Office-side toggle to show one exists. Not
   applied anywhere yet - defined ahead of that so the shell above is
   ready to use it. */
.subscribe-card--featured {
  border-color: var(--level-color);
  box-shadow: 0 0 0 1px var(--level-color), var(--shadow);
}
.subscribe-card--featured::before {
  content: "Sale";
  position: absolute;
  top: -10px;
  left: 20px;
  background: var(--level-color);
  color: #fff;
  font-size: var(--fs-1);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  padding: 2px 10px;
  border-radius: 999px;
}
.subscribe-section-title {
  font-family: var(--font-display);
  font-size: var(--fs-6);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--text-dim);
  margin: 0 0 4px;
}
.subscribe-section-blurb {
  color: var(--text-dim);
  font-size: var(--fs-4);
  margin: 0 0 14px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
}
.subscribe-card {
  position: relative;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 24px;
  display: flex;
  flex-direction: column;
}
.subscribe-card h3 { font-family: var(--font-display); font-size: var(--fs-6); margin: 0 0 12px; }
.subscribe-price { font-size: var(--fs-8); font-weight: 700; font-variant-numeric: tabular-nums; margin-bottom: 4px; }
.subscribe-cadence { font-size: var(--fs-3); font-weight: 400; color: var(--text-dim); }
.subscribe-levels { color: var(--text-dim); font-size: var(--fs-4); margin: 0 0 16px; }
.subscribe-bullets {
  list-style: none;
  margin: 0 0 20px;
  padding: 0;
  font-size: var(--fs-3);
  color: var(--text-dim);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.subscribe-bullets li { padding-left: 16px; position: relative; }
.subscribe-bullets li::before { content: "\2022"; position: absolute; left: 0; color: var(--level-color); }
.subscribe-card form { margin-top: auto; }
.subscribe-dots { position: absolute; top: 18px; right: 20px; display: flex; gap: 4px; }
.subscribe-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }

/* /account/levels (Phase 4 punchlist step 2, the Level-picker) - reuses
   .auth-subtitle/.auth-error/.auth-submit unchanged. Cube grid is
   deliberately dense (3-4 across, per the user) rather than a wide card
   per Level - a 30-deep pick list has to stay workable on both desktop
   and mobile without scrolling forever. */
/* padding-top removed - see .auth-page's own comment on why (double
   padding stacked on top of <main class="content">'s own 32px). */
/* Shared by every /account/* page (2026-09-09, per the user - Overview
   and Security used to sit in a boxed .auth-card, Leagues didn't, and
   they read as mismatched; Leagues' own open, .detail-card-based layout
   was the one that already "looks like the rest of the site," so
   Overview/Security moved to match it instead). Renamed from
   .levels-page/.levels-intro (this class used to be Leagues-only) - see
   account-levels-page.php's own docblock for the full reasoning. */
/* Widened 720px -> 820px (2026-09-09, per the user - "widen the
   min-width enough to fit a Support tab after Watchlist") - confirmed
   live, the 7-tab nav (adding Support) needed 688px just for the tabs
   themselves, leaving no margin inside the old 720px container and
   wrapping "Billing" onto its own second line. Plenty of room to spare
   at real desktop widths (1280px+) either way. */
.account-page { max-width: 820px; margin: 0 auto; padding: 0 16px 80px; }
/* No <h1> in here any more (2026-09-09, per the user - "having the tabs
   make the H1's redundant") - account-nav.php is the only page-identity
   marker on /account/* now. Only account-levels-page.php still uses this
   wrapper, for the picker error message's spacing. */
.account-intro { text-align: center; margin-bottom: 32px; }
.account-intro .auth-subtitle { margin: 0 auto; max-width: 480px; }
.levels-empty { text-align: center; padding: 32px; }
.levels-empty p { color: var(--text-dim); margin-bottom: 16px; }

.levels-group {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 20px;
  margin-bottom: 20px;
}
/* Text-size sweep (2026-09-09, per the user - "the league page was done
   first so the text styles/sizes aren't in alignment with the rest of
   the support section... mostly smaller"): .levels-group h2 kept its
   own bespoke 1.05rem from before the rest of /account/* was unified to
   the shared .account-page .detail-card h2 size - .levels-group isn't a
   .detail-card (deliberately, its own grey background stays - see that
   class's own comment), so it never picked up that later fix. Same
   value applied directly here instead. .levels-group-meta bumped to
   match .account-row-label's own .88rem (same role - a small secondary
   descriptor next to a real heading/row). */
.levels-group h2 { font-family: var(--font-display); font-size: calc(clamp(1.2rem, 3vw, 1.625rem) - 2px); margin: 0 0 14px; display: flex; align-items: center; gap: 8px; }
.levels-group-meta { font-size: var(--fs-4); font-weight: 400; color: var(--text-dim); text-transform: uppercase; letter-spacing: .04em; }
.levels-group.is-fixed .levels-group-meta { color: var(--level-color); }

/* A fixed small cube size (2026-09-09, real bug found live by the user,
   twice - a column-COUNT-based grid, whether 4-up or 12-up, still
   stretches each cube to fill the container's leftover width, so a
   5-cube free-choice group ends up with the exact same "huge box, tiny
   icon" problem a 30-cube fixed group did. Fixed cube SIZE instead of a
   fixed cube COUNT - auto-fill with a non-1fr second minmax value packs
   as many true 64px squares as fit, left-aligned, same result whether
   there are 5 cubes or 30, no separate fixed/free-choice rule needed. */
.level-cubes { display: grid; grid-template-columns: repeat(auto-fill, 64px); gap: 8px; }
.level-cube {
  position: relative;
  width: 64px;
  height: 64px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg);
  /* +2px (2026-09-09, per the user - "super tiny, even moreso with the
     text adjustments around them") - was .62rem (~10px). */
  font-size: var(--fs-2);
  font-weight: 700;
  text-align: center;
  padding: 4px;
}
.level-cube-icon { font-size: var(--fs-7); }
.level-cube.is-open {
  border-style: dashed;
  color: var(--text-dim);
  cursor: pointer;
}
.level-cube.is-open:hover, .level-cube.is-open.is-dragover { border-color: var(--level-color); color: var(--level-color); background: color-mix(in srgb, var(--level-color) 8%, var(--bg)); }
.level-cube-plus { font-size: var(--fs-8); font-weight: 400; line-height: 1; }
/* cursor: pointer added (2026-09-09, per the user) - every filled cube
   now opens its Level's overview in a new tab on click, fixed groups
   included. */
.level-cube.is-filled { background: var(--bg-raised); border-color: var(--border); cursor: pointer; }
.level-cube.is-unlocked.is-dragover { border-color: var(--level-color); box-shadow: 0 0 0 2px color-mix(in srgb, var(--level-color) 40%, transparent); }
.level-cube.is-locked { opacity: .6; }
/* Days-until-change ring (2026-09-09, per the user - replaces a raw
   "until {date}" text, which read as an expiration rather than a
   countdown to being able to change). Same corner position/size as
   .level-cube-swap so a cube's top-right corner is always the one
   "what can I do here" indicator, whichever state it's in. */
/* Sized to match .level-cube-swap's own existing offset (2026-09-09,
   real bug found live by the user - the first "enlarged" pass, 32px at
   a -12px offset, physically touched the next cube over since cubes
   only have an 8px gap; -6px was already the safe offset .level-cube-swap
   itself used the whole time, just never noticed because that badge was
   smaller). Bigger than the original 20px (which was the "2px, can't
   see it" complaint) without repeating this bug - stroke-width/font-size
   inside the SVG's own viewBox carry the legibility gain now, not the
   outer container size. */
.level-cube-cooloff-ring { position: absolute; top: -6px; right: -6px; width: 24px; height: 24px; }
.level-cube-cooloff-bg { fill: var(--bg-raised); stroke: var(--border); stroke-width: 1; }
.level-cube-cooloff-track { fill: none; stroke: var(--border); stroke-width: 3.5; }
.level-cube-cooloff-fill {
  fill: none;
  stroke: var(--level-color);
  stroke-width: 3.5;
  stroke-linecap: round;
  transform: rotate(-90deg);
  transform-origin: center;
}
/* font-size bumped 11px -> 14px (2026-09-09, real bug caught live by the
   user - "the '15' is so small it could be mistaken for a '13'" - the
   ring container itself was already the right size, only the digits
   inside it were too small to read confidently at a glance). */
.level-cube-cooloff-num { font-size: 14px; font-weight: 700; text-anchor: middle; fill: var(--text); font-family: var(--font-body); }
/* Subtle (2026-09-09, per the user) - a muted outline instead of a solid
   red disc, once cooloff has actually elapsed and a swap becomes
   possible. Same corner slot/size the ring badge uses on the same cube
   while locked, so the two states read as one continuous indicator, not
   two unrelated widgets. */
.level-cube-swap {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg-raised);
  color: var(--text-dim);
  font-family: inherit;
  font-size: var(--fs-3);
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.level-cube-swap:hover { border-color: #d64545; color: #d64545; }
.level-cube.is-swap-target { box-shadow: 0 0 0 2px #d64545; }

/* Restyled to a plain .detail-card (2026-09-09, per the user - "convert
   the Available Leagues div to look like the other account pages: ring
   border, black back, subheader same size as other pages") - this used
   to carry the same background/box-shadow "grey card" treatment
   .levels-group still deliberately keeps ("the grey card on the leagues
   is good because it stands out" - that one card is untouched). Now
   just adds this section's own layout rules (below) on top of
   .detail-card's own border/padding/spacing, applied via a second class
   in the markup. */
/* Grouped by sport, collapsed by default (2026-09-09, per the user) -
   each chip shows the Level's own short code, not its full name (a
   title tooltip carries that). Grouping alone still left a genuine
   "two-screen scroll" on mobile - collapsing each sport behind a toggle
   is what actually fixes that; default state is closed, .is-open (set by
   the inline script) reveals that one sport's grid. */
.levels-palette-sport { margin-bottom: 8px; }
.levels-palette-sport:last-child { margin-bottom: 0; }
/* font-family: inherit is required here, not decorative (2026-09-09,
   real bug caught live by the user - "breaks the Dosis rule") - a
   <button> doesn't inherit the page's own font-family by default in any
   browser, it uses the OS/browser UI font instead, unlike a <div>/<span>.
   .detail-nav .more-toggle already carries the same fix elsewhere in
   this file for the exact same reason - every NEW button needs it too,
   not just that one. */
.levels-palette-sport-toggle {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 14px;
  font-family: inherit;
  font-size: var(--fs-3);
  font-weight: 700;
  color: var(--text);
  cursor: pointer;
}
.levels-palette-sport-count {
  font-size: var(--fs-2);
  font-weight: 400;
  color: var(--text-dim);
  background: var(--bg-raised);
  border-radius: 999px;
  padding: 1px 8px;
}
.levels-palette-sport-toggle:hover { border-color: var(--level-color); }
.levels-palette-sport.is-open .levels-palette-sport-toggle { border-color: var(--level-color); border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.levels-palette-grid { display: none; flex-wrap: wrap; gap: 8px; padding: 12px; border: 1px solid var(--level-color); border-top: none; border-radius: 0 0 8px 8px; }
.levels-palette-sport.is-open .levels-palette-grid { display: flex; }
/* .picker-chip, NOT .level-chip (2026-09-09, real regression found live) -
   .level-chip is already the site-wide colored Level pill (story cards,
   .detail-header, .elo-list - defined ~1100 lines above this file).
   Reusing that exact name here for the picker's own palette chip
   silently overrode background: var(--chip-color) with this rule's own
   background: var(--bg), turning every front-page Level pill flat/black
   site-wide - caught by the user, not by any check here. Never reuse an
   existing class name without checking for one first. */
.picker-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg);
  font-size: var(--fs-3);
  font-weight: 700;
  cursor: grab;
  user-select: none;
}
.picker-chip:hover { border-color: var(--level-color); }
.picker-chip.is-armed { border-color: var(--level-color); background: color-mix(in srgb, var(--level-color) 12%, var(--bg)); box-shadow: 0 0 0 1px var(--level-color); }
/* No open slots anywhere (2026-09-09, per the user - "levels should not
   be draggable if the user has no slots available") - draggable="false"
   in the markup already stops real drag-and-drop; this is the matching
   visual/click-affordance state so it doesn't look interactive either. */
.picker-chip.is-inert { opacity: .4; cursor: not-allowed; }
.picker-chip.is-inert:hover { border-color: var(--border); }

/* Pre-action "Are you sure?" gate (2026-09-09, per the user - "the
   confirmation is wrong, the confirmation should be *Are you sure*" -
   supersedes an earlier same-day post-action "you picked X" popup
   entirely). One reusable modal, populated via JS right before a real
   assign/change would fire - Confirm actually submits the form, Cancel
   (or a click on the backdrop) just clears the pending action, nothing
   is committed either way until Confirm. */
/* display:none as the base state, NOT relying on [hidden] alone (real
   bug caught live, 2026-09-09) - the HTML `hidden` attribute and an
   author rule setting `display` on the same class have EQUAL CSS
   specificity, and an author stylesheet always wins over the browser's
   own UA default ([hidden] { display: none }) at equal specificity -
   so this class's own display: flex silently defeated `hidden` and the
   modal showed on every page load. [hidden] below now sets display back
   to none explicitly, so removing the attribute (JS toggles it) is what
   actually reveals the modal, not the class alone. */
.levels-confirm-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, .6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
  padding: 16px;
}
.levels-confirm-overlay[hidden] {
  display: none;
}
.levels-confirm-card {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 32px;
  max-width: 340px;
  width: 100%;
  text-align: center;
}
.levels-confirm-icon { display: block; font-size: var(--fs-11); margin-bottom: 12px; }
.levels-confirm-card h3 { font-family: var(--font-display); font-size: var(--fs-7); margin: 0 0 8px; }
.levels-confirm-card p { color: var(--text-dim); font-size: var(--fs-4); margin: 0 0 20px; }
.levels-confirm-actions { display: flex; gap: 10px; }
.levels-confirm-actions .auth-submit { margin-top: 0; }
.levels-confirm-cancel {
  flex: 1;
  padding: 11px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: var(--text-dim);
  font-family: inherit;
  font-weight: 700;
  font-size: var(--fs-5);
  cursor: pointer;
}
.levels-confirm-cancel:hover { border-color: var(--text-dim); color: var(--text); }

/* Drag hint moved here from the page-level subtitle (2026-09-09, per the
   user - short, and "cube" never appears in user-facing copy, only in
   this codebase's own internal naming). */
/* .92rem to match .account-section-empty's own size (2026-09-09, text-
   size sweep - same role, a brief informational line). */
.levels-palette-hint { color: var(--text-dim); font-size: var(--fs-4); margin: -6px 0 14px; }

/* /subscribe/success's own spinner (2026-09-09, per the user - "as long
   as you put a rotator on it... you can buy 3-5 seconds with that
   approach") - a plain rotating ring, respects prefers-reduced-motion
   (falls back to a static ring - the page still forwards on its own
   timer regardless, so a still ring loses nothing functionally). */
.subscribe-success-card { text-align: center; }
.subscribe-success-spinner {
  width: 40px;
  height: 40px;
  margin: 0 auto 20px;
  border-radius: 50%;
  border: 3px solid var(--border);
  border-top-color: var(--level-color);
  animation: subscribe-spin 0.8s linear infinite;
}
@media (prefers-reduced-motion: reduce) { .subscribe-success-spinner { animation: none; } }
@keyframes subscribe-spin { to { transform: rotate(360deg); } }

/* Mt. Frontend Phase 5, Support desk (2026-09-11) - /support (public FAQ
   + ticket submission) and /account/support (ticket history). Reuses
   .subscribe-page's own max-width/padding shell rather than a new one -
   both are plain single-column public pages. */
.support-page { max-width: 760px; margin: 0 auto; padding: 0 16px 32px; }
/* The H1 doubles as a "back to menu" control (2026-09-11, per the user) -
   a plain <button> styled to read exactly like the site's other bare H1s,
   not like a button (no border/background/padding, inherits the h1's own
   size/weight/color, font-family: inherit since form controls don't pick
   up Dosis by default - see the global input/textarea/select/button rule
   this would otherwise need to duplicate). */
.support-title-link { background: none; border: none; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; text-align: left; }
.support-intro { color: var(--text-dim); margin: 0 0 20px; }
.support-received { margin-bottom: 20px; }
.support-sections { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 24px; }
/* [hidden] and this class's own `display: grid` are equal-specificity,
   same-origin selectors - an author rule always wins over the browser's
   UA default ([hidden] { display: none }) at a tie, so JS setting the
   `hidden` attribute alone silently failed to hide this grid (real bug,
   caught live 2026-09-11 testing the FAQ deep-link fix below - the topic
   menu stayed visible above the opened panel). Same fix already applied
   to .more-menu/.confirm-overlay/.levels-confirm-overlay elsewhere in
   this file for the identical reason. */
.support-sections[hidden] { display: none; }
@media (max-width: 600px) { .support-sections { grid-template-columns: 1fr; } }
.support-section-btn {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  padding: 14px 16px; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg-raised); color: var(--text); font-family: inherit; font-size: var(--fs-5);
  cursor: pointer; transition: border-color .15s;
}
.support-section-btn:hover, .support-section-btn:focus-visible { border-color: var(--level-color, var(--accent)); }
.support-section-btn i { color: var(--text-dim); width: 18px; text-align: center; flex: none; }
.support-panel h2 { margin: 0 0 16px; }
.support-faq { margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
.support-faq:last-of-type { border-bottom: none; margin-bottom: 8px; padding-bottom: 0; }
.support-faq h3 { font-size: var(--fs-5); margin: 0 0 8px; }
.support-faq-answer { color: var(--text-dim); font-size: var(--fs-4); line-height: 1.6; }
.support-faq-answer p { margin: 0 0 10px; }
.support-back-btn { margin-top: 8px; margin-right: 8px; }
.support-phone-note { color: var(--text-dim); font-size: var(--fs-4); margin-top: 28px; text-align: center; }

/* Contact form modal (2026-09-11, "make that a modal like Data
   Corrections") - reuses the shared .confirm-overlay/.confirm-card shell
   (same one corrections-modal-card overrides for its own textarea-only
   form), sized wider since this form carries 4 fields, not 1. */
.support-form-modal-card { text-align: left; max-width: 440px; }
.support-form-modal-card h3 { margin: 0 0 4px; }
#support-form-modal-topic { color: var(--text-dim); font-weight: 400; }
.support-form-note { color: var(--text-dim); font-size: var(--fs-4); margin: 0 0 14px; }
.support-form-modal-card textarea { width: 100%; resize: vertical; }

.support-ticket-list { display: flex; flex-direction: column; gap: 16px; }
.support-ticket { border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; }
.support-ticket-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; font-size: var(--fs-3); }
.support-ticket-no { font-family: var(--font-display); color: var(--text-dim); }
.support-ticket-type { text-transform: uppercase; letter-spacing: .04em; color: var(--text-dim); }
.support-ticket-status { padding: 2px 8px; border-radius: 999px; font-weight: 700; font-size: var(--fs-2); }
.support-ticket-status.is-pending { background: rgba(230, 165, 60, .18); color: #e6a53c; }
.support-ticket-status.is-done { background: rgba(70, 180, 120, .18); color: #46b478; }
.support-ticket-date { margin-left: auto; color: var(--text-dim); }
.support-ticket-text { white-space: pre-wrap; margin: 0; }
.support-ticket-response { margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--border); }
.support-ticket-response-label { font-size: var(--fs-2); text-transform: uppercase; letter-spacing: .04em; color: var(--text-dim); margin: 0 0 4px; }
.support-ticket-response p { white-space: pre-wrap; margin: 0; }

/* Data Corrections footer modal (2026-09-11) - form content inside the
   shared .confirm-card shell; overrides its centered-text default since
   this card holds a real form, not a short confirm message. */
.corrections-modal-card { text-align: left; max-width: 420px; }
.corrections-modal-card h3 { margin: 0 0 8px; }
.corrections-modal-card > p { margin: 0 0 16px; }
.corrections-modal-card textarea { width: 100%; resize: vertical; }
.corrections-toast {
  position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
  z-index: 200; box-shadow: var(--shadow);
}

/* Flat/legal-style pages (2026-09-11) - /about, /legal, /privacy,
   /updates. Same shell shape as .support-page/.subscribe-page - a plain
   single-column public page, no Level context. */
.static-page { max-width: 700px; margin: 0 auto; padding: 0 16px 32px; }
.static-page .detail-prose { max-width: none; }
.static-page .account-coming-soon { padding-top: 24px; }

/* API docs (2026-09-14, Phase 7 docs migration) - /api-v2/docs,
   /api-v3/docs, /api-v4/docs. Wider than .static-page's 700px prose cap:
   real content here includes level-code and parameter reference tables
   that read poorly squeezed into a 65ch/700px column. No sidebar (a
   deliberate choice, not an oversight - this is reference material, not
   a drilldown page, so Team/Game/Player's own .page-layout+sidebar
   treatment doesn't apply), same shell shape as .static-page otherwise. */
.docs-page { max-width: 960px; margin: 0 auto; padding: 0 16px 32px; }
.docs-page .detail-prose { max-width: none; }
.docs-page .detail-prose table {
  width: 100%; border-collapse: collapse; margin: 8px 0 14px; display: block; overflow-x: auto;
}
.docs-page .detail-prose table tr { border-bottom: 1px solid var(--border); }
.docs-page .detail-prose table td, .docs-page .detail-prose table th {
  padding: 8px 12px; text-align: left; white-space: nowrap;
}
.docs-page .detail-prose code {
  background: var(--bg-raised); border: 1px solid var(--border); border-radius: 4px;
  padding: 1px 6px; font-size: .9em;
}
.docs-page .detail-prose blockquote {
  background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 14px 16px; margin: 8px 0 14px;
}
.docs-page .detail-prose blockquote pre { margin: 0; white-space: pre-wrap; word-break: break-word; }
.docs-page .account-coming-soon { padding-top: 24px; }

/* Header action row (2026-09-14, per the user, through several live
   revisions) - "a pulldown top right to navigate between all 5 API
   docs" -> "tiny and a hard target, put it in house style for
   pulldowns" (swapped .more-toggle/.more-menu for a native <select>,
   .detail-season-picker's own established look) -> "a link to
   [lookup] can be universalized and put to the left of the top-right
   dropdown" -> "take the endpoints links out of the tabs and put it up
   on that top level... Endpoints, Code Lookup, API pulldown" (final
   real order, both links share one .docs-header-link style - no icon
   on Code Lookup, "it looks like *that's* the search" per the user,
   correctly: the real search input lives on the destination page, this
   is only a nav link to get there). .detail-header is already a flex
   row (align-items: baseline, gap: 12px); margin-left: auto on the
   WRAPPER (not the select alone) pushes the whole row to the far right
   together. */
.docs-header-actions { margin-left: auto; display: flex; align-items: center; gap: 16px; }
.docs-header-link { color: var(--text-dim); font-size: var(--fs-4); font-weight: 500; white-space: nowrap; }
.docs-header-link:hover { color: var(--level-color); text-decoration: none; }
/* Slightly bolder than a bare .detail-season-picker (a real border +
   background, not fully borderless) since this sits alone in the
   header rather than right next to an H1 of matching visual weight -
   it needs to read as its own control, not a quiet modifier on the
   title. */
.docs-version-picker { border: 1px solid var(--border); border-radius: 6px; padding: 6px 26px 6px 12px; font-size: var(--fs-5); }
.docs-version-picker:hover { border-color: var(--level-color); }

/* Endpoints tab (2026-09-14) - list view grid of endpoint cards, and the
   detail view's example/breadcrumb chrome. Parameter tables reuse the
   generic ".docs-page .detail-prose table" rule above as-is (no separate
   rule needed - .docs-param-table is just a marker class for the JS-free
   styling to key off nothing extra). */
/* Instasearch (2026-09-14, per the user) - reuses .docs-lookup-input's
   own styling directly (see that class further below), just placed
   above this grid instead of inside a .docs-lookup-box. */
.docs-endpoints-search { margin-top: 12px; max-width: 320px; }
.docs-endpoints-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; margin-top: 16px;
}
.docs-endpoint-card {
  display: block; padding: 14px 16px; background: var(--bg-raised); border: 1px solid var(--border);
  border-radius: var(--radius); color: var(--text); text-decoration: none;
}
/* [hidden] and .docs-endpoint-card's own `display: block` are
   equal-specificity, same-origin selectors - without this override the
   author rule wins the cascade tie and the card stays visible even
   once docs-endpoints-search.js sets [hidden] on it. Same bug/fix
   already documented elsewhere in this file (.more-menu[hidden], the
   Account Leagues confirm-modal). */
.docs-endpoint-card[hidden] { display: none; }
.docs-endpoint-card:hover { border-color: var(--level-color); text-decoration: none; }
.docs-endpoints-empty { color: var(--text-dim); font-style: italic; grid-column: 1 / -1; }
.docs-endpoint-card-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.docs-endpoint-name { font-family: var(--font-display); font-weight: 700; font-size: var(--fs-5); }
.docs-endpoint-type {
  font-size: var(--fs-2); text-transform: uppercase; letter-spacing: .04em; color: var(--text-dim);
  background: var(--bg); border-radius: 4px; padding: 2px 6px;
}
.docs-endpoint-subhead { color: var(--text-dim); font-size: var(--fs-4); margin-top: 4px; }
.docs-endpoint-breadcrumb { margin-bottom: 4px; }
.docs-endpoint-meta { display: flex; gap: 8px; align-items: center; }
.docs-endpoint-example {
  background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 14px 16px; margin: 8px 0 14px; display: flex; flex-direction: column; gap: 6px; align-items: flex-start;
}
.docs-endpoint-example-label { font-weight: 700; color: var(--text-dim); font-size: var(--fs-3); }

/* Interstat's Overview tab (2026-09-14) - .largeintro carries forward
   legacy's own real distinction between hero-weight intro copy and
   regular body paragraphs (api-is-docs.html's own CSS had this same
   class), just token-based instead of a hardcoded color. Its hero image
   gets a radius to match the rest of this site's card language, not
   legacy's flush-edge original. */
.docs-page .detail-prose .largeintro { font-size: var(--fs-5); color: var(--text); }
.docs-page .detail-prose img { border-radius: var(--radius); margin-bottom: 8px; }

/* Lookup tab (/{version}/docs/lookup, 2026-09-14) - the one shared
   team/league/player code lookup, reachable from every doc set's own
   header via .docs-header-link. See Frontend\Content\DocsLookup's own
   docblock for why this replaced the earlier per-tab "just point at
   /search" fix. Three independent search boxes, each with its own
   live-region results list (docs-lookup.js). */
.docs-lookup-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; margin-top: 16px;
}
.docs-lookup-box {
  background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px;
}
.docs-lookup-box h3 { margin: 0 0 10px; font-size: var(--fs-5); }
/* Input + "Limit..." pulldown share one row (2026-09-14, per the user -
   "let's put a 'Limit...' pulldown to allow just that Level/League to
   be shown"). min-width: 0 on the input is the same real flex-shrink
   fix already applied to .docs-lookup-name below - without it the
   input refuses to shrink below its own content/intrinsic width and
   the fixed-width select gets pushed out of the box. */
.docs-lookup-controls { display: flex; gap: 8px; }
.docs-lookup-input {
  flex: 1; min-width: 0; box-sizing: border-box; padding: 8px 12px; border: 1px solid var(--border);
  border-radius: 6px; background: var(--bg); color: var(--text); font-family: var(--font-body); font-size: var(--fs-5);
}
.docs-lookup-input:focus { outline: none; border-color: var(--level-color); }
.docs-lookup-limit {
  flex-shrink: 0; width: 96px; box-sizing: border-box; padding: 8px 8px; border: 1px solid var(--border);
  border-radius: 6px; background: var(--bg); color: var(--text); font-family: var(--font-body); font-size: var(--fs-4);
}
.docs-lookup-limit:focus { outline: none; border-color: var(--level-color); }
/* Right padding keeps every row's own content (especially the name,
   see its own truncation rule below) clear of the scrollbar track
   itself - REVISED 2026-09-14, per the user: "teams gets blown out
   sometimes and overlaps the scrollbar." */
.docs-lookup-results { list-style: none; margin: 10px 0 0; padding: 0 6px 0 0; max-height: 260px; overflow-y: auto; }
.docs-lookup-results li {
  display: flex; align-items: center; gap: 8px; padding: 6px 2px; border-bottom: 1px solid var(--border); font-size: var(--fs-4);
}
.docs-lookup-results li:last-child { border-bottom: none; }
/* Level pill - fixed width, centered text, tiny (2026-09-14, per the
   user - "tiny level pills on the left (fixed width/centered
   Watchlist-style)"). Reuses .level-chip as-is (the same colored pill
   every other page on the site uses - real per-Level color read from
   docs-lookup-page.php's own embedded color map, see docs-lookup.js)
   with a size/width override matching .watchlist-item .level-chip's
   own compact treatment, plus the fixed width + centering that one
   didn't need (a watchlist row has only one pill; a lookup row is a
   dense list where every pill needs to line up in a column). flex-
   shrink: 0 keeps it from ever being squeezed by a long name. */
.docs-lookup-level-chip {
  flex-shrink: 0; width: 52px; text-align: center; font-size: var(--fs-1); padding: 1px 4px;
}
/* min-width: 0 is the real fix, not overflow-hidden alone - a flex
   item's default min-width is `auto`, meaning it refuses to shrink
   below its own content's min-content size (the exact CSS Grid/Flexbox
   gotcha already documented elsewhere in this file, e.g. .page-main's
   own fix for the same root cause) - without it, a long team name
   pushes the row wider instead of truncating, which is the actual bug
   the user reported ("team name might need truncation"). */
.docs-lookup-name {
  color: var(--text); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.docs-lookup-code {
  background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px;
  font-weight: 700; flex-shrink: 0; margin-left: auto;
}
.docs-lookup-empty { color: var(--text-dim); font-style: italic; }

.updates-list { display: flex; flex-direction: column; gap: 20px; }
.updates-item { display: flex; gap: 14px; border-bottom: 1px solid var(--border); padding-bottom: 20px; }
.updates-item:last-child { border-bottom: none; padding-bottom: 0; }
.updates-item-icon { flex: none; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: var(--bg); color: var(--level-color, var(--accent)); font-size: var(--fs-5); }
.updates-item-body { flex: 1; min-width: 0; }
.updates-item-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; }
.updates-item-label { font-family: var(--font-display); font-weight: 700; }
/* The synthesized "Update {date}" fallback label (NetworkUpdates::
   label(), used when a row has no real Name or V) reads as less
   authoritative than a real one on purpose (2026-09-11, per the user) -
   plain body weight/font, not the display face, and dimmed via opacity
   (not a text-dim color swap) so it stays visually distinct from every
   other .updates-item-date/-desc already on var(--text-dim). */
.updates-item-label.is-fallback { font-family: inherit; font-weight: 400; opacity: .6; }
.updates-item-date { margin-left: auto; color: var(--text-dim); font-size: var(--fs-4); }
.updates-item-desc { color: var(--text-dim); margin: 0; line-height: 1.6; }
.updates-item-desc i { font-style: italic; }

/* Signup's legal-disclaimer checkbox (2026-09-11) - flex-start, not
   .account-checkbox-field's own center alignment, since this label wraps
   to multiple lines (a real sentence, not a short single-line toggle) -
   the checkbox should sit with the first line, not float in the visual
   middle of the whole paragraph. */
/* .auth-field label {display:block} (this exact file, used for every
   other field's own <label>) is a class+element selector - higher
   specificity than .auth-checkbox-field alone, so it silently won the
   display fight, stacking the checkbox above the text instead of
   inline beside it (real bug, caught live 2026-09-11 - "it's floating
   above the text block"). Scoped as .auth-field .auth-checkbox-field to
   beat it outright, same fix shape as this file's other
   equal-or-lower-specificity overrides.
   That same .auth-field label rule ALSO sets font-weight:700/font-size:
   .88rem - properties THIS rule didn't originally declare, so they kept
   right on winning even after the display fix (real bug, still bold
   after a full cache bust - equal-specificity cascade applies per
   PROPERTY, not as a single all-or-nothing block; overriding `display`
   here said nothing about `font-weight`). Explicit normal weight now -
   "make the text block normal text" (was dimmed/shrunk/bold like a
   fine-print disclaimer; the checkbox already signals "this matters" on
   its own) - and font-size matched to .auth-switch's own .92rem (the
   "Already have an account?" line), per the user, rather than the
   1rem/16px body default. */
.auth-field .auth-checkbox-field { display: flex; align-items: flex-start; gap: 8px; cursor: pointer; color: var(--text); font-weight: 400; font-size: var(--fs-4); line-height: 1.5; }
/* .auth-field input's own site-wide `width: 100%` (every text field on
   every auth page) was silently winning here too - equal specificity to
   this rule, and unopposed since this rule never declared its own
   `width` at all, so the checkbox's actual layout box (not its visual
   glyph) stretched to fill the whole row, leaving the text span 0px of
   room (real bug, caught live 2026-09-11 - "it's floating above the
   text block", still broken after the first display:flex fix). Explicit
   width/padding/border/background resets here, not just `flex: none` -
   this element only LOOKS unaffected by those properties because a
   checkbox's native appearance ignores most box styling, not because
   the shared rule doesn't apply to it. */
.auth-checkbox-field input { width: auto; padding: 0; border: none; background: none; margin: 3px 0 0; flex: none; }
.auth-checkbox-field span { flex: 1 1 auto; min-width: 0; }

/* /{level}/export (Mt. Frontend Phase 5, REVISED 2026-09-11 - moved
   from a standalone /exports picker into a real Level Hub page, per the
   user: "this should be integrated into the site levels, not its own
   thing"). No max-width on .exports-page itself any more - it's used
   alongside .detail-page now, which per this file's own established
   ground rule (see the Layout system note elsewhere in this file) never
   gets per-page sizing; the one-click table and the Custom Export card
   both now live inside real .detail-cards (2026-09-11, per the user -
   "needs a container div and 'Data Exports' subheader"), so nothing on
   this page needs its own bespoke width cap any more. */
.exports-status { margin-bottom: 16px; }

.exports-table-wrap { overflow-x: auto; }
.exports-table { width: 100%; border-collapse: collapse; }
/* Header row was falling back to the plain bare-<th> look (bold, no
   explicit weight/color/size override) instead of the site's own
   data-table convention - .game-stat-grid th (ELO/Stats/every other
   table on the site) explicitly resets weight/color/size on every th
   it owns; this table never did (2026-09-11, per the user - "the row
   headers are bold - every other screen on the site is normal text,
   dark grey, smaller... please utilize the style guide we made"). Same
   recipe as .game-stat-grid th, just left-aligned since Name/
   Description are prose, not a number column. */
.exports-table th { text-align: left; padding: 6px 12px; font-size: calc(.9rem - 2px); font-weight: 400; text-transform: uppercase; letter-spacing: .02em; color: color-mix(in srgb, var(--text-dim) 65%, transparent); border-bottom: 1px solid var(--border); white-space: nowrap; }
.exports-table td { padding: 4px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; line-height: 1.3; }
.exports-table tr:last-child td { border-bottom: none; }
.exports-table-name { font-weight: 700; white-space: nowrap; }
.exports-table-desc { color: var(--text-dim); font-size: var(--fs-4); }
.exports-table-records { font-variant-numeric: tabular-nums; color: var(--text-dim); white-space: nowrap; }
.exports-table-download { white-space: nowrap; }
/* Description is the widest, least essential column on a narrow phone
   screen - a real download decision only needs Name/Records/Download
   (2026-09-11, per the user - "hide description column on mobile"). */
@media (max-width: 600px) {
  .exports-table-desc-head, .exports-table-desc { display: none; }
}
/* Team Directory - City/Venue least essential on a narrow phone screen
   (design sweep, 2026-09-11, per the user), same pattern as the
   Exports table's own Description column just above. */
@media (max-width: 600px) {
  .team-directory-city, .team-directory-venue { display: none; }
}
.exports-dl-btn {
  display: inline-block; padding: 3px 9px; margin: 0 3px 0 0;
  border-radius: 6px; font-size: var(--fs-2); font-weight: 700;
  background: var(--bg); border: 1px solid var(--border); color: var(--text);
}
.exports-dl-btn:hover { border-color: var(--level-color); color: var(--level-color); text-decoration: none; }

/* Custom Export card (left) + an empty placeholder card (right) reserved
   for quota copy (2026-09-11, per the user - "build form first, second
   empty for now"). Same 2-up/600px-collapse recipe as .support-sections/
   .account-suggestions-grid elsewhere on the site, not a new pattern. */
.exports-custom-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; align-items: start; }
@media (max-width: 600px) { .exports-custom-grid { grid-template-columns: 1fr; } }
.exports-bonus-banner {
  background: color-mix(in srgb, var(--level-color, var(--accent)) 16%, var(--bg-raised));
  border: 1px solid var(--level-color, var(--accent));
  border-radius: var(--radius);
  padding: 10px 14px;
  font-weight: 700;
  text-align: center;
}
.exports-daily-badge { display: inline-flex; align-items: center; gap: 8px; font-size: var(--fs-4); color: var(--text-dim); }
.exports-daily-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
.exports-daily-badge.is-available .exports-daily-dot { background: #46b478; }
.exports-daily-badge.is-used .exports-daily-dot { background: var(--text-dim); }
.exports-error { margin-bottom: 16px; }

.exports-gauge-wrap { margin-bottom: 20px; }
.exports-gauge-label { display: flex; justify-content: space-between; gap: 10px; font-size: var(--fs-3); color: var(--text-dim); margin-bottom: 6px; }
.exports-gauge { height: 10px; border-radius: 999px; background: var(--border); overflow: hidden; }
.exports-gauge-fill { height: 100%; border-radius: 999px; transition: width .3s ease; }
.exports-gauge.is-ok .exports-gauge-fill { background: #46b478; }
.exports-gauge.is-large .exports-gauge-fill { background: #e6a53c; }
.exports-gauge.is-over .exports-gauge-fill { background: #e2483a; }

.exports-form { margin-top: 8px; }
.exports-season-row { display: flex; gap: 14px; }
.exports-season-row .auth-field { flex: 1; }

/* Quota card (2026-09-11, per the user, settled after a same-day back-
   and-forth) - the thermometer reuses .exports-gauge/.exports-gauge-fill
   as-is (the same component built for a single export's own estimate
   gauge above), thickened slightly (+3px over the base 10px - "can be
   +3px thicker to make it stand out more"). The Big File dot (migration
   072 - removing it read as "endlessly convoluted... its non-clarity
   will make users angry," since a 1GB export is otherwise permanently
   impossible against a 250MB pool) sits beside it, matched to the bar's
   own height/color language rather than reading as an unrelated
   control: hollow = var(--border), the same track color the bar's own
   empty portion uses; filled = #46b478, the exact color .exports-gauge.
   is-ok already fills with. */
.exports-quota-row { display: flex; align-items: flex-end; gap: 14px; }
.exports-quota-gauge-wrap { flex: 1; min-width: 0; margin-bottom: 0; }
.exports-quota-gauge { height: 13px; }
.exports-quota-bigfile-dot { flex: none; width: 13px; height: 13px; margin-bottom: 1px; border-radius: 50%; border: 2px solid var(--border); background: none; }
.exports-quota-bigfile-dot.is-used { background: #46b478; border-color: #46b478; }
.exports-quota-copy { color: var(--text-dim); font-size: var(--fs-4); margin: 14px 0 0; }

/* A real subheader, just smaller than "Quota" above it (2026-09-11,
   round 3, per the user - the eyebrow-label treatment from round 2 read
   as "supertiny... all-caps," not what was asked for). Plain bare <h3>
   sizing (1.1rem, site-wide base rule) does exactly this on its own -
   h2's own clamp tops out at 1.625rem, h3 sits flat at 1.1rem, no
   bespoke override needed beyond spacing. */
.exports-quota-recent-heading { margin-top: 20px; }
.exports-recent-list { list-style: none; margin: 0; padding: 0; }
/* Real CSS Grid, not flex (2026-09-11, round 3, per the user - the
   flex version rendered "all different sizes... stuffed into two
   columns" live, not the clean 3-column row intended). Same recipe as
   .account-addon-row elsewhere on the site: explicit column tracks so
   every row lines up identically, and type/meta both left on the SAME
   inherited font-size (neither sets its own) - differentiated by COLOR
   only (bright/dim), matching .account-addon-title/-expiry's own
   established convention, not by size. */
.exports-recent-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--border); }
.exports-recent-row:last-child { border-bottom: none; }
.exports-recent-type { font-weight: 400; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.exports-recent-meta { color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Level-colored by default, not just on hover (2026-09-11, round 3, per
   the user - "the redownload buttons should be level color is the
   generate button is - it's an action"), matching .btn's own use of
   var(--level-color) to mark a real action, not a plain grey/neutral
   informational icon. */
.exports-recent-again { justify-self: end; display: inline-flex; color: var(--level-color); text-decoration: none; }
.exports-recent-again:hover { opacity: .75; text-decoration: none; }

/* NatStat API Query Builder (2026-09-14, Phase 7; revised same day -
   the header/icon/subtitle are gone, replaced by level-header.php's
   own Level identity block and .detail-card-head, same shell every
   other Level Hub tab uses). Reuses .detail-page/.detail-card/
   .detail-prose/.docs-param-table/.btn (same "one design system, not a
   bespoke one per feature" discipline as the rest of the site). */
.qb-header-tiles { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
/* Plain inline text (2026-09-14, per the user - the old boxed .qb-tile
   pair was blowing out the header row). #qb-remaining/#qb-reset stay as
   their own <span>s only so query-builder.js's periodic refresh can
   update just the numbers, not the surrounding copy. */
.qb-quota-text { color: var(--text-dim); font-size: var(--fs-4); white-space: nowrap; }
.qb-quota-text #qb-remaining, .qb-quota-text #qb-reset { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }

.qb-uri-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px; margin-top: 20px; padding: 14px 16px;
  background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius); font-size: var(--fs-4);
}
.qb-uri-segment { white-space: nowrap; }
.qb-key-regenerate {
  padding: 2px 4px; color: var(--text-dim); font-size: var(--fs-3); background: none; border: none;
  cursor: pointer; opacity: .45; transition: opacity .15s;
}
.qb-key-regenerate:hover, .qb-key-regenerate:focus-visible { color: var(--level-color); opacity: 1; }
.qb-uri-copy { margin-left: auto; flex: none; }
.qb-uri-host { color: var(--text-dim); }
.qb-uri-slash { color: var(--text-dim); opacity: .5; }
.qb-uri-select, .qb-uri-range {
  background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 5px 8px;
  color: var(--text); font-family: var(--font-body); font-size: var(--fs-4);
}
.qb-uri-range { flex: 1; min-width: 160px; }

.qb-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 14px; }
.qb-format-toggle { display: inline-flex; background: var(--bg-raised); border: 1px solid var(--border); border-radius: 999px; padding: 3px; gap: 2px; }
.qb-format-option { background: none; border: none; border-radius: 999px; padding: 6px 14px; font-size: var(--fs-3); font-weight: 700; font-family: var(--font-body); color: var(--text-dim); cursor: pointer; }
.qb-format-option:hover { color: var(--text); }
.qb-format-option.is-active { background: var(--level-color); color: #fff; }
.qb-btn-primary { background: var(--level-color); }
.qb-lookup-wrap { position: relative; display: inline-flex; align-items: center; }
.qb-lookup-input {
  width: 240px; box-sizing: border-box; padding: 7px 32px 7px 12px; border: 1px solid var(--border); border-radius: 999px;
  background: var(--bg); color: var(--text); font-family: var(--font-body); font-size: var(--fs-4); cursor: text;
}
.qb-lookup-input:focus { outline: none; border-color: var(--level-color); }
.qb-lookup-icon { position: absolute; right: 12px; color: var(--text-dim); font-size: var(--fs-3); pointer-events: none; }
#qb-lookup-results { padding: 4px; }
#qb-lookup-results li {
  display: flex; align-items: center; gap: 8px; padding: 6px 4px; border-bottom: 1px solid var(--border);
  font-size: var(--fs-4); cursor: pointer;
}
#qb-lookup-results li:last-child { border-bottom: none; }
#qb-lookup-results li:hover, #qb-lookup-results li.is-active { background: color-mix(in srgb, var(--level-color) 12%, transparent); border-radius: 6px; }
.qb-lookup-result-icon { flex-shrink: 0; width: 20px; text-align: center; color: var(--level-color); font-size: var(--fs-3); }

.qb-body { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 24px; align-items: start; }
.qb-params { margin: 0; }
.qb-params-title { color: color-mix(in srgb, var(--text-dim) 50%, var(--text) 50%); }
.qb-params table { width: 100%; border-collapse: collapse; margin: 8px 0 0; font-size: var(--fs-4); }
.qb-params table tr { border-bottom: 1px solid var(--border); }
.qb-params table tr:last-child { border-bottom: none; }
.qb-params table td, .qb-params table th { padding: 5px 10px; text-align: left; vertical-align: top; }
.qb-params table th { font-size: var(--fs-2); font-weight: 400; text-transform: uppercase; letter-spacing: .02em; color: color-mix(in srgb, var(--text-dim) 65%, transparent); white-space: nowrap; }
.qb-output {
  position: relative;
  background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px;
  max-height: 600px; overflow: auto;
}
.qb-output-copy { position: absolute; top: 10px; right: 10px; background: var(--bg); }
.qb-output-copy[hidden] { display: none; }
.qb-output pre {
  margin: 0; white-space: pre-wrap; word-break: break-word; font-size: var(--fs-3); color: var(--text-dim);
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
}

@media (max-width: 900px) {
  .qb-body { grid-template-columns: 1fr; }
  .qb-header-tiles { width: 100%; }
}

/* /leagues - one-page overview of every League. Accent is var(--level-color),
   which is the site's slate blue on non-League pages. */
.leagues-page { max-width: 1180px; margin: 0 auto; padding: 0 16px 32px; }
.leagues-intro { text-align: center; margin-bottom: 28px; }
.leagues-intro h1 { font-family: var(--font-display); font-size: var(--fs-10); margin: 0 0 6px; }
.leagues-intro p { margin: 0; color: var(--text-dim); }
.leagues-section { margin-bottom: 36px; scroll-margin-top: 90px; }
.leagues-section h2 { display: flex; align-items: center; gap: 10px; font-family: var(--font-display); font-size: var(--fs-8); margin: 0 0 14px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
.leagues-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 14px; }
.leagues-card { display: flex; flex-direction: column; gap: 6px; padding: 14px 16px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-raised); color: var(--text); }
.leagues-card:hover { border-color: var(--chip-color); text-decoration: none; }
.leagues-card-head { display: flex; align-items: center; gap: 10px; }
.leagues-pill { display: inline-block; width: 64px; text-align: center; padding: 2px 0; border-radius: 999px; background: var(--chip-color); color: var(--sidebar-cta-text); font-weight: 700; font-size: var(--fs-4); letter-spacing: .02em; }
/* Season: ComCen's convention (.level-season-inline) - 600 weight, dim grey. */
.leagues-season { font-size: var(--fs-5); font-weight: 600; color: var(--text-dim); font-variant-numeric: tabular-nums; }
/* Games today: solid accent disc with just the count (ComCen's .level-games-count), site accent instead of teal; not rendered at 0. */
.leagues-today-disc { cursor: pointer; margin-left: auto; min-width: 22px; height: 22px; padding: 0 6px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; background: var(--level-color); color: var(--sidebar-cta-text); font-size: var(--fs-2); font-weight: 700; font-variant-numeric: tabular-nums; flex-shrink: 0; }
.leagues-today-disc:hover { filter: brightness(1.15); }
.leagues-name { font-size: var(--fs-5); font-weight: 700; color: var(--text); }
.leagues-range { font-size: var(--fs-5); font-weight: 500; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.leagues-counts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; color: var(--text-dim); font-size: var(--fs-4); }
.leagues-counts strong { color: var(--text-dim); font-weight: 700; font-variant-numeric: tabular-nums; }

/* Vidiprinter: priority toggle and the instasearch on one row. */
.vidi-controls { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 10px; }
.vidi-controls .toggle-group { margin-bottom: 0; }

/* ---- NatStat.tv (2026-09-20): /tv pages, sidebar embed, lightbox ---- */
.tv-page { max-width: 1100px; margin: 0 auto; }
.tv-page h1 { margin: 0 0 12px; }
.tv-head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 14px; }
.tv-head h1 { margin: 0; }
.tv-toggle { margin: 0; }
.tv-toggle .toggle-group-option { display: inline-block; text-decoration: none; transition: background .2s, color .2s, box-shadow .2s; }
.tv-toggle .toggle-group-option.is-off:not(.is-active) { opacity: .55; }
/* .detail-prose caps at 65ch and sits flush left - fine in a prose column, wrong under a full-width player. Centered, and 2px up from the body size (per the user). */
.tv-tagline { max-width: 65ch; margin: 22px auto 0; text-align: center; font-size: calc(1em + 2px); }
.tv-links { max-width: 65ch; margin: 14px auto 0; color: var(--text-dim); font-size: calc(1em + 2px); line-height: 1.6; text-align: center; }

.tv-embed {
  position: relative; aspect-ratio: 16 / 9; width: 100%; overflow: hidden;
  background: #000 center / cover no-repeat; border-radius: var(--radius);
}
.tv-embed .tv-video { position: absolute; inset: 0; width: 100%; height: 100%; background: #000; object-fit: cover; } /* boxes are always 16:9, so the live picture is never cropped; the 3:2 poster just fills the frame */
.tv-play {
  position: absolute; inset: 0; width: 100%; height: 100%; border: 0; padding: 0; cursor: pointer;
  background: transparent; display: flex; align-items: center; justify-content: center;
}
.tv-play[hidden] { display: none; }
.tv-play-icon {
  width: 56px; height: 56px; border-radius: 50%; background: rgba(0, 0, 0, .58); color: #fff;
  display: flex; align-items: center; justify-content: center; font-size: 20px; padding-left: 3px;
  transition: transform .15s, background .15s;
}
.tv-play:hover .tv-play-icon, .tv-play:focus-visible .tv-play-icon { transform: scale(1.08); background: rgba(0, 0, 0, .78); }
.tv-embed-page .tv-play-icon { width: 76px; height: 76px; font-size: 28px; }
.tv-widget-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
.tv-widget-head .sidebar-widget-title { margin: 0; }
.tv-widget-head .sidebar-widget-title a { color: inherit; text-decoration: none; }
.tv-widget-head .sidebar-widget-title a:hover { text-decoration: underline; }
.tv-expand {
  flex: none; width: 28px; height: 28px; border: 1px solid var(--border); border-radius: 6px; cursor: pointer; padding: 0;
  background: transparent; color: var(--text-dim); font-size: 12px; display: flex; align-items: center; justify-content: center;
}
.tv-expand:hover, .tv-expand:focus-visible { color: var(--text); border-color: var(--text-dim); }
.tv-msg, .tv-off-note {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 2; width: min(88%, 460px); padding: 12px 16px;
  background: rgba(0, 0, 0, .72); color: #fff; font-size: var(--fs-4); text-align: center; border-radius: 8px;
}
.tv-embed-off { filter: saturate(.6); }
.tv-blurb { margin-top: 10px; color: var(--text-dim); font-size: var(--fs-4); }
.tv-blurb p { margin: 0 0 6px; }
.tv-blurb-link { font-size: var(--fs-3); }

.tv-lightbox {
  position: fixed; inset: 0; z-index: 200; background: rgba(0, 0, 0, .82);
  display: flex; align-items: center; justify-content: center; padding: 16px;
}
.tv-lightbox-card { width: min(1280px, 100%); }
.tv-lightbox-head { display: flex; align-items: center; justify-content: space-between; color: #fff; padding: 0 2px 8px; }
.tv-lightbox-close {
  border: 0; background: rgba(255, 255, 255, .16); color: #fff; width: 34px; height: 34px; border-radius: 6px; cursor: pointer; font-size: 16px;
}
.tv-lightbox-close:hover, .tv-lightbox-close:focus-visible { background: rgba(255, 255, 255, .3); }
.tv-lightbox-stage { position: relative; aspect-ratio: 16 / 9; max-height: calc(100vh - 90px); margin: 0 auto; background: #000; border-radius: var(--radius); overflow: hidden; }
.tv-lightbox-stage .tv-video { position: absolute; inset: 0; width: 100%; height: 100%; }
body.tv-lightbox-open { overflow: hidden; }
@media (prefers-reduced-motion: reduce) { .tv-play-icon { transition: none; } }

/* Delete Account (2026-09-21) */
.btn-danger { background: #b3261e; border-color: #b3261e; color: #fff; }
.btn-danger:hover { background: #8c1d18; border-color: #8c1d18; }
