/* MagpieStats — shared site styles. Plain CSS, no build step. */

/* Paul, 2026-08-29: "2 different modes a dark mode and a light mode that
   toggle with the browser setting." Every color on this site was already
   funneled through these tokens EXCEPT a handful of hardcoded hex values
   found while doing this (nav-hover tint, the withdrawn-caveat box, the
   S&W "bad" color, badge backgrounds, prose paragraph color, and every
   `color: #fff` paired with an active/filled `background: var(--ink))` --
   see this file's own grep history in todo.md if that list needs
   re-verifying after a future edit. All of those got real tokens here too,
   specifically so dark mode below isn't fighting stray hardcoded colors
   nobody remembered to convert.
   Full docs/design rationale for every one of these: docs/STYLE_GUIDE.md
   -- written to be copy-pasted into another product's own CSS, not just
   an internal comment; keep the two in sync if either changes. */
:root {
  --ink: #16181d;
  /* Text color to put ON a background painted with `--ink` itself (every
     active/filled pill/button: `background: var(--ink); color:
     var(--ink-invert)`). White in light mode, since --ink is dark there --
     NOT simply "white always": dark mode's own --ink is light, so its own
     --ink-invert has to flip to dark, or an "active" pill would render
     light-text-on-light-fill and vanish. */
  --ink-invert: #ffffff;
  --paper: #fafafa;
  --panel: #ffffff;
  --line: #dfe1e6;
  --muted: #6b7078;
  --accent: #b8860b;      /* amber, magpie-adjacent without being literal */
  --accent-ink: #7a5a06;
  --jackdaw: #2a4d8f;      /* blue: raw in-house regression */
  --magpie: #1f7a4d;       /* green: blended composite */
  --warn-bg: #fff6e5;
  --warn-line: #e8c77a;
  /* Stronger than --warn-* -- the Magpie-withdrawn caveat specifically,
     not a routine caution box. */
  --error-bg: #fdecea;
  --error-line: #e6a9a1;
  /* Strengths & Weaknesses panel's negative indicator -- --magpie already
     doubles as its positive one (.sw-good), this is --sw-bad's real
     counterpart, not a fresh "error" color reused for a different job. */
  --bad: #a8461f;
  /* One subtle hover tint, reused everywhere something highlights on
     hover (nav links, table rows) -- was two near-identical hardcoded
     greys before this, consolidated rather than given two tokens that
     would only ever need to move in lockstep. */
  --hover-bg: #f0eee8;
  --badge-jackdaw-bg: #e4ebfa;
  --badge-magpie-bg: #e2f3ea;
  --badge-neutral-bg: #eee;
  /* Prose (how-it-works.html) paragraph text -- deliberately a hair
     softer than --ink for long-form reading comfort, not the same color
     under a different name. */
  --prose-ink: #2b2e35;
  --radius: 8px;
  /* Nav horizontal-scroll edge fade (2026-09-08, mobile QA: "no visual
     cue that more items exist off the right edge of the nav") -- see
     `header.site nav`'s own comment for the technique. A dark shadow
     reads fine here; dark mode needs a light one instead against its
     own dark panel, same "can't just reuse the light value" reasoning
     as --jackdaw/--magpie above. */
  --nav-scroll-shadow: rgba(0, 0, 0, 0.18);
}

/* Same tokens, dark values -- follows the OS/browser's own light/dark
   setting automatically (no manual toggle control anywhere on the site;
   Paul didn't ask for one, and prefers-color-scheme already answers "which
   mode" correctly per-visitor without one). Every token above gets a real
   dark-mode value here, none silently inherit the light one. Neutrals
   (--ink/--paper/--panel/--line/--muted) were deliberately matched to
   draftassist's own already-shipped dark palette (a sibling product, its
   worker/README.md's own scaffold page) rather than invented independently
   -- see docs/STYLE_GUIDE.md's own note on this. Brand colors (--accent/
   --jackdaw/--magpie/--bad) are NOT copied from anywhere -- brightened
   from their light-mode values by hand for real contrast against a dark
   panel, verified by eye against real rendered pages, not computed. */
@media (prefers-color-scheme: dark) {
  :root {
    --ink: #e6eaef;
    --ink-invert: #12151a;
    --paper: #0f1216;
    --panel: #171b21;
    --line: #2a323d;
    --muted: #8b97a6;
    --accent: #d4a017;
    --accent-ink: #e8b93a;
    --jackdaw: #6ea8ff;
    --magpie: #3ecf8e;
    --warn-bg: #3a2f14;
    --warn-line: #6b5220;
    --error-bg: #3a1a17;
    --error-line: #7a3a32;
    --bad: #e8734a;
    --hover-bg: #1e242c;
    --badge-jackdaw-bg: #1c2a42;
    --badge-magpie-bg: #16301f;
    --badge-neutral-bg: #262c34;
    --prose-ink: #d7dbe2;
    --nav-scroll-shadow: rgba(0, 0, 0, 0.5);
  }
}

* { box-sizing: border-box; }

/* Paul, 2026-08-29, with a screenshot: "Some of the icons and buttons are
   black on dark[panel] in [dark mode] and therefore difficult to read."
   Real, pre-existing bug dark mode exposed, not something the theme work
   introduced: browsers don't let a <button> inherit `color` from its
   ancestors by default (a real UA-stylesheet default, not this file's own
   doing) -- nothing here ever reset it globally, only patched around it
   piecemeal in a couple of spots that happened to need `font: inherit`
   too. In light mode the browser's own default button color is
   near-black, which happened to match --ink there by coincidence, so this
   was invisible until --ink flipped light for dark mode and the
   never-reset buttons stayed black regardless -- every .icon-btn's SVG
   (stroke="currentColor") and every plain button.secondary's own text
   (confirmed live: getComputedStyle returned rgb(0,0,0) on both, verified
   BEFORE this fix, not assumed). `.active`/`.primary` buttons were never
   affected -- they already set their own explicit `color`, which already
   wins over this on specificity, so this can't override anything real by
   accident. */
button { color: inherit; }

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  background: var(--paper);
  color: var(--ink);
  line-height: 1.5;
}

a { color: var(--accent-ink); }

/* Sticky, full viewport width -- Paul, 2026-08-20: "the header is wrong...
   just put things back how they were." Briefly changed the same day to align
   .header-inner to main's 1180px column (logo/nav pinned to where the page
   content ends rather than the browser edge); Paul reverted that call, so
   this is back to logo pinned to the left edge, nav pinned to the right edge,
   full width, no max-width/margin:auto on .header-inner. */
/* --header-height is the single source of truth for the sticky header's
   rendered height -- table.rankings th's sticky offset reads the same
   variable below, so the two can't drift out of sync the way two independent
   pixel guesses did (a real bug: the table's first body row rendered above
   the sticky header because the guessed offset didn't match the header's
   actual height). Fixed height + vertical centering makes it deterministic
   instead of intrinsic/content-based, so this number is exact, not a guess. */
:root { --header-height: 64px; }
header.site {
  position: sticky;
  top: 0;
  z-index: 100;
  height: var(--header-height);
  padding: 0 24px;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
  box-sizing: border-box;
}
.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* nowrap, not wrap -- Paul, 2026-08-20: "header rows are still jacked up."
     header.site has a hard `height: var(--header-height)`, fixed on purpose
     so the sticky table header's offset below can trust it exactly (see that
     comment). flex-wrap:wrap let a narrower viewport push nav onto a second
     line, which the fixed-height box doesn't grow to fit -- the overflow
     line rendered outside the header, on top of whatever came right after it
     (the sticky table header, when scrolled to the top). nowrap + the
     min-width:0/shrink rules below keep the header genuinely one line at any
     width, so --header-height stays true instead of an assumption that broke
     under real content. */
  flex-wrap: nowrap;
  gap: 10px;
  height: 100%;
  min-width: 0;
}

header.site .brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-weight: 700;
  font-size: 1.15rem;
  letter-spacing: -0.01em;
  text-decoration: none;
  color: var(--ink);
  flex-shrink: 0;
}
header.site .brand-logo { width: 26px; height: 26px; display: block; }
/* Paul, 2026-08-24: several rounds on this. First "3px above the bottom of
   the image" (attempted via flex-end + a DOM-box-measured margin, but that
   measured the text's LINE BOX, not its visible ink; line-height pads
   several extra px below the glyphs, so what verified as "3px" on-screen
   was actually 7px and looked wrong -- Paul: "the after is worse," and was
   right). Then simplified directly: "move the text down 10 pixels" (plain
   position:relative; top:10px, verified as an exact 10px delta). Then
   "move it 10px back up" (net zero from centered), then "move down 5
   pixels" -- current value is that last instruction taken at face value
   from the centered baseline, not a running tally of every prior number
   in this thread. position:relative; top, not margin, throughout -- never
   touches .brand's own flex height, so no risk to header.site's fixed
   64px --header-height or the sticky-header offset that depends on it
   regardless of how this number moves next. */
header.site .brand-text { position: relative; top: 5px; }

/* nowrap here too (same fixed-height reasoning as .header-inner above) --
   this is the level that was actually wrapping. overflow-x:auto is the
   narrow-viewport fallback: nav scrolls horizontally within its own row
   instead of wrapping to a second one and breaking --header-height. */
/* Paul's mobile QA pass, 2026-08-29 (self-initiated sweep, logged in
   todo.md): "no visual cue that more items exist off the right edge of
   the nav" -- confirmed real (scrollWidth > clientWidth at 375px), NHL/
   How it works reachable by sideways swipe with zero hint they existed.
   Classic 4-layer CSS "scroll shadow" trick, no JS: two solid `--panel`-
   colored "cover" gradients that scroll WITH the content
   (background-attachment: local) sit on top of two translucent shadow
   gradients that stay fixed to the viewport (background-attachment:
   scroll). At either scroll extreme, the moving cover slides fully over
   its own shadow and hides it -- so a fade only ever actually shows on
   whichever side there's real more content to reveal, correctly
   disappearing on a wide viewport where nav never overflows at all. */
header.site nav {
  display: flex; gap: 4px; flex-wrap: nowrap; overflow-x: auto; min-width: 0;
  background-image:
    linear-gradient(to right, var(--panel) 30%, transparent),
    linear-gradient(to left, var(--panel) 30%, transparent),
    linear-gradient(to right, var(--nav-scroll-shadow), transparent),
    linear-gradient(to left, var(--nav-scroll-shadow), transparent);
  background-repeat: no-repeat;
  background-color: var(--panel);
  background-position: left center, right center, left center, right center;
  background-size: 24px 100%, 24px 100%, 10px 100%, 10px 100%;
  background-attachment: local, local, scroll, scroll;
}

header.site nav a {
  padding: 6px 12px;
  border-radius: 999px;
  text-decoration: none;
  color: var(--ink);
  font-size: 0.92rem;
  flex-shrink: 0;
}

header.site nav a.active,
header.site nav a:hover { background: var(--hover-bg); }

/* Paul, 2026-08-25: originally wrapped nav+#season-picker together as one
   flex child of .header-inner. 2026-08-28: moved out entirely -- see
   .page-title-row below, the current real home for #season-picker; this
   rule now just wraps nav alone, kept as its own class rather than inlined
   onto .header-inner in case something else joins this row later. */
.header-right { display: flex; align-items: center; gap: 10px; flex-wrap: nowrap; min-width: 0; }
.season-picker {
  flex-shrink: 0;
  font-size: 0.85rem;
  padding: 5px 8px;
  border-radius: 6px;
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--ink);
}
/* Paul, 2026-08-25: "NBA I think has 1 year we should try that but caveat
   that it's still learning" -- a sport with zero real historical seasons
   (nothing in cfg.historicalSeasons yet) hides the picker entirely rather
   than showing a single-option dropdown with nothing to actually pick.
   renderSeasonPicker() in app.js toggles this. */
.season-picker[hidden] { display: none; }

main { max-width: 1180px; margin: 0 auto; padding: 24px; }

.hero { padding: 40px 0 24px; }
.hero h1 { font-size: 2.1rem; margin: 0 0 8px; }
.hero p.lede { color: var(--muted); font-size: 1.08rem; max-width: 640px; }

.sport-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
  margin-top: 24px;
}

.sport-card {
  display: block;
  padding: 20px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  text-decoration: none;
  color: var(--ink);
}
.sport-card:hover { border-color: var(--accent); }
.sport-card .sport-name { font-weight: 700; font-size: 1.1rem; }
.sport-card .sport-status { color: var(--muted); font-size: 0.88rem; margin-top: 6px; }

.caveat {
  background: var(--warn-bg);
  border: 1px solid var(--warn-line);
  border-radius: var(--radius);
  padding: 12px 16px;
  font-size: 0.92rem;
  margin-bottom: 20px;
}

.caveat.withdrawn {
  background: var(--error-bg);
  border-color: var(--error-line);
  font-weight: 600;
}

.tabs button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.accuracy-table tr.withdrawn { opacity: 0.55; text-decoration: line-through; }
.accuracy-table tr.withdrawn td.metric { text-decoration: none; }

/* Paul, 2026-08-27: "move the season selector to the top right of the
   'How accurate is each system?' section." One shared control for the
   whole section (js/app.js's own renderAccuracyFor() renders it here now,
   not per-stat-block) -- a flex row on the heading itself, with the
   season picker pushed to the far right via margin-left:auto. Empty
   (no width) on a tab/mode with no real per-season data at all, same as
   before this moved -- nothing to show, nothing reserved. */
.accuracy-heading { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 10px; }
.accuracy-season-picker { margin-left: auto; }
/* Paul, 2026-08-28: "The NHL accuracy year selector should have a grey box
   same as everything else" -- real gap, not NHL-specific: this select never
   had #season-picker's own box styling (border/padding/background) at all,
   just a font-size override, so it rendered as a bare native <select> on
   every sport, not just NHL. Same look as #season-picker now. */
.accuracy-season-picker select {
  font-size: 0.85rem;
  padding: 5px 8px;
  border-radius: 6px;
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--ink);
}

/* Paul, 2026-08-28: "add an option for these to show a non PPR version" --
   NFL-only (RB/WR/TE), a second small toggle beside the season picker.
   `.accuracy-controls` takes over the season picker's own margin-left:auto
   role (so the two controls cluster together at the heading's right edge
   instead of each independently trying to push right) -- the more specific
   nested override below cancels the season picker's own rule rather than
   changing it site-wide, since MLB/NBA/NHL's own season pickers still sit
   directly in .accuracy-heading with no wrapper at all. Empty (no width)
   on any position without a real non-PPR row, same "nothing to show,
   nothing reserved" convention the season picker already uses. */
.accuracy-controls { margin-left: auto; display: flex; align-items: center; gap: 10px; }
.accuracy-controls .accuracy-season-picker { margin-left: 0; }
.accuracy-scoring-picker { display: flex; gap: 6px; }
.accuracy-scoring-picker button {
  padding: 4px 10px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--panel); cursor: pointer; font-size: 0.78rem;
}
.accuracy-scoring-picker button.active { background: var(--ink); color: var(--ink-invert); border-color: var(--ink); }

.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 18px;
  margin-bottom: 20px;
}

.panel h2 { margin-top: 0; font-size: 1.15rem; }
.panel h3 { font-size: 1rem; margin-bottom: 8px; }

.tabs { display: flex; gap: 6px; margin-bottom: 16px; flex-wrap: wrap; }
.tabs button {
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--panel);
  cursor: pointer;
  font-size: 0.9rem;
}
.tabs button.active { background: var(--ink); color: var(--ink-invert); border-color: var(--ink); }

.row-flex { display: flex; gap: 24px; flex-wrap: wrap; align-items: flex-start; }
.row-flex > * { flex: 1 1 280px; }

.scoring-form label {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  font-size: 0.88rem;
  padding: 3px 0;
}
.scoring-form input[type="number"] {
  width: 80px;
  padding: 4px 6px;
  border: 1px solid var(--line);
  border-radius: 4px;
}
/* Paul, 2026-08-27: "Remove the up/down arrows from the scoring section."
   Chrome/Safari/Edge (WebKit/Blink) render a number input's spinner as a
   pseudo-element, not a normal box -- appearance:none alone (the standard
   property, Firefox's own rule below) doesn't touch it; needs its own
   -webkit-prefixed selector. Firefox has no such pseudo-element -- its
   spinner is controlled by appearance itself, `textfield` being the value
   that specifically means "no spinner" (plain `none` still shows one in
   Firefox, a real, easy-to-miss cross-browser gap). Scoped to the scoring
   form specifically, not every number input site-wide (roster-size boxes
   etc keep their spinners -- not what was asked). */
.scoring-form input[type="number"] { -moz-appearance: textfield; }
.scoring-form input[type="number"]::-webkit-outer-spin-button,
.scoring-form input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.scoring-form fieldset { border: 1px solid var(--line); border-radius: 6px; margin-bottom: 10px; padding: 8px 12px; }
.scoring-form legend { font-size: 0.82rem; font-weight: 600; color: var(--muted); padding: 0 4px; }
/* Paul, 2026-08-19: "the scoring sections into 3 columns... in their half" --
   only engages when the form actually has grouped fieldsets (NFL's Offense/
   Kicker/Defense today); a flat label list (every other sport right now) is
   untouched, since there's no group structure there to lay out side by side.
   :has() has been reliably supported for a couple of years; a browser without
   it just keeps the fieldsets stacked, same as before this existed -- not
   broken, just not 3-up.
   Column count was hardcoded to 3 (NFL's own group count) until 2026-08-28
   (Paul: "Split the MLB scoring for points to be batting and pitching
   columns" -- 2 groups, not 3). auto-fit sizes to however many fieldsets a
   sport actually defines instead of assuming NFL's number everywhere. */
.scoring-form:has(fieldset) { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0 14px; align-items: start; }
.scoring-form:has(fieldset) fieldset { margin-bottom: 0; }

/* Roster boxes: one number input per position, same visual language as the
   scoring form's labeled inputs rather than a new pattern. A column, not a
   wrapping row -- Paul, 2026-08-19: "order all the other roster boxes into a
   column," the roster half's counterpart to the scoring half going 3-up. */
.roster-presets { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.roster-presets button.active { background: var(--ink); color: var(--ink-invert); border-color: var(--ink); }
/* Paul, 2026-08-27: "move the number field to be at the right of the
   table so the superflex text doesn't wrap." Was a flex column of labels,
   each one individually flexed with justify-content:space-between inside
   a 220px-wide box -- fine for short labels ("QB", "FLEX (RB/WR/TE)"),
   but "Superflex (QB/RB/WR/TE)" has nowhere to go at that width except
   wrap, since space-between squeezes the label text against the input
   inside the SAME row rather than giving it a real column of its own.
   Grid instead: two real columns (label text, input), shared across every
   row -- `label { display: contents }` removes the label's own box
   without removing its children from the grid, so the text and its
   input become direct grid items in column 1/2 respectively. The label
   column is free to wrap onto a second line at whatever width it has
   without ever pushing the input out of its own fixed-width column on
   the right. */
.roster-boxes { display: grid; grid-template-columns: 1fr auto; align-items: center; column-gap: 8px; row-gap: 8px; margin-bottom: 12px; }
.roster-boxes label { display: contents; font-size: 0.88rem; }
.roster-boxes input[type="number"] { padding: 4px 6px; border: 1px solid var(--line); border-radius: 4px; }

/* Paul, 2026-08-27: "put Auction budget $ on a new line." NFL already had
   this (a leftover inline style on just that one page); moved here so
   every sport's Roster sidebar stacks "Teams in your league" above
   "Auction budget $" instead of only NFL's -- MLB/NBA/NHL had no such
   rule at all, so those two labels could render side by side whenever
   the sidebar was wide enough to fit both inline. */
#vor-note { display: flex; flex-direction: column; gap: 6px; }
/* Paul, 2026-08-27: "Right align the text boxes in the league section."
   Each label was a plain inline element -- text and its input just
   flowed together with normal word spacing, not aligned to any shared
   edge. space-between pushes the input to the row's own right edge
   (same visual result .roster-boxes' grid gives its own inputs, simpler
   here since neither label is long enough to risk wrapping the way
   "Superflex (QB/RB/WR/TE)" was). */
#vor-note label { display: flex; justify-content: space-between; align-items: center; gap: 6px; }
/* Paul, 2026-08-28: "format the league text boxes like the others (grey
   rounded corners)." These two inputs (Teams in your league / Auction
   budget $) had no border/radius rule of their own -- just a bare
   width:60px inline style, so they rendered with the browser's own
   default number-input chrome instead of matching .scoring-form/
   .roster-boxes' shared look. Same padding/border/radius as both of
   those, spinners left alone (not what was asked, same call
   .roster-boxes' own comment already made). */
#vor-note input[type="number"] { padding: 4px 6px; border: 1px solid var(--line); border-radius: 4px; }

/* Paul, 2026-08-27: "Reduce the defaults, custom, league, risk headings
   by 2 points." Plain <h4>s here inherited the browser default (16px) --
   no shared rule existed to reduce in the first place. 14px, applied via
   a real class rather than four more inline styles. */
.subsection-heading { font-size: 14px; }

/* Rankings panel's top-right quick links (jump to scoring/roster + CSV) --
   sits beside the h2 rather than stacked under it. */
.panel-header-row { display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: 8px; }
.panel-header-row .panel-links { display: flex; align-items: center; gap: 14px; font-size: 0.9rem; }
/* Paul, 2026-08-28: "I don't want the year picker to be in the header in
   the corner I want it next to the rankings header" -- #season-picker
   briefly lived in the Rankings panel-header-row (a .panel-title-row
   wrapper wrapped the h2+select there); superseded same-day: "Actually
   put the season picker next to the nfl projections header" -- moved
   again, to sit beside the page's own <h1>...Projections</h1> instead.
   See .page-title-row below, the current real home for this control. */
/* Paul, 2026-08-28: "The selector should be moved upwards so its in line
   with the header." align-items:baseline matched text baselines exactly,
   but the h1's own font size dwarfs the select's, so its real text
   baseline sits well below where a reader's eye expects a same-row
   control to line up -- same class of "technically aligned, doesn't read
   as level" issue the System panel's description text hit earlier this
   session. center reads level regardless of the size mismatch. */
.page-title-row { display: flex; align-items: center; gap: 10px; }
/* Paul, 2026-08-29, with a screenshot: "Half the size of this gap" --
   #last-updated had no rule of its own, so the space above it was just
   the browser's default <p> margin-top (measured live: 13.6px). First
   attempt (just `#last-updated { margin-top: 7px }`) had ZERO visible
   effect -- found why by measuring live, not by staring at the CSS:
   #data-status sits between .page-title-row and #last-updated, and once
   loaded it's emptied to '' by app.js (its "Loading projections…" text
   was only ever transient) but the <p> itself, and its own default
   margin-top/margin-bottom, are still there. An empty block element's
   own top+bottom margins collapse into ONE, and that collapses again with
   the NEXT element's margin-top by taking whichever is LARGER -- #data-
   status's collapsed 13.6px beat #last-updated's new 7px every time,
   so the gap never moved. `:empty` (real CSS, not a hack) zeroes this
   specifically once #data-status has nothing left in it -- its own
   default margin while genuinely showing "Loading projections…" is
   untouched, since that state was never the one being asked about. */
#data-status:empty { margin: 0; }
#last-updated { margin-top: 7px; }

/* CSV export: a standard download glyph + a rollover label, not button text --
   Paul, 2026-08-19. aria-label carries the accessible name since the visible
   text is gone; data-tooltip is the sighted hover affordance, CSS-only (no JS,
   no native `title` — title's delay/inconsistent styling doesn't read as
   "designed"). */
.icon-btn { position: relative; display: inline-flex; align-items: center; justify-content: center; padding: 6px 10px; line-height: 0; }
.icon-btn svg { display: block; }
/* Paul, 2026-08-28: "the mousover for both the link and the csv download
   look bad" -- real bug, not a style nit: these buttons live in the
   Rankings panel's own header row, which is often scrolled right to the
   top of the viewport -- an upward-popping tooltip (this rule's old
   `bottom: 100%`) had nowhere to go there and rendered clipped by the
   browser's own viewport edge, not a CSS overflow this repo controls.
   Below the button instead -- the rankings table underneath always has
   real room, unlike above. */
.icon-btn[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute; top: 100%; left: 50%; transform: translateX(-50%) translateY(6px);
  background: var(--ink); color: var(--ink-invert); font-size: 0.75rem; padding: 4px 8px; border-radius: 4px;
  white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.12s ease;
  z-index: 5;
  /* Paul, 2026-08-27: "the black background isnt big enough to show some
     of the text" after the position fix above -- real bug: .icon-btn sets
     `line-height: 0` (to vertically center its own SVG icon), and this
     pseudo-element inherits that, collapsing its own line box to 0px so
     the pill's background height came only from the 4px/4px padding while
     the actual 12px text still needed real line-height space to render
     without overflowing top/bottom. Reset it explicitly instead of
     inheriting the icon's own line-height. */
  line-height: 1.4;
}
.icon-btn[data-tooltip]:hover::after,
.icon-btn[data-tooltip]:focus-visible::after { opacity: 1; }

/* Compact top-of-page Jackdaw/Magpie toggle, alongside the fuller one further
   down in "Create my rankings" -- Paul, 2026-08-19 wants both, not one moved
   to replace the other. */
.system-toggle-panel { padding: 14px 20px; }
.system-toggle-panel .row-flex { align-items: center; }
.system-toggle-panel p { margin: 0 0 4px; }
/* Paul, 2026-08-27: "Put the magpie/jackdaw options on the left hand side
   and the description on the right ... Make the magpie/jackdaw buttons
   vertically centred in the section." Revises the same-day column-stack
   version above this comment (a real "move the description to its own
   line" ask turned out to mean a `<br>` inside the paragraph itself --
   see each sports/*.html's own system-toggle-row markup -- not the whole
   row stacking vertically).
   `align-items: center` (2026-08-27) looked right for NFL's own two-line
   description, but Paul's 2026-08-28 follow-up ("align the description
   text so it's in line with the buttons," with a screenshot) caught it
   breaking on MLB's real, longer three-line description -- centering a
   taller block against the short button row sinks the buttons down to
   roughly the SECOND line's height instead of the first, reading as
   misaligned even though it's centered exactly as asked. flex-start
   lines the button row's own top up with the description's first line
   instead, correct regardless of how many lines the real text wraps to. */
.system-toggle-row { display: flex; align-items: flex-start; flex-wrap: wrap; gap: 14px; }
.system-toggle-row p { flex: 1 1 320px; margin: 0; }

/* Info-icon toggle (2026-08-20): a small (i) beside a heading that reveals a
   methodology/framing paragraph on click, instead of it always taking up
   space. Same pattern for Sleepers & Busts and Strengths & Weaknesses. */
.info-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; margin-left: 4px; padding: 0;
  border: 1px solid var(--line); border-radius: 999px; background: var(--panel);
  color: var(--muted); font-size: 0.75rem; line-height: 1; cursor: pointer; vertical-align: middle;
}
.info-icon:hover, .info-icon.active { color: var(--ink); border-color: var(--ink); }
.info-content { margin-top: 6px; }
.detail-toggle summary { cursor: pointer; font-size: 0.85rem; color: var(--ink); font-weight: 600; }
.detail-toggle summary:hover { text-decoration: underline; }
.detail-toggle[open] summary { margin-bottom: 4px; }
.detail-toggle p { margin: 0; }

/* Strengths & Weaknesses (2026-08-20). Text-first, not a chart -- a checkmark/
   cross reads at a glance without inventing a status-color scheme for what's
   fundamentally a short list. */
.sw-section { margin-bottom: 18px; }
.sw-section:last-child { margin-bottom: 0; }
.sw-section h3 { margin-bottom: 4px; }
.sw-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 8px; }
.sw-grid > div { min-width: 0; }
.sw-grid h4 { font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.02em; margin-bottom: 6px; }
.sw-grid h4.sw-good { color: var(--magpie); }
.sw-grid h4.sw-bad { color: var(--bad); }
.sw-grid ul { margin: 0; padding-left: 18px; font-size: 0.9rem; }
.sw-grid li { margin-bottom: 4px; }
.sw-grid .metric { font-variant-numeric: tabular-nums; color: var(--muted); }
@media (max-width: 560px) { .sw-grid { grid-template-columns: 1fr; } }

/* Paul, 2026-08-24: "make the choice a slider with categories as the default
   on the left" -- was an equal-weight button pair (no default reading order
   implied), now a single pill track with a sliding thumb behind whichever
   label is active. Markup order is categories-first, points-second in every
   sports/<x>.html that has this (MLB/NBA/NHL; NFL has no categories mode and
   never had this element) so "left = categories" is a markup fact, not just
   a CSS one. data-mode on the wrapper (kept in sync by renderModeVisibility())
   drives the thumb position -- the translateX(100%) math below is exact for
   any width: the thumb is calc(50% - 3px) wide (half the padded content box),
   and shifting by its own width lands it exactly on the second slot. */
/* Paul, 2026-08-28: "Move the floor ceiling slider next to the points/
   categories slider. Give them both an i with a description of what
   they're for." Both pills keep their own margin-bottom (used when they
   render standalone elsewhere, e.g. NFL's second risk-slider copy in the
   Rankings panel) -- align-items:center here just keeps the row's own
   (i) icons vertically centered against pills of two different heights/
   widths, not fighting those margins. */
.mode-risk-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
.mode-risk-row .mode-switch, .mode-risk-row .risk-slider { margin-bottom: 0; }
/* Paul, 2026-08-28: "Expand the risk slider so that the text renders
   properly" -- real bug, not a display nit: the row's own inline
   max-width:320px on each control is a CEILING, not a floor, and neither
   control had flex-shrink disabled -- on a narrow enough row (both
   controls competing for space via flex-wrap's own shrink-before-wrap
   default), the risk slider's own three equal-width segments (33.333%
   each, .risk-slider-thumb's own hardcoded math) could compress past the
   point "Projected" (the longest of its 3 labels) still fits on one line,
   clipping/wrapping it. min-width, not a bigger max-width alone, is the
   real fix: it gives flex-wrap a real floor to wrap the WHOLE row onto
   two lines against instead of silently squishing either control below
   a legible size. */
.mode-risk-row .risk-slider { min-width: 280px; }
.mode-risk-row .mode-switch { min-width: 200px; }
.mode-switch { position: relative; display: flex; margin-bottom: 14px; border: 1px solid var(--line); border-radius: 999px; background: var(--panel); padding: 3px; }
.mode-switch-thumb {
  position: absolute;
  top: 3px; bottom: 3px; left: 3px;
  width: calc(50% - 3px);
  border-radius: 999px;
  background: var(--accent);
  transition: transform 0.18s ease;
  pointer-events: none;
}
.mode-switch[data-mode="points"] .mode-switch-thumb { transform: translateX(100%); }
.mode-switch button {
  position: relative;
  z-index: 1;
  flex: 1;
  padding: 8px;
  border: none;
  background: transparent;
  cursor: pointer;
  border-radius: 999px;
  font: inherit;
  /* Paul, 2026-08-24: "change the categories/points slider text to be the
     same size as the magpie/jackdaw text above" -- font:inherit above
     only carries family/weight down from .mode-switch, which never set a
     size itself, so it fell through to the browser default (16px), a full
     1.6px bigger than the Magpie/Jackdaw .tabs button text (0.9rem =
     14.4px) sitting right above it. Matched directly to that same value,
     not a separate guess. */
  font-size: 0.9rem;
  color: var(--ink);
}
.mode-switch button.active { color: var(--ink-invert); }

/* Risk slider (todo.md, 2026-08-25): same pill-track/thumb visual language
   as .mode-switch above, but a real 3-way variant (Floor/Projected/Ceiling)
   -- kept as its own class rather than overloading .mode-switch, whose
   width:50%/translateX(100%) math is hardcoded to exactly two stops with no
   clean way to add a third. */
.risk-slider { position: relative; display: flex; margin-bottom: 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--panel); padding: 3px; }
.risk-slider-thumb {
  position: absolute;
  top: 3px; bottom: 3px; left: 3px;
  width: calc(33.333% - 3px);
  border-radius: 999px;
  background: var(--accent);
  transition: transform 0.18s ease;
  pointer-events: none;
}
.risk-slider[data-risk="mid"] .risk-slider-thumb { transform: translateX(100%); }
.risk-slider[data-risk="ceiling"] .risk-slider-thumb { transform: translateX(200%); }
.risk-slider button {
  position: relative;
  z-index: 1;
  flex: 1;
  padding: 8px;
  border: none;
  background: transparent;
  cursor: pointer;
  border-radius: 999px;
  font: inherit;
  font-size: 0.9rem;
  color: var(--ink);
}
.risk-slider button.active { color: var(--ink-invert); }

.cat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 4px 10px; }
.cat-grid label { display: flex; gap: 6px; align-items: center; font-size: 0.86rem; }

/* Paul, 2026-08-28: "break the list down into pitchers and hitters" --
   #categories-form is normally itself a .cat-grid (a flat checkbox grid);
   when cfg.categoryGroups is in play (js/app.js toggles this class) it
   instead holds one block per group (each with its own inner .cat-grid),
   so the outer container needs a plain row layout, not a second grid
   fighting the inner ones. #id + class beats the plain .cat-grid rule's
   specificity regardless of source order. */
#categories-form.cat-grid-groups { display: flex; gap: 24px; flex-wrap: wrap; }
#categories-form.cat-grid-groups > div { flex: 1 1 200px; }

button.primary {
  background: var(--ink);
  color: var(--ink-invert);
  border: none;
  padding: 10px 18px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.95rem;
}
button.primary:hover { opacity: 0.9; }
button.secondary {
  background: var(--panel);
  border: 1px solid var(--line);
  padding: 10px 18px;
  border-radius: 6px;
  cursor: pointer;
}

/* Paul, 2026-08-29: "Shrink the columns as much as you can" -- a real
   width-reduction request, not a token gesture, made alongside pulling
   back the sticky-column scope below: the table's own width is now the
   main lever left for reducing how often horizontal scroll (and the
   vertical sticky-header tradeoff that comes with it, see .is-scrolling's
   own comment) triggers at all, now that showing every weighted stat
   column can make the table genuinely wide. Was 0.88rem/7px 10px --
   tightened as far as felt legible, not further; this alone won't
   eliminate horizontal scroll on every screen/config, just reduces how
   often it's needed. */
table.rankings { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
table.rankings tr.show-more-row td { text-align: left; padding: 12px 10px; border-bottom: none; }
table.rankings tr.show-more-row button { margin: 0 4px; }
/* Paul, 2026-08-28: "In the sleepers and busts section add a buffer before
   the show more buttons the same as on the rankings table. Currently it
   overlaps the last gridline." The main rankings table gets its buffer for
   free -- show-more-row's own td padding-top (12px, above) -- because that
   button lives inside a <tr> after the last data row. Sleepers/Busts'
   buttons are plain siblings straight after </table>, not table rows, so
   they had no equivalent gap and sat flush against the last row's own
   border-bottom. Same 12px, via margin instead of td padding since these
   aren't inside a table. */
#sleepers-busts button[data-sb-showall], #sleepers-busts button[data-sb-showless] { margin-top: 12px; }
table.rankings th, table.rankings td { padding: 5px 6px; text-align: right; border-bottom: 1px solid var(--line); }
/* Mobile QA (todo.md, 2026-08-29): the vertical sticky header can't
   survive `.is-scrolling` at all (see that class's own comment -- a
   real CSS Overflow 3 constraint, confirmed in a real browser, not a
   missed opportunity), and mobile hits `.is-scrolling` almost
   immediately given how many stat columns most sports carry. A true
   fix would mean a JS-synced header decoupled from position:sticky
   entirely -- assessed once already as real, standalone work, not
   something to build as a side effect of a CSS pass. This is the
   accepted, scoped-down mobile compromise instead: once the header IS
   going to scroll away with the rest of the table (not stay pinned),
   at least fit more real rows in the same screen before it does, by
   shrinking row height specifically at phone widths. Doesn't remove
   the underlying limitation -- a long scroll still eventually loses
   the header -- just delays it. 560px matches this file's own existing
   phone breakpoint (.sw-grid above). */
@media (max-width: 560px) {
  table.rankings { font-size: 0.72rem; }
  table.rankings th, table.rankings td { padding: 3px 4px; }
}
table.rankings th:first-child, table.rankings td:first-child,
table.rankings th:nth-child(2), table.rankings td:nth-child(2),
table.rankings th:nth-child(3), table.rankings td:nth-child(3) { text-align: left; }
/* Fixed, not auto -- the rank column's own width used to float with its
   content (1-3 digit numbers). 40px is the real measured auto width (see
   js/app.js's updateTableScrollState() comment) -- pinning it here just
   makes that width a guarantee instead of a coincidence, so it doesn't
   visibly jump as a sort/filter changes which rank numbers are showing.
   (No longer load-bearing for the sticky-column offset below -- column 1
   itself isn't pinned anymore, only column 2 is, at `left: 0` directly --
   but keeping the width stable is still worth it on its own.)
   Real bug, caught live (Paul: "The NFL accuracy columns are jacked up"):
   `.rankings` is also the shared base class for the accuracy-comparison
   table (js/app.js's renderAccuracyFor(), <table class="rankings
   accuracy-table">) and the Sleepers/Busts table (renderSleepersBusts()) --
   NEITHER has a short rank number in column 1 (System name / Player name,
   both real free text), so an unscoped `table.rankings td:first-child`
   here crushed both down to 40px too, on top of accuracy-table's own
   deliberate `width: 30%` rule (higher specificity here always won --
   this selector has one more type selector, `table`, at equal class
   count). Scoped to `.rankings-scroll` (the wrapper ONLY the main
   player-rankings table is ever inside -- see the grep of every
   `class="rankings` call site before assuming otherwise if a new one
   shows up later) so this can't leak onto a table it wasn't written for
   again. */
.rankings-scroll table.rankings th:first-child, .rankings-scroll table.rankings td:first-child { width: 40px; }
/* Paul, 2026-08-29: "For magpie on MLB the player names word wrap onto 2
   lines by default for some reason, stop that from happening." Real bug,
   not MLB-specific in cause even though MLB's own column count/widths are
   what exposed it: the Player-name cell (column 2) had no `white-space`
   rule of its own at all -- only `table.rankings th` (headers) got
   `nowrap` -- so whenever this table's other columns claimed enough width
   to squeeze it below a name's natural width (confirmed live: MLB's name
   column measured 86px, "Bobby Witt Jr." wrapped to 2 lines, 49px tall vs
   a real single-line ~24px), the browser's default table layout wrapped
   the name instead of just letting the table (already horizontally
   scrollable via .rankings-scroll) run wider. Scoped to `.rankings-scroll`
   same as the width:40px rule just above, for the same reason (this class
   is also reused by the accuracy-comparison and Sleepers/Busts tables,
   neither of which has a name in column 2). */
.rankings-scroll table.rankings td:nth-child(2) { white-space: nowrap; }
/* Sticky within the page's own scroll (not a nested scroll container) --
   Paul, 2026-08-20, and a real bug from the first attempt: the offset was a
   separate hardcoded guess (64px) that didn't actually match the site
   header's rendered height, so a scrolled-past row could render above the
   "stuck" table header instead of under it. Now reads --header-height, the
   same variable the header's own fixed height uses, so the two literally
   cannot disagree.
   The OTHER half of making this work is .rankings-scroll below -- "not a
   nested scroll container" is a hard requirement of this rule, not a
   description of it. Read that comment before touching either. */
table.rankings th { position: sticky; top: var(--header-height); cursor: pointer; user-select: none; color: var(--muted); font-weight: 600; white-space: nowrap; background: var(--panel); z-index: 10; }

/* Horizontal-overflow wrapper for table.rankings. Was an inline
   `style="overflow-x:auto"` on the div in all four sport pages; it is a class
   now so there is one place to get this right instead of four to get it wrong.
   NEVER give this element a non-`visible` overflow unconditionally.
   Paul, 2026-08-23: "the top ranked player is above the header row in the
   rankings table again." Cause, verified in Chrome rather than reasoned about:
   per CSS Overflow 3, when one axis computes to `visible` and the other does
   not, the `visible` one is forced to `auto`. So `overflow-x: auto` ALONE
   silently makes this a scroll container on *both* axes -- computed overflow-y
   came back `auto`. Writing `overflow-y: visible` beside it does not help;
   that is precisely the case the spec overrides, and that exact non-fix is
   what shipped last time.
   Once it is a scroll container, the th rule above resolves `position: sticky`
   against THIS box instead of the viewport. The box is content-height so it
   never scrolls, and `top: var(--header-height)` then pushes the header row
   ~64px DOWN inside the table while the body rows stay put -- which is what
   puts rank 1 visually above the header row. The header was never "failing to
   stick"; it was sticking to the wrong thing.
   Hence: no overflow at all while the table actually fits, and horizontal
   scroll only once it genuinely doesn't -- where sticky is switched off, so
   it cannot resolve against the scroll container even then.
   tools/test_sticky_header.py asserts all of this in a real browser. */
.rankings-scroll { overflow: visible; }

/* Paul, 2026-08-27: "when adding the new categories to the scoring system
   the values should be shown in the table... this probably gives us
   horizontal scroll." Real consequence: categories mode's column count now
   tracks however many scoring categories a reader has switched on (see
   resolveStatCols()'s own comment in js/app.js), so it's no longer a fixed
   number a viewport-width breakpoint can approximate -- the OLD version of
   this rule was `@media (max-width: 1000px)`, measured against table.rankings'
   own min-content width at the time (~890px). `.is-scrolling` is applied by
   js/app.js's updateTableScrollState(), which measures the real thing
   instead (table.scrollWidth vs. the wrapper's own clientWidth) after every
   render and on resize -- correct regardless of how many columns are
   currently active, not just at the one column count that was measured here
   before. Same tradeoff as before once it fires: the header gives up its
   own vertical stick (still can't coexist with a scrolling ancestor, see the
   comment above; see todo.md for why a full always-frozen header wasn't
   built instead). */
.rankings-scroll.is-scrolling { overflow-x: auto; }
.rankings-scroll.is-scrolling table.rankings th { position: static; }
/* Paul, 2026-08-29: "For columns I only want the name to be frozen (the
   ranking can drop) also no need to freeze the $ value." Pulled back from
   an earlier version that also pinned column 1 (#) on the left and the
   last column (Auction $) on the right -- only Player name (column 2)
   stays pinned now, so it's `left: 0` directly (nothing frozen before it
   to offset past anymore). */
.rankings-scroll.is-scrolling table.rankings th:nth-child(2),
.rankings-scroll.is-scrolling table.rankings td:nth-child(2) {
  position: sticky;
  left: 0;
  /* Real bug caught in testing (still applies here): th:nth-child(2) also
     matches the general `table.rankings th { position: sticky; top:
     var(--header-height); }` rule above, which this selector's higher
     specificity only overrides on `position` -- `top` was left inherited
     from that rule, so this header cell tried to stick BOTH vertically
     (against .rankings-scroll, now a real scroll container -- exactly the
     conflict this whole file's other comments warn about) AND
     horizontally at once. `top: auto` cancels the inherited value so only
     the horizontal stick (left) applies here, matching every other `th`
     in this row, which already gave up its own vertical stick via the
     `position: static` rule just above. */
  top: auto;
  z-index: 4;
  background: var(--panel);
}
/* A thin shadow-like divider so the pinned column reads as "in front of"
   the scrolling content behind it, not just coincidentally overlapping it
   -- the flat var(--panel) background alone reads fine over white but not
   over a hovered/tinted row scrolling underneath. */
.rankings-scroll.is-scrolling table.rankings td:nth-child(2) { border-right: 1px solid var(--line); }
/* Paul, 2026-08-29: "MLB points - H (allowed) looks clumsy is there a
   better way we can demarcate that some are pitching cats?" Chose the
   grouped-header-row option: a second `<thead>` row above the real
   column headers, spanning group labels ("Batting"/"Pitching") over
   however many of that group's stats are actually in the current
   statCols -- same `cfg.weightGroups` data the weight-editor's own
   fieldsets already group by (see mlb.html's own weightGroups comment),
   so this can never disagree with that grouping. Generic (js/app.js
   gates it on `cfg.weightGroups` existing, not an MLB-specific flag) --
   NFL's own Offense/Kicker/Defense weightGroups get this for free too.
   Points mode only -- categories mode's own labels (R/HR/RBI/SB/AVG vs.
   W/SV/K/ERA/WHIP) never collide the way points mode's raw H vs. "H
   (allowed)" did, so there's nothing to demarcate there.
   Two real header rows means two different sticky offsets, not one --
   `--group-row-height` is measured live in JS (renderTable()) and set as
   a custom property on <thead>, the same "single source of truth,
   measured not guessed" discipline --header-height itself already
   established, rather than a second hardcoded pixel guess that could
   drift from the row's real rendered size. */
table.rankings thead tr.group-row th {
  font-size: 0.72rem;
  padding: 3px 6px;
  text-align: center;
  font-weight: 700;
  letter-spacing: 0.02em;
  cursor: default;
}
table.rankings thead tr.col-row th { top: calc(var(--header-height) + var(--group-row-height, 27px)); }
/* The group row's blank leading/trailing spacer cells (over #/Player/Team/
   Pos, and over Value/VOR/Auction $) carry no label -- excluded from the
   hover-darken rule below so an empty cell doesn't look interactive. */
table.rankings thead tr.group-row th.group-header-blank { cursor: default; }
/* Real conflict, caught before shipping (not live-discovered): the
   sticky-left Player-name rule below keys off `th:nth-child(2)` -- a
   PER-ROW index, not "the Player column" specifically. The group row's
   OWN 2nd cell is the first group label (e.g. "Batting"), not a name,
   because its leading spacer cell spans colspan="4". Without this, that
   label would incorrectly try to pin itself to the left edge during
   horizontal scroll, the same way the real Player cell does one row
   below it. Reset back to the base (non-sticky-left) behavior here,
   scoped narrowly (higher specificity than the rule it overrides, via
   the extra `tr.group-row` -- see that rule's own comment for why `top`
   specifically needs resetting too, same reasoning applies here for
   `left`/`position`). */
.rankings-scroll.is-scrolling table.rankings tr.group-row th:nth-child(2) {
  position: static;
}
table.rankings th:hover:not(.unsortable) { color: var(--ink); }
table.rankings th.sorted { color: var(--ink); }
/* The row-number column sorts by nothing — don't offer a pointer it won't honour. */
table.rankings th.unsortable { cursor: default; }
table.rankings tbody tr:hover { background: var(--hover-bg); }
.badge { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 0.72rem; font-weight: 600; }
.badge.jackdaw { background: var(--badge-jackdaw-bg); color: var(--jackdaw); }
.badge.magpie { background: var(--badge-magpie-bg); color: var(--magpie); }
.badge.n { background: var(--badge-neutral-bg); color: var(--muted); }

.accuracy-table td.metric { font-variant-numeric: tabular-nums; }
.accuracy-table tr.system-jackdaw td:first-child { color: var(--jackdaw); font-weight: 600; }
.accuracy-table tr.system-magpie td:first-child { color: var(--magpie); font-weight: 600; }
/* The system currently driving rankings (state.system), not a static per-system
   color — this can land on any row, anonymised ones included, since a reader
   can pick either Jackdaw or Magpie regardless of who else is in the table. */
.accuracy-table tr.selected-system { background: color-mix(in srgb, var(--ink) 5%, transparent); }
.accuracy-table tr.selected-system td:first-child::before { content: '▸ '; }
/* Paul, 2026-08-25: "for display all of the redacted system names should
   be in italics" -- visually marks an anonymised label (statsengine's
   "Redacted <Word>-<hash>" pseudonyms) as not a real system name, distinct
   from Jackdaw/Magpie/Marcel/SPS/Last Season's plain-text real ones. */
.accuracy-table tr.redacted-system td:first-child { font-style: italic; }

/* Meter: one value per row (Tier 1 "Meter / progress track"). Fill carries
   identity (--bar-color, set inline per row: jackdaw/magpie brand color, or
   var(--muted) for an anonymised third-party row). Track is the SAME color at
   low opacity rather than a flat gray, so the color reads through the whole
   bar, not just the filled portion -- falls back to the solid muted color on
   browsers without color-mix() (the plain background below it in source order). */
/* Paul, 2026-08-25: "Extend the accuracy graphs to fill the rest of their
   column." Was a fixed 64px box regardless of how much real room the
   Accuracy column actually had (40% of the table as of the width-balancing
   change above, comfortably more than 64px on any real viewport) -- now a
   flex child that grows to fill whatever the cell has left after the
   percentage label's own fixed width, so the bar visually uses the column
   it was already sized to have. */
.acc-bar { display: block; flex: 1 1 auto; height: 8px; border-radius: 4px; background: var(--line); overflow: hidden; }
.acc-bar { background: color-mix(in srgb, var(--bar-color, var(--muted)) 18%, transparent); }
.acc-bar-fill { height: 100%; border-radius: 4px; background: var(--bar-color, var(--muted)); }
.acc-bar-label { flex: 0 0 auto; font-variant-numeric: tabular-nums; color: var(--muted); font-size: 0.85em; }
/* Paul, 2026-08-20: "make the start of the bars line up." Originally a
   text-align fix (the bar used to float at the right edge of a
   variable-width cell under table.rankings' own right-align default);
   now a flex row instead, which pins the start AND lets the bar and its
   label share the cell's real width properly, needed for the fill-the-
   column change above -- inline-block siblings had no shared box to grow
   the bar against. */
.accuracy-table td.acc-bar-cell { text-align: left; display: flex; align-items: center; gap: 6px; }

/* Paul, 2026-08-25: "Balance the size of the columns... name field
   (whatever size is best), 3 equal size columns for the numbers, the
   rest taken up by the accuracy chart." table-layout:fixed makes column
   widths obey the header cells' own widths (auto layout would instead
   size every column to its widest ROW's content, which is what forced
   the .acc-bar-cell fixed-width workaround above in the first place --
   still needed for horizontal alignment across rows, but no longer for
   the OVERALL table width now that every column has a real one).
   System's 30% comfortably fits the longest real label today ("Quiet
   Cormorant (redacted - 4bbe)", 33 characters) without wrapping; MAE/
   Skill vs. baseline/Rank corr. get an explicit, equal 10% each (30%
   total); Accuracy is left unset, so table-layout:fixed hands it the
   remaining 40% automatically -- the widest real content on the row
   (the bar + percentage label) benefits most from the leftover space.

   These nth-child selectors went quietly out of sync with the markup for
   a day: an `n` column was inserted right after System on 2026-08-26,
   making nth-child(2) actually `n`, not MAE -- these rules kept applying
   to the wrong columns (10% on n/MAE/Skill, Rank corr./Accuracy splitting
   the remaining 40% instead of Accuracy alone) with nothing visibly
   broken enough to notice until the layout was compared against this
   comment's own description. Fixed 2026-08-27 by removing the `n` column
   entirely (js/app.js) rather than renumbering these selectors -- restores
   the exact 5-column shape this rule was always written for. */
.accuracy-table { table-layout: fixed; }
.accuracy-table th:first-child, .accuracy-table td:first-child { width: 30%; }
.accuracy-table th:nth-child(2), .accuracy-table td:nth-child(2),
.accuracy-table th:nth-child(3), .accuracy-table td:nth-child(3),
.accuracy-table th:nth-child(4), .accuracy-table td:nth-child(4) { width: 10%; }
/* table.rankings' base th rule sets white-space:nowrap (correct for the
   main rankings table's own short headers) -- at an equal 10% width,
   "Skill vs. baseline" has nowhere to go and overflows the column instead
   of wrapping. Headers wrap to two lines here rather than winning back
   width from the other columns, which would break the equal-thirds
   balance just asked for; the short data cells underneath (144.18,
   +14.8%, 0.68) never need it. */
table.rankings.accuracy-table th { white-space: normal; }
/* The 3 number columns read as one consistent group -- MAE/Skill vs.
   baseline used to inherit table.rankings' own left-align rule (meant for
   the main rankings table's #/Player/Team columns, which happen to share
   the same first-three-children position here); Rank corr. was already
   right-aligned by the shared default. Aligned the same way now, on
   purpose, not by accident of which nth-child rule happened to apply. */
.accuracy-table th:nth-child(2), .accuracy-table td:nth-child(2),
.accuracy-table th:nth-child(3), .accuracy-table td:nth-child(3) { text-align: right; }

.muted { color: var(--muted); }
.small { font-size: 0.85rem; }

footer.site {
  border-top: 1px solid var(--line);
  padding: 24px;
  text-align: center;
  color: var(--muted);
  font-size: 0.85rem;
  margin-top: 40px;
}

.prose h2 { margin-top: 32px; }
.prose h3 { margin-top: 22px; }
.prose p, .prose li { color: var(--prose-ink); }
.prose { max-width: 760px; }
