/* dashboard.css — League Dashboard page */

/* Forward link (e.g. "Open player card ›") */
.forward-link {
    display: inline-block;
    color: var(--color-accent);
    text-decoration: none;
    font-weight: 600;
    font-size: var(--fs-085);
    transition: color 0.15s;
}
.forward-link:hover {
    color: var(--color-text);
}

/* "Open player card ›" uses the .open-full-btn accent pill (same as
   "Open full table ›"); cancel that button's right-pushing auto margin
   so it keeps its place in the chart-controls row. */
.open-full-btn.player-card-link {
    margin-left: 0;
}

/* Summary cards — always one row, equal-width columns, fluid sizing down to mobile. */
.dashboard-cards {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    gap: clamp(6px, 0.3rem + 0.6vw, 16px);
    margin-bottom: var(--space-md);
}

.dash-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: clamp(8px, 0.4rem + 0.8vw, 18px) clamp(6px, 0.3rem + 0.6vw, 16px);
    box-shadow: var(--shadow-sm);
    text-align: center;
    /* Every card keeps at least its own value legible. Without this floor the
       one card allowed to expand (Leading Player) takes the room from the two
       that aren't, and Average PR renders as "9.". Declared here, above the
       .dash-card--flex rule, so that card's max-content floor still wins. */
    min-width: min-content;
    /* The Progress card is two stacked rows, so it sets the strip's height.
       Centring every card's value keeps the single-line cards (Average PR,
       Leading Player) optically level with it instead of clinging to the top. */
    display: flex;
    flex-direction: column;
    justify-content: center;
}

/* Flex card: stays 1fr when content fits, but expands past 1fr instead of
   ellipsis-clipping when content is wider — the other cards shrink to make
   room, down to their own min-content floors and no further. */
.dash-card.dash-card--flex {
    min-width: max-content;
}
.dash-card.dash-card--flex .dash-card-value {
    overflow: visible;
    text-overflow: clip;
    /* …and when even THAT isn't enough — a 25-character leader name on a
       375px phone, where growing the card would leave the neighbours less
       room than their own numbers need — the name wraps onto a second line
       instead of spilling across the card border. max-content above still
       means one line is the preferred outcome; this is only the floor. */
    white-space: normal;
    /* `anywhere`, not `break-word`: the two break identically on screen, but
       only `anywhere` also tells the grid that this card CAN be narrow. Under
       break-word an unbreakable 40-character name still reports its full width
       as a minimum, and the grid honours it by starving the neighbours —
       Average PR was measured at 17px, i.e. "9.". */
    overflow-wrap: anywhere;
}
/* On a phone there is no room to expand INTO: a 25-character name claimed
   230 of 367 available pixels and left Average PR 22px, i.e. "9." — so below
   this width the card gives up the max-content floor and the strip stops
   splitting itself into equal columns. Sizing the columns by their CONTENT
   instead gives the leader name the slack the two number cards don't need,
   which matters because player names here have no spaces ("YossiEliezer23"):
   a name that doesn't fit can only break mid-name, so the fix is to make it
   fit. */
@media (max-width: 560px) {
    .dashboard-cards {
        grid-auto-columns: auto;
    }
    .dash-card.dash-card--flex {
        min-width: 0;
    }
}

.dash-card-label {
    font-size: var(--fs-085);
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-bottom: clamp(2px, 0.1rem + 0.2vw, 6px);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dash-card-value {
    font-size: var(--fs-112);
    font-weight: 700;
    color: var(--color-text);
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* League Progress card — two readings (schedule and calendar) in one card.
   It is the widest of the three, so it takes two of the auto columns; the
   others keep 1fr each. */
/* Same 1fr share as the other two cards whenever the row can afford it — the
   two readings need ~180px at desktop type sizes, so a third of the strip is
   already generous and a wider card would only stretch the bars into long
   empty tails.

   What it does NOT do is shrink without limit. The neighbouring Leading Player
   card is min-width: max-content and will otherwise take every pixel it wants:
   a long leader name once squeezed this card to 85px while its numbers needed
   99px, and the readings drew straight across the card's own border. The floor
   below settles that conflict in this card's favour — it cannot go under, so
   the leader card is the one that has to give (it wraps its name). */
.dash-card--progress {
    /* The widest UNBREAKABLE token ("236 / 300") — the phone floor, where the
       fluid type is at its smallest and every pixel is contested. */
    min-width: min-content;
}
@media (min-width: 561px) {
    .dash-card--progress {
        /* Measured: the card's content needs 180px at full desktop type. 190
           keeps one line per reading with a little slack. */
        min-width: 190px;
    }
}
/* Its value is a block of rows, not one line of text — cancel the single-line
   ellipsis clamp inherited from .dash-card-value, but keep the clip: nothing
   may leave the card. */
.dash-card--progress .dash-card-value {
    white-space: normal;
    overflow: hidden;
    text-overflow: clip;
}

.dash-progress {
    display: flex;
    flex-direction: column;
    gap: clamp(4px, 0.2rem + 0.4vw, 10px);
    /* Green on schedule, orange once the games fall behind the calendar.
       --color-warning is deliberately constant across themes (a caution signal
       is a traffic light, not a palette mood); --color-success follows it. */
    --dash-prog-tone: var(--color-success);
}
.dash-progress.is-behind {
    --dash-prog-tone: var(--color-warning);
}

.dash-prog-head {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 0.4em;
    /* Each piece stays on its own line; the ROW wraps when the card is too
       narrow to hold "GAMES 236 / 300 79%" in one, so the percentage drops
       under the count instead of being cut off. */
    flex-wrap: wrap;
    row-gap: 0;
    white-space: nowrap;
}

.dash-prog-name {
    font-size: var(--fs-072);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

/* The count itself keeps the card-value size, so "236 / 300" still reads as
   the headline figure next to "9.75" in the Average PR card. */
.dash-prog-value {
    font-size: var(--fs-112);
    font-weight: 700;
    color: var(--color-text);
    line-height: 1.2;
    font-variant-numeric: tabular-nums;
}

.dash-prog-pct {
    font-size: var(--fs-090);
    font-weight: 700;
    color: var(--dash-prog-tone);
    font-variant-numeric: tabular-nums;
}

/* Stands in for the Days bar on an unlimited league. Sized and cased like
   .dash-prog-name (the label it replaces), not like the count — it states a
   setting, it is not a reading. */
.dash-prog-note {
    font-size: var(--fs-072);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
    text-align: center;
}

.dash-prog-track {
    height: clamp(4px, 0.2rem + 0.25vw, 7px);
    border-radius: 999px;
    background: var(--color-border);
    overflow: hidden;
}

.dash-prog-fill {
    display: block;
    height: 100%;
    border-radius: inherit;
    background: var(--dash-prog-tone);
}

/* Flag in the Leading Player card — sized in em so it tracks the player-name text,
   matching the proportion used in the B2 leaderboard cell. */
.dash-card-value .flag {
    height: 1em;
    width: auto;
    margin-right: 0.3em;
    vertical-align: middle;
    border-radius: 0.15em;
}

/* Section frame, heading and collapse now come from the shared css/sections.css
   (.app-section / .app-section--card / .app-section-h2). `.dash-section` stays
   on each section only as a hook for the dashboard's content rules below. */
.dash-tabs-shell { margin-bottom: var(--space-lg); }

.dash-controls {
    display: flex;
    gap: var(--space-sm);
    align-items: center;
    margin-bottom: var(--space-md);
    flex-wrap: wrap;
}

.dash-controls label {
    color: var(--color-text-secondary);
    font-size: var(--fs-085);
}

.dash-controls select,
.dash-controls button {
    padding: 6px 12px;
    font-size: var(--fs-085);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    cursor: pointer;
    font-family: var(--font-main);
}


.dash-controls button:hover:not(:disabled) {
    background: var(--color-hover);
}

.dash-controls button:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

/* Snapshot stepper — the ‹ select › triple, as ONE unwrappable unit.
   Used by the Table panel (F2) and by What-If's "Run from" baseline picker, so
   the two pickers are identical in geometry and not merely in chrome.

   Why a wrapper and not three flex siblings: .dash-controls wraps, and three
   siblings can break ANYWHERE between them — so on a 430px phone the "Run from"
   label plus a flex-grown select ate row one and stranded the `›` button alone
   on row two. Nesting them makes the triple a single flex item: it wraps as a
   whole or not at all.

   flex: 1 1 240px — both instances grow into whatever the row has left, which
   is what makes their widths match; 240px is the basis below which the select
   would start truncating snapshot labels. min-width: 0 lets the select actually
   shrink inside it (a flex item's auto minimum would otherwise pin it to its
   longest <option> and force the wrap this rule exists to prevent). */
.snap-nav {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    flex: 1 1 240px;
    min-width: 0;
}
.snap-nav select { flex: 1 1 auto; min-width: 0; }
.snap-nav button { flex: 0 0 auto; }

/* The snapshot picker is the canonical search field, not a <select> — a point
   is a MATCH, and a native option cannot carry two flags and two sets of title
   badges. `.app-combo-wrap` is the wrapper mountSearchField puts around the
   input, so it is the flex child here; the field itself fills it. */
/* The picker asks for the width it actually needs — 430px of field plus 74px of
   steppers and gaps — and `flex-wrap` on the row does the rest: while that fits
   beside the "Open full table" link they share a line, and when it does not the
   picker takes a line of its own. No breakpoint decides this, because the answer
   depends on what else is in the row, not on how wide the window is.
   `container-type` makes the picker's OWN width queryable below, which is the
   measurement that governs its contents — a viewport query answered the wrong
   question and put a 381px field into a layout that needs 430px. */
.snap-nav {
    flex: 1 1 504px;
    min-width: min(100%, 504px);
    container-type: inline-size;
}
.snap-nav .app-combo-wrap { flex: 1 1 auto; min-width: 0; }
.snap-nav .snap-select { width: 100%; min-width: 0; }

/* One option row: date, winner, "beats", loser. The identity chips size off the
   row's own text (1em), per the V2 principles, so the badges track the list
   font at every width instead of a fixed token. */
.snap-opt {
    display: flex;
    align-items: center;
    gap: 0.4em;
    flex-wrap: wrap;
    min-width: 0;
}
.snap-opt-date {
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.snap-opt-verb {
    color: var(--color-text-muted);
    font-style: italic;
    flex: 0 0 auto;
}
.snap-opt-side { display: inline-flex; align-items: center; min-width: 0; }

/* The live state — the newest match — is marked by WEIGHT, not by a pill. A
   "CURRENT" badge cost 51px, and on the phone's 313px field that was exactly the
   difference between the row fitting and the date plus a name being truncated.
   The date drops its muted colour too, so the emphasis reads as one row rather
   than as a bold name beside a grey date. */
.snap-opt.is-current { font-weight: 700; }
.snap-opt.is-current .snap-opt-date { color: var(--color-text); }

/* The picked point, echoed OVER the field — same chip markup as the row, so what
   you picked still looks like what you picked. Opaque (it hides the input's own
   text underneath) and pointer-events:none (a click goes through to the input,
   which focuses it and hides this). Sized in JS off the input's computed font. */
/* INSET BY THE BORDER WIDTH, and radius matched to the input's.
   `inset: 0` covers the wrapper's whole box — which is the input's BORDER box,
   so an opaque overlay paints straight over the 1px border. With a LARGER radius
   on top of it (--radius-md 10px over the input's --radius-sm 6px) the two
   curves diverge only at the corners, so the border survived exactly there and
   nowhere else: a field that reads as four floating corner arcs with no box
   between them. Sitting on the padding box instead leaves the input's own border
   untouched — including the accent colour it takes on :focus, which an overlaid
   border would have had to reimplement and would then have drifted from. */
.snap-display {
    position: absolute;
    inset: 1px;
    display: flex;
    align-items: center;
    gap: 0.4em;
    padding: 0 0.7em;
    background: var(--color-surface, var(--color-bg));
    border-radius: var(--radius-sm);
    color: var(--color-text);
    overflow: hidden;
    white-space: nowrap;
    pointer-events: none;
}
.snap-display .snap-opt { flex-wrap: nowrap; }
.snap-display .snap-opt-date { flex: 0 0 auto; }
.snap-display .app-identity-name { overflow: hidden; text-overflow: ellipsis; }

/* Narrow screens: the picker takes the whole control row (the "Open full table"
   link wraps under it), and inside the field the DATE is what gives way first.
   Both rules exist because of the same measurement: sharing a 386px row with the
   link left the field 205px, and the two player NAMES — the only flexible items
   in it — were squeezed to 0px and 1px. The names are the subject of the row, so
   they get a floor and the date yields; a shortened date still reads, an erased
   name does not. */
/* TWO LINES once the picker itself is narrower than the one-line layout needs:
   the date above, the match below.
   Measured, not guessed: across this league's 89 matches the widest pairing in
   its emphasised (current) form needs a 430px field, and the steppers take 74px
   more. Below that, one line truncated in three places at once — "7 Jul 2026,
   16:…", a title badge cut off mid-row, "ilanblu…" — erasing exactly what the
   row exists to show. When the date and the match cannot share a line, the
   honest layout is to stop making them compete: the match line then owns the
   full width and nothing is cut, at the cost of a slightly taller field.
   Keyed to the PICKER's width, so it holds for a narrow window, a narrow phone
   and any future column this control is dropped into alike. */
/* The probe: the same row, laid out on one line with no width limit, so its
   natural width can be compared with what the field actually has. Not painted —
   it only ever gets measured. */
.snap-display--probe {
    visibility: hidden;
    inset: auto;
    top: 0;
    left: 0;
    width: max-content;
    max-width: none;
    background: none;
}

/* A row that does NOT fit on one line stacks: date above, match below. The
   decision is per row (see mountSnapshotPicker), because the rows differ far
   more than the viewports do — one breakpoint would stack "ys beats Yaniv162",
   which has room to spare at any width, and still truncate "Hummus NC M2 beats
   YossiEliezer23", which never fits beside a date. */
.snap-display.is-stacked {
    flex-wrap: wrap;
    align-content: center;
    gap: 0 0.3em;
    white-space: normal;
}
.snap-display.is-stacked .snap-opt {
    flex-wrap: wrap;
    gap: 0 0.3em;
    row-gap: 0;
}
.snap-display.is-stacked .snap-opt-date {
    flex: 1 1 100%;
    font-size: 0.88em;
    line-height: 1.25;
}

/* Only the HEIGHT stays width-driven: once the picker is narrow enough that some
   row will stack, the field reserves both lines for every row. Otherwise the
   control — and everything below it — would change height on each ‹ › step, as
   long and short rows alternate. */
@container (max-width: 503px) {
    .snap-nav .snap-select { min-height: 3.2em; }
    .snap-display { padding: 0 0.5em; }
}

/* ── Title Race (B4b) ───────────────────────────────────────────────────────
   The chart itself reuses .chart-host / .bar-chart-canvas / .chart-info-panel
   from components.css — only the controls and the legend are new here. */
/* Show above, Add-a-player below — one control per line at every width, which
   is what the narrow layout did anyway. Side by side they read as one compound
   control: a dropdown that filters and a field that searches are two unrelated
   actions, and a shared row implies they cooperate. */
.titlerace-controls {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-sm);
    margin-bottom: var(--space-sm);
}
.titlerace-add { width: 100%; max-width: 320px; min-width: 0; }
.titlerace-controls .app-combo-wrap { width: 100%; max-width: 320px; min-width: 0; }

.titlerace-legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs) var(--space-sm);
    margin-bottom: var(--space-sm);
    min-height: 1.6em;
}
.tr-legend-empty { color: var(--color-text-muted); font-size: var(--fs-085); }

/* A legend entry is the same identity chip the chart's detail panel and every
   picker use, prefixed by the line's colour and followed by its remove control —
   so the thing you added, the line it drew and the row in the panel all read as
   one object. */
.tr-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.35em;
    padding: 0.15em 0.45em;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-inset);
    font-size: var(--fs-085);
}
.tr-swatch {
    width: 0.75em;
    height: 0.75em;
    border-radius: 2px;
    flex: 0 0 auto;
}
.tr-chip-x {
    border: 0;
    background: none;
    color: var(--color-text-muted);
    cursor: pointer;
    font-size: 1.15em;
    line-height: 1;
    padding: 0 0.15em;
}
.tr-chip-x:hover { color: var(--color-loss); }

/* "beats" inside the detail panel's title — muted and italic, matching the
   snapshot picker's verb, so the same relation reads the same way twice. */
.tr-verb {
    color: var(--color-text-muted);
    font-style: italic;
    font-weight: normal;
}

/* One player per row in the detail panel: swatch, name, value — with the value
   pushed to the trailing edge so the percentages form a column the eye can read
   down. Packed inline they ran together and the pairing of name to number had
   to be worked out rather than seen. */
.chart-info-panel .tr-odds-head {
    color: var(--color-text-muted);
    font-size: var(--fs-085);
    margin-top: 0.35em;
}
.chart-info-panel .tr-odds-row {
    display: flex;
    align-items: center;
    gap: 0.45em;
}
.chart-info-panel .tr-odds-name { min-width: 0; }

/* Record and odds form TWO columns at the trailing edge. The auto margin lives
   on the record (the first of the pair) so both stay flush right together; put
   it on each and the free space would split between them, scattering the
   records across the middle of the panel. */
.chart-info-panel .tr-odds-rec {
    margin-left: auto;
    flex: 0 0 auto;
    font-size: var(--fs-085);
    font-variant-numeric: tabular-nums;
    color: var(--color-text-muted);
    white-space: nowrap;
}
.chart-info-panel .tr-rec-w { color: var(--color-win); font-weight: 600; }
.chart-info-panel .tr-rec-l { color: var(--color-loss); font-weight: 600; }
.chart-info-panel .tr-rec-d { color: var(--color-text-muted); font-weight: 600; }
.chart-info-panel .tr-odds-val {
    margin-left: 0.7em;
    min-width: 3.4em;
    text-align: right;
    flex: 0 0 auto;
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

.round-label {
    font-weight: 600;
    min-width: 130px;
    text-align: center;
    color: var(--color-text);
}

/* Scroll wrapper — overflow-x + corner clipping live here, not on the table.
   overflow-x:auto triggers border-radius clipping per CSS spec (no overflow:hidden needed). */
.dash-table-wrap {
    --col1-w: 36px; /* fallback replaced by JS (iron rule 12) */
    overflow-x: auto;
    min-width: 0;
    box-shadow: var(--shadow-sm);
}

/* B2 rank column — sticky col 1 */
.dash-table-wrap .dash-table thead th:nth-child(1) {
    position: sticky;
    left: 0;
    z-index: 5;
    background: var(--header-bg);
}
.dash-table-wrap .dash-table tbody td:nth-child(1) {
    position: sticky;
    left: 0;
    z-index: 4;
    background: var(--color-inset);
}
.dash-table-wrap tbody tr:hover td:nth-child(1) {
    background: var(--color-hover);
}
/* B2 sticky col 1 — row tint overrides (iron rule 11) */
.dash-table-wrap .dash-table tbody tr.rank-gold td:nth-child(1)   { background: var(--color-gold-bg); }
.dash-table-wrap .dash-table tbody tr.rank-silver td:nth-child(1) { background: var(--color-silver-bg); }
.dash-table-wrap .dash-table tbody tr.rank-bronze td:nth-child(1) { background: var(--color-bronze-bg); }
.dash-table-wrap .dash-table tbody tr.unplayed-row td:nth-child(1) { background: var(--color-unplayed-bg); }

/* B2 Historical — player col is sticky col 2 (offset by col 1 width) */
.dash-table-wrap .dash-table th.player-col {
    position: sticky;
    left: var(--col1-w);
    z-index: 4;
}
.dash-table-wrap .dash-table td.player-cell {
    left: var(--col1-w);
}

/* B2 sticky column boundary drop-shadow — Google-style, appears only on scroll */
.dash-table-wrap .dash-table thead th.player-col {
    box-shadow: inset 0 -1px 0 var(--color-border);
}
.dash-table-wrap.is-scrolled-x .dash-table thead th.player-col {
    box-shadow: inset 0 -1px 0 var(--color-border), 6px 0 8px -4px rgba(0, 0, 0, 0.18);
}
.dash-table-wrap.is-scrolled-x .dash-table tbody td.player-cell {
    box-shadow: inset 0 -1px 0 var(--color-border), 6px 0 8px -4px rgba(0, 0, 0, 0.18);
}
.dash-table-wrap.is-scrolled-x .dash-table tbody tr:last-child td.player-cell {
    box-shadow: 6px 0 8px -4px rgba(0, 0, 0, 0.18);
}

/* Theme-aware dash tables — sit on --color-inset so the table reads as a
   recessed well inside its --color-surface section card (not the same colour). */
.dash-table {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    background: var(--color-inset);
    color: var(--color-text);
}

.dash-table th,
.dash-table td {
    padding: 0.45em 0.5em;
    box-shadow: inset 0 -1px 0 var(--color-border);
    text-align: left;
    white-space: nowrap;
    font-size: inherit;
}

.dash-table tbody tr:last-child td { box-shadow: none; }

.dash-table th {
    background: var(--header-bg);
    color: var(--header-text);
    font-weight: 600;
}

.dash-table tr:hover td {
    background: var(--color-hover);
}

.dash-table tr.unplayed-row td {
    background: var(--color-unplayed-bg);
    color: var(--color-text-muted);
    font-style: italic;
}

/* Distinct medal row tints (override token collisions in variables.css) */
.dash-table tr.rank-gold td { background: var(--color-gold-bg); color: var(--color-gold-text); }
.dash-table tr.rank-silver td { background: var(--color-silver-bg); color: var(--color-silver-text); }
.dash-table tr.rank-bronze td { background: var(--color-bronze-bg); color: var(--color-bronze-text); }

/* Player column / cell alignment + sticky */
.dash-table th.player-col,
.dash-table td.player-cell {
    text-align: left;
    padding-left: var(--space-md);
}
.dash-table td.player-cell {
    position: sticky;
    left: 0;
    background: var(--color-inset);
}

.dash-table .flag {
    height: 1em;
    width: auto;
    vertical-align: middle;
    margin-right: 0.3em;
    object-fit: contain;
    border-radius: 0.15em;
}

/* .chart-panel, .chart-host, .bar-chart-canvas, .chart-info-panel — shared, defined in components.css */

/* Pinned to the panel's own top-right corner (not a flex child of
   .dash-controls), so it never drops onto a wrapped line on narrow screens —
   it stays put no matter how the player/metric controls reflow beneath it. */
.remove-chart {
    position: absolute !important;
    top: var(--space-sm) !important;
    right: var(--space-sm) !important;
    background: transparent !important;
    border: none !important;
    font-size: var(--fs-140) !important;
    color: var(--color-text-muted) !important;
    padding: 0 4px !important;
    cursor: pointer;
    z-index: 1;
}

.remove-chart:hover {
    color: var(--color-loss) !important;
}

/* Reserve room so wrapped controls never run under the pinned close button. */
.chart-panel .dash-controls {
    padding-right: 30px;
}

/* .corr-controls carries the reservation on its own top row only (the row
   the close button actually sits above) — the full-width pills below it
   don't need to dodge the button, and should span the same width as the
   graph canvas beside them, so they get their padding-right reset to 0.
   (Matches ".chart-panel .dash-controls" specificity above so it actually
   wins the cascade instead of losing to it.) */
.chart-panel .corr-controls {
    padding-right: 0;
}
.corr-controls-top {
    padding-right: 30px;
}

.add-chart-btn {
    padding: 8px 16px;
    border: 1px dashed var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text-secondary);
    cursor: pointer;
    font-family: var(--font-main);
    font-size: var(--fs-095);
}

.add-chart-btn:hover {
    background: var(--color-hover);
    color: var(--color-text);
    border-color: var(--color-accent);
}

/* ── PR <-> Result correlation rows ─────────────────── */
.corr-panel {
    padding-bottom: var(--space-sm);
}

.corr-panel--general {
    background: var(--color-inset);
    border-style: dashed;
}

.corr-host {
    margin-top: var(--space-xs);
}

/* Controls block: player/label row on top, then one full-width metric pill
   per row below. Same structure and rules at every viewport width — no
   mobile-only hides or shrink breakpoints, so mobile and desktop always show
   identical content, just reflowed by the fluid --fs-* font tokens. */
.corr-controls {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-sm);
}

.corr-controls-top {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    flex-wrap: wrap;
}

.corr-alltime-label {
    color: var(--color-text-secondary);
    font-size: var(--fs-085);
}

/* Groups the "Gaussian fit" and "Trim to 99%" toggles and the PR-gap shift
   stepper together, pushed to the far side of the control row away from the
   label. */
.corr-shift-group {
    display: flex;
    align-items: stretch;
    gap: var(--space-sm);
    margin-left: auto;
    flex-wrap: wrap;
}

.corr-gaussian-stats {
    color: var(--color-warning);
    font-size: var(--fs-085);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Plain on/off toggles (All League Matches row) — pressed state just tints
   the button, no stepper affordance. */
.corr-trim-toggle,
.corr-gaussian-toggle,
.corr-explanation-toggle {
    display: flex;
    align-items: center;
    padding: 0 14px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-inset);
    color: var(--color-text-secondary);
    font-family: var(--font-main);
    font-size: var(--fs-085);
    cursor: pointer;
    transition: background 0.15s, color 0.15s, border-color 0.15s;
}

.corr-trim-toggle:hover:not(:disabled),
.corr-gaussian-toggle:hover:not(:disabled),
.corr-explanation-toggle:hover:not(:disabled) {
    background: var(--color-hover);
    color: var(--color-text);
}

.corr-trim-toggle.is-active {
    background: var(--color-accent);
    border-color: var(--color-accent);
    color: var(--color-on-accent);
}

.corr-gaussian-toggle.is-active {
    background: var(--color-warning);
    border-color: var(--color-warning);
    color: #fff;
}

.corr-explanation-toggle.is-active {
    background: var(--color-accent);
    border-color: var(--color-accent);
    color: var(--color-on-accent);
}

.corr-trim-toggle:disabled,
.corr-gaussian-toggle:disabled,
.corr-explanation-toggle:disabled {
    opacity: 0.35;
    cursor: not-allowed;
}

/* Shift stepper (All League Matches row) — a single connected segmented
   control (decrement | caption+value | increment). Hover the whole control
   for the full explanation of what "shift" means (see the title attribute
   set in dashboardPage.js) — the caption above the number is just a short
   label, not the full explanation, so it stays legible at any --fs-* size. */
.corr-shift-control {
    display: flex;
    align-items: stretch;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-inset);
    overflow: hidden;
    cursor: help;
}

.corr-shift-value {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 2px 12px;
    min-width: 6.5em;
    border-left: 1px solid var(--color-border);
    border-right: 1px solid var(--color-border);
}

.corr-shift-caption {
    color: var(--color-text-muted);
    font-size: var(--fs-070);
    text-transform: uppercase;
    letter-spacing: 0.03em;
    white-space: nowrap;
}

.corr-shift-amount {
    color: var(--color-accent);
    font-size: var(--fs-095);
    font-variant-numeric: tabular-nums;
}

.corr-shift-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.2em;
    border: none;
    background: transparent;
    color: var(--color-text-secondary);
    font-size: var(--fs-120);
    line-height: 1;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
}

.corr-shift-btn:hover:not(:disabled) {
    background: var(--color-hover);
    color: var(--color-accent);
}

.corr-shift-btn:disabled {
    opacity: 0.35;
    cursor: not-allowed;
}

.corr-games-count {
    color: var(--color-text-muted);
    font-size: var(--fs-085);
    font-variant-numeric: tabular-nums;
}

.corr-row-canvas {
    display: block;
    width: 100%;
    background: var(--color-inset);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
}

.corr-panel--general .corr-row-canvas {
    background: var(--color-surface);
}

/* Grid, not flex: each part of the pill (caption / bar / label) owns a fixed
   proportional column of the pill's own width, in `fr` units (NOT percent —
   a raw percentage track ignores column-gap and adds it on top, so columns
   summing to 100% plus gaps would overflow the container; `fr` correctly
   divides only the space left AFTER subtracting gaps, so these columns
   always sum to exactly the container's width, at every viewport). The
   value (e.g. "0.116") isn't a column of its own — it sits UNDER the bar,
   directly below its marker (see .brier-mini-value), so it doesn't compete
   with the caption/label for horizontal room at narrow viewports; the pill
   is simply a little taller instead. The 27/46/27 split is chosen so the
   bar column's own midpoint (27 + 46/2 = 50, out of 100) lands on the
   pill's midpoint. */
.corr-metric-pill {
    display: grid;
    grid-template-columns: 27fr 46fr 27fr;
    align-items: center;
    column-gap: var(--space-sm);
    width: 100%;
    box-sizing: border-box;
    background: var(--color-inset);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    padding: 8px 16px;
    font-size: var(--fs-085);
    font-variant-numeric: tabular-nums;
    cursor: help;
}

/* A grid track's default min-width is "auto" — big enough to fit its item's
   full unbroken content (min-content), which for a nowrap text span is the
   ENTIRE text width. That silently overrides the fr track sizes above and
   pushes the row wider than the pill at narrow viewports. min-width 0 here
   is what actually makes the 27/46/27 split hold everywhere. */
.corr-metric-pill > * {
    min-width: 0;
}

.corr-metric-pill i {
    color: var(--color-text-muted);
    font-style: normal;
}

.brier-caption {
    color: var(--color-text-secondary);
    text-align: center;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Mini -1..+1 Luck scale shown inline next to the number. Fills its grid
   column exactly (a grid item stretches to its track's width by default),
   so it's always the same fraction of the pill at every viewport, never
   wider than its column, and never pushes a neighbouring column past the
   pill's edge. Its height sets the whole row's height, so the caption/label
   (vertically centred by .corr-metric-pill's align-items:center) end up
   centred on this box too — the track inside is positioned to share that
   same centre line (see .luck-mini-track), with .brier-mini-value's fixed
   room reserved below it. */
.luck-mini-scale {
    position: relative;
    height: 46px;
}

.luck-mini-track {
    position: absolute;
    top: 18px;
    left: 0;
    right: 0;
    height: 10px;
    border-radius: 5px;
    background: linear-gradient(to right, var(--brier-bad) 0%, var(--brier-mid) 50%, var(--brier-good) 100%);
}

.brier-mini-marker {
    position: absolute;
    top: 16px; /* tracks .luck-mini-track's top (18px), minus the same
        2px overhang it always had */
    width: 3px;
    height: 14px;
    border-radius: 1px;
    background: var(--color-text);
    box-shadow: 0 0 0 1px var(--color-surface);
    transform: translateX(-50%);
}

/* The metric's own number (e.g. "0.116"), positioned directly under its
   marker on the bar above — freeing up the caption/label columns so they no
   longer have to share horizontal room with the value at narrow viewports.
   Its `left` is set inline per-render to the same (clamped) percentage as
   the marker, so it always tracks the marker's position. */
.brier-mini-value {
    position: absolute;
    top: 32px; /* just below .brier-mini-track's new bottom edge (18+10=28px) */
    color: var(--color-accent);
    font-weight: 600;
    white-space: nowrap;
    transform: translateX(-50%);
}

/* The verdict text ("Slightly lucky", "Extremely unlucky", ...). It wraps
   rather than truncates: unlike a player name, this is a closed set of five
   strings we author ourselves (see luckConfidenceLabel in
   compute/luckConfidence.js), so there is no width at which cutting one off
   is the honest trade-off — ellipsis belongs to open-ended data only.
   nowrap+ellipsis here truncated 'Slightly lucky' below ~344px viewport and
   'Extremely unlucky' already at 390px, because --fs-085 bottoms out at its
   clamp() minimum while the 27fr column keeps shrinking with the viewport.
   Every label is one or two words, so the break lands on the space between
   them and the text stays centred in the same column. It costs no height:
   two lines (~30px) still fit inside the 46px set by .luck-mini-scale, so
   the pill does not grow. */
.brier-label {
    font-weight: 600;
    text-align: center;
    overflow: hidden;
    text-wrap: balance;
    line-height: 1.15;
}

/* -1..+1 Luck visual scale, shown in the "?" explanation popup. Same
   gradient as .luck-mini-track. */
.brier-scale {
    position: relative;
    margin: var(--space-md) 4px var(--space-lg);
}

.brier-scale-bar {
    height: 12px;
    border-radius: 6px;
    background: linear-gradient(to right, var(--brier-bad) 0%, var(--brier-mid) 50%, var(--brier-good) 100%);
}

.brier-scale-ticks {
    position: relative;
    height: 34px;
}

.brier-scale-tick {
    position: absolute;
    top: 4px;
    transform: translateX(-50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    font-size: var(--fs-078);
    color: var(--color-text);
    white-space: nowrap;
}

.brier-scale-tick:first-child { transform: translateX(0); align-items: flex-start; }
.brier-scale-tick:last-child { transform: translateX(-100%); align-items: flex-end; }

.brier-scale-tick small {
    color: var(--color-text-muted);
    font-size: var(--fs-070);
}

/* Open full table button (next to historical date select) */
.open-full-btn {
    margin-left: auto;
    padding: 6px 14px;
    background: var(--color-accent);
    color: var(--color-on-accent);
    border-radius: var(--radius-sm);
    text-decoration: none;
    font-size: var(--fs-085);
    font-weight: 500;
    transition: background 0.15s;
}

.open-full-btn:hover {
    background: var(--color-text);
}

/* Player name link — disabled (general card placeholder) */
.player-name-link {
    color: var(--color-accent);
    text-decoration: none;
    cursor: pointer;
}

.player-name-link:hover {
    text-decoration: underline;
}

.player-name-link.disabled-link {
    color: var(--color-text-muted);
    cursor: not-allowed;
    text-decoration: line-through;
}

.player-name-link.disabled-link:hover {
    text-decoration: line-through;
}

/* Right-click context menu */
.player-context-menu {
    position: fixed;
    background: var(--popover-bg);
    border: 1px solid var(--popover-border-color);
    border-radius: var(--popover-radius);
    box-shadow: var(--popover-shadow);
    padding: 4px 0;
    min-width: 200px;
    z-index: 1000;
    font-size: var(--fs-090);
}

.player-context-menu .cm-item {
    display: block;
    padding: 8px 14px;
    color: var(--color-text);
    text-decoration: none;
    cursor: pointer;
}

.player-context-menu .cm-item:hover:not(.disabled) {
    background: var(--color-hover);
}

.player-context-menu .cm-item.disabled {
    color: var(--color-text-muted);
    cursor: not-allowed;
    text-decoration: line-through;
}

/* ── Championship Predictor ─────────────────────────── */
.predictor-tooltip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--color-border);
    color: var(--color-text-muted);
    font-size: var(--fs-075);
    font-weight: 700;
    cursor: pointer;
    vertical-align: middle;
    margin-left: var(--space-xs);
    user-select: none;
}
.predictor-tooltip:hover {
    background: var(--color-text-muted);
    color: var(--color-bg);
}

/* Language flags next to a "?" button — click sets the site-wide popup
   language (see js/utils/popupLang.js) and opens that popup. */
.popup-lang-flags {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    margin-left: var(--space-xs);
    vertical-align: middle;
}
.popup-lang-flag {
    display: inline-flex;
    width: 18px;
    height: 18px;
    padding: 0;
    border: 1px solid var(--color-border);
    border-radius: 50%;
    background: none;
    cursor: pointer;
    opacity: 0.55;
    transition: opacity 0.15s, border-color 0.15s;
}
.popup-lang-flag:hover {
    opacity: 0.85;
}
/* Flags rendered INSIDE a popup (top of its content, not next to the "?"
   that opened it) — a plain flow row, left-aligned so it never collides
   with the absolutely-positioned × close button in the top-right corner. */
.popup-lang-flags-bar {
    margin-bottom: var(--space-sm);
}
.popup-lang-flags-bar .popup-lang-flags {
    margin-left: 0;
}
.popup-lang-flag img {
    width: 100%;
    height: 100%;
    border-radius: 50%;
    object-fit: cover;
    pointer-events: none;
}
/* Highlights whichever flag matches the current global language — toggled
   in JS (wireLangPopup, popupLang.js), not CSS, since a flag button and its
   popup aren't always adjacent siblings in the DOM. */
.popup-lang-flag.is-active {
    opacity: 1;
    border-color: var(--color-accent);
}

/* Each popup carries both languages inline; data-active-lang (kept in sync
   by wireLangPopup, see popupLang.js) picks which block CSS shows — no
   innerHTML swap, no re-render. Hebrew blocks switch to RTL reading order. */
.predictor-info-popup[data-active-lang="en"] .popup-lang-he,
.predictor-info-popup[data-active-lang="he"] .popup-lang-en,
.predictor-info-popup:not([data-active-lang]) .popup-lang-he {
    display: none;
}
.predictor-info-popup[data-active-lang="he"] .popup-lang-he {
    direction: rtl;
    text-align: right;
}
.predictor-info-popup[data-active-lang="he"] .popup-lang-he ul {
    padding-left: 0;
    padding-right: 1.4em;
}
.predictor-info-popup[data-active-lang="he"] .popup-lang-he table {
    direction: rtl;
}
.predictor-info-popup[data-active-lang="he"] .popup-lang-he th,
.predictor-info-popup[data-active-lang="he"] .popup-lang-he td {
    text-align: right;
}
/* Justify the popup body prose in BOTH languages so paragraphs align flush to
   the right AND the left edge. Targets p/li directly (more specific than the
   block-level right-align above), so headings stay right-aligned and inline
   text-align:center lines — the formulas — keep their own centring. Only lines
   that wrap are stretched; a single/last line keeps its natural edge. */
.predictor-info-popup .popup-lang-en p,
.predictor-info-popup .popup-lang-en li,
.predictor-info-popup .popup-lang-he p,
.predictor-info-popup .popup-lang-he li {
    text-align: justify;
}

.predictor-info-popup {
    position: relative;
    background: var(--popover-bg);
    border: 1px solid var(--popover-border-color);
    border-radius: var(--popover-radius);
    padding: var(--space-md) var(--space-lg);
    margin-bottom: var(--space-md);
    font-size: var(--fs-085);
    line-height: 1.55;
    color: var(--color-text);
    box-shadow: var(--popover-shadow);
}
.predictor-info-popup h3 {
    margin: 0 0 var(--space-sm);
    font-size: var(--fs-100);
}
.predictor-info-popup h4 {
    margin: var(--space-sm) 0 var(--space-xs);
    font-size: var(--fs-090);
    color: var(--color-text-muted);
}
/* Subsection heading (e.g. the numbered steps inside "The mathematical
   derivation") — smaller than h4, with a subtle nesting rule on the leading edge. */
.predictor-info-popup h5 {
    margin: var(--space-sm) 0 var(--space-xs);
    font-size: var(--fs-085);
    color: var(--color-text-muted);
    font-weight: 600;
    padding-inline-start: 0.6em;
    border-inline-start: 3px solid var(--color-border);
}
/* Muted "this is a note / can be skipped" lead paragraph inside a popup. */
.predictor-info-popup .popup-sample-note {
    font-size: var(--fs-085);
    color: var(--color-text-muted);
    background: var(--color-inset);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    padding: var(--space-xs) var(--space-sm);
    margin: var(--space-xs) 0 var(--space-sm);
}
.predictor-info-popup ul {
    margin: var(--space-xs) 0;
    padding-left: 1.4em;
}
.predictor-info-popup li {
    margin-bottom: var(--space-xs);
}
.predictor-info-popup p {
    margin: var(--space-xs) 0;
}
/* Real typeset equation (MathML, see BRIER_FORMULA_MATHML/LUCK_FORMULA_MATHML
   in dashboardPage.js) under a score's prose explanation — a muted chip like
   before, but no forced monospace font: MathML resolves its own math font
   regardless of font-family, and centering it reads like a proper displayed
   equation rather than an inline text fragment. Always LTR (math notation
   direction doesn't flip in RTL popups — see the math element's own dir="ltr"). */
.corr-formula {
    display: flex;
    justify-content: center;
    font-size: var(--fs-100);
    color: var(--color-text-muted);
    background: var(--color-inset);
    border-radius: var(--radius-sm);
    padding: var(--space-sm);
    overflow-x: auto;
}
/* PM — PR Matrix Format (production mirror of table-lab/formats/pm/pm.css).
   Two shapes share one look: .pm-matrix (PR-gap × match-length grid, centred)
   and .pm-list (per-gap rows, left-aligned). See docs/TABLE-DESIGN.md ▸ PM. */
/* The figure IS the paragraph-width box (mirror of pm.css): fills the paragraph
   column; a wider table scrolls inside it rather than stretching past — so with
   .pm-table { width:100% } every table's box equals the paragraph width. */
.pm-figure {
    margin: var(--space-sm) 0;
    overflow-x: auto;
}
.pm-caption {
    font-size: 0.9em;
    margin-bottom: 0.3em;
}
.pm-note {
    font-weight: normal;
    opacity: 0.72;
    font-size: 0.92em;
}
.pm-scroll {
    overflow-x: auto;
    margin: var(--space-sm) 0;
}
.pm-scroll .pm-figure { margin: 0; }
/* Grid lines via single-edge ownership (see pm.css): each cell paints only its
   TOP + LEFT border and the table its RIGHT + BOTTOM, so every gridline is drawn
   once — avoids Chrome's doubled/uneven internal borders on collapsed tables at
   fractional display scaling (125% / 150%). */
.pm-table {
    border-collapse: separate;
    border-spacing: 0;
    border-right: 1px solid var(--popover-border-color);
    border-bottom: 1px solid var(--popover-border-color);
    margin: var(--space-sm) 0;
    width: 100%;
    font-size: var(--fs-085);
}

/* Row of the three worked contribᵢ(θ) example curves in the Luck-Confidence
   explainer — wraps to a single column on narrow popups. */
.contrib-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    justify-content: center;
    margin: var(--space-sm) 0;
}
.contrib-grid .luck-contrib-curve {
    flex: 1 1 200px;
    min-width: 190px;
    max-width: 320px;
}

/* Small reference table under a Brier/Luck mini-scale in the "?" popups,
   listing every band's exact range and label — the gradient bar itself only
   has room for a couple of anchor ticks (0/0.25/1, or -1/0/+1), not all 5-6
   bands, so this is where the full breakdown lives. */
.corr-band-table {
    border-collapse: collapse;
    margin: var(--space-sm) 0;
    width: 100%;
    font-size: var(--fs-085);
}
.corr-band-table th,
.corr-band-table td {
    border: 1px solid var(--popover-border-color);
    padding: 3px 8px;
    text-align: left;
}
.corr-band-table th {
    background: var(--header-bg);
    color: var(--header-text);
    font-weight: 600;
}

/* Marks a gap's empirical %/error as backed by too few matches to trust
   (see MIN_GAP_SAMPLE in dashboardPage.js) — greyed rather than removed, so
   the shape of the trend is still visible, just visibly less certain. */
.corr-gap-low-n {
    color: var(--color-text-muted);
    font-style: italic;
    cursor: help;
}
.pm-table th,
.pm-table td {
    border-top: 1px solid var(--popover-border-color);
    border-left: 1px solid var(--popover-border-color);
    padding: 0.15em 0.4em;
    white-space: nowrap;
}
/* Soften the internal separators BETWEEN body rows to a lighter tint than the
   outer frame (mirror of pm.css). On high-DPR phones a 1px inner line straddling
   the device-pixel grid renders heavier than the crisp box edge, so the stack of
   full-strength inner rules looked too thick against the (correct) bottom edge.
   The outer edges, the header, and the header↔body divider stay full strength. */
.pm-table tbody tr:not(:first-child) th,
.pm-table tbody tr:not(:first-child) td {
    border-top-color: color-mix(in srgb, var(--popover-border-color) 45%, transparent);
}
.pm-table thead th,
.pm-table th[scope="row"] {
    background: var(--header-bg);
    color: var(--header-text);
    font-weight: 600;
}
.pm-table.pm-matrix,
.pm-table.pm-matrix th,
.pm-table.pm-matrix td {
    text-align: center;
}
.pm-table.pm-list th,
.pm-table.pm-list td {
    text-align: start;
    padding: 0.2em 0.6em;
}
.pm-table.pm-list td:first-child {
    font-variant-numeric: tabular-nums;
    direction: ltr;
    unicode-bidi: embed;
    font-weight: 600;
}
.pm-table .pm-thin td { opacity: 0.5; }
/* Hebrew (RTL) list variant: right-align every column; numeric ranges are
   wrapped in <span dir="ltr"> by the caller so they keep reading order. */
.pm-table.pm-list.pm-rtl th,
.pm-table.pm-list.pm-rtl td { text-align: right; }
.pm-table.pm-list.pm-rtl td:first-child { direction: rtl; }
/* Plain first column (value range, not a row identifier) — drop the emphasis. */
.pm-table.pm-list.pm-plain-first td:first-child { font-weight: normal; }

/* Mobile: drop the longest match-length columns (19+) from the matrix so it
   shows up to 17 without needing to scroll. The table still keeps width:100% and
   its .pm-figure scroll box (above), so its box stays exactly the paragraph
   width at every viewport — it is NOT shrunk-to-content or centered. Font stays
   at the shared --fs-085 token, matching the surrounding prose. */
@media (max-width: 640px) {
    .pm-table.pm-matrix thead tr:nth-child(2) > th:nth-child(n+10),
    .pm-table.pm-matrix tbody tr > td:nth-child(n+11) {
        display: none;
    }
}

/* League-type pills sitting inline in the "?" popup prose must be sized RELATIVE
   to the surrounding text — exactly as .ana-entity and the league-header cards
   already do — not off the absolute --fs-085 token the base pill uses. Without
   this, on mobile (where the popup prose is smaller) the fixed-size pill looks
   noticeably oversized next to its sentence; on desktop the prose is larger so
   it's only slightly off. The em size fixes both. */
.predictor-info-popup .league-type-pill {
    font-size: 0.72em;
    padding: 0.08em 0.5em;
    line-height: 1.35;
    vertical-align: baseline;
}
.predictor-info-close {
    position: absolute;
    top: var(--space-xs);
    right: var(--space-sm);
    background: none;
    border: none;
    font-size: var(--fs-120);
    cursor: pointer;
    color: var(--color-text-muted);
    line-height: 1;
}
.predictor-info-close:hover {
    color: var(--color-text);
}

.predictor-moe {
    font-size: var(--fs-080);
    color: var(--color-text-muted);
    margin-bottom: var(--space-sm);
}


.predictor-topx-control {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    margin-bottom: var(--space-sm);
    font-size: var(--fs-088);
    color: var(--color-text-secondary);
}

.topx-select {
    padding: 2px var(--space-xs);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-size: var(--fs-090);
    font-family: var(--font-main);
    cursor: pointer;
}
.topx-select:focus { outline: none; border-color: var(--color-accent); }

.predictor-pct-cell {
}

.predictor-pct-bar {
    position: relative;
    background: linear-gradient(to right, var(--bar-color, var(--tier-high)) var(--pct, 0%), transparent var(--pct, 0%));
    border-radius: var(--radius-sm);
    padding: 0.15em 0.6em;
    font-size: inherit;
}

.predictor-expand-btn {
    display: block;
    margin: var(--space-sm) auto var(--space-xl);
    padding: 4px 16px;
    font-size: var(--fs-085);
    font-family: var(--font-main);
    color: var(--color-text);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    cursor: pointer;
    transition: background 0.15s;
}

.predictor-expand-btn:hover {
    background: var(--color-hover);
}

/* Collapsible section headings (Prizes & Medals / Championship Predictor /
   What If) now use the shared mechanism in css/sections.css + sectionCollapse.js
   (.app-section-h2.is-collapsible + .app-section.is-collapsed). */

.prizes-info {
    margin-bottom: var(--space-sm);
    font-size: var(--fs-090);
}
.prizes-entry b {
    color: var(--color-accent);
}

.prizes-table-wrap {
    overflow-x: auto;
    min-width: 0;
    width: 70%;
    margin: 0 auto;
    box-shadow: var(--shadow-sm);
}
.prizes-table {
}
/* Medal icon cell — sized relative to the table's font-small variant (em),
   so it stays proportional if the table ever switches font-size class. */
.prizes-table td:first-child {
    font-size: 1.6em;
}
/* B1 — Prizes & Medals: tint each tier row with its medal colour (theme-aware
   tokens, matching the rank-gold/silver/bronze rows on the leaderboard + standings)
   instead of the old faint hardcoded rgba. Text colour follows so the row stays
   legible on the dark medal fills used by dark themes. */
.prize-row-gold td {
    background: var(--color-gold-bg);
    color: var(--color-gold-text);
}
.prize-row-silver td {
    background: var(--color-silver-bg);
    color: var(--color-silver-text);
}
.prize-row-bronze td {
    background: var(--color-bronze-bg);
    color: var(--color-bronze-text);
}

/* ── What-If Simulator ──────────────────────────────────
   Collapsible .app-section card (shared chrome); the `?` reuses
   .predictor-tooltip and .predictor-info-popup — shared with every
   other "?" explainer (Predictor, PR-corr, Luck Percentile) so all
   four use identical box styling and theme tokens. */

#whatif-body {
    margin-top: var(--space-sm);
    padding: var(--space-md);
    background: repeating-linear-gradient(
        135deg,
        var(--color-surface),
        var(--color-surface) 10px,
        var(--color-bg) 10px,
        var(--color-bg) 20px
    );
    border: 1px dashed var(--color-border);
    border-radius: var(--radius-md);
}

/* "Run from" baseline picker — reuses .dash-controls chrome (see B2) and the
   shared .snap-nav stepper, so it is the same control as the Table panel's.
   Sits above the Player A/B picker inside #whatif-body. */
.whatif-baseline[hidden] { display: none; }

.whatif-picker {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    flex-wrap: wrap;
}

/* Layout only — the box is canonical in .app-search-input (layout.css).

   120px, not 140px: "A vs B" is one thought, and at 140px the pair needed a
   339px-wide picker to stay on one line — which a 430px phone clears but a
   390px one does not, so the same scenario read as a pair on one handset and
   as two stacked fields on another. 120px brings that down to ~300px, so every
   phone from 390px up keeps them side by side; below that they stack, which is
   the honest outcome at that width.

   Not lower than 120px: once a player is picked, the field's own flag and title
   badges take ~85px of its inside (measured: 32px left + 53px right for a
   two-badge name), and a 100px field would leave almost nothing for the name
   itself. */
.whatif-input {
    flex: 1;
    min-width: 120px;
}

/* Custom combobox (works on mobile, unlike native <datalist>) */
.whatif-combo {
    position: relative;
    flex: 1;
    min-width: 120px;
    display: flex;
}
.whatif-combo .whatif-input {
    width: 100%;
    min-width: 0;
}
/* The canonical search field wraps the input in .app-combo-wrap — make that
   wrapper fill the combo so the input keeps its full width. */
.whatif-combo .app-combo-wrap {
    flex: 1;
    min-width: 0;
}
.whatif-options {
    position: absolute;
    top: calc(100% + 2px);
    left: 0;
    right: 0;
    z-index: 30;
    margin: 0;
    padding: var(--space-2xs, 2px);
    list-style: none;
    max-height: 240px;
    overflow-y: auto;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
    -webkit-overflow-scrolling: touch;
}
.whatif-options[hidden] {
    display: none;
}
.whatif-option {
    padding: var(--space-xs) var(--space-sm);
    border-radius: var(--radius-sm);
    color: var(--color-text);
    font-size: var(--fs-090);
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.whatif-option:hover,
.whatif-option.active {
    background: var(--color-accent);
    color: var(--color-bg);
}

/* Flag + name (+ optional trailing PLAYED / NOT-PLAYED pill) on one row.
   `--flag` lays out every option; `--pill` only adds the Player-B badge. */
.whatif-option--flag,
.whatif-option--pill {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    white-space: normal;
}
.whatif-option-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.whatif-option--pill .whatif-played-badge,
.whatif-option--pill .whatif-unplayed-badge {
    flex-shrink: 0;
}

.whatif-vs {
    font-weight: 600;
    color: var(--color-text-muted);
    font-size: var(--fs-085);
}

.whatif-add-btn,
.whatif-run-btn,
.whatif-clear-btn {
    padding: var(--space-xs) var(--space-md);
    background: var(--color-surface);
    border: 1px solid var(--color-accent);
    border-radius: var(--radius-sm);
    color: var(--color-accent);
    cursor: pointer;
    font-size: var(--fs-088);
    font-weight: 600;
    transition: background 0.15s, color 0.15s;
}
.whatif-add-btn:hover,
.whatif-run-btn:hover,
.whatif-clear-btn:hover {
    background: var(--color-accent);
    color: var(--color-bg);
}
.whatif-run-btn:disabled {
    opacity: 0.5;
    cursor: wait;
}

.whatif-err {
    color: var(--color-danger, #ef4444);
    font-size: var(--fs-082);
}

.whatif-staged {
    margin: var(--space-md) 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
}

.whatif-empty {
    text-align: center;
    color: var(--color-text-muted);
    font-style: italic;
    padding: var(--space-md);
    font-size: var(--fs-088);
}

.whatif-row {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-xs) var(--space-sm);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-size: var(--fs-088);
    flex-wrap: wrap;
}
.whatif-row.was-played {
    background: var(--color-hover);
}

/* Holds the canonical .app-identity chip (flag · name · titles). The 80px floor
   keeps short names from collapsing the column; above it the chip is free to
   shrink, so a very long name ellipsises inside its own box instead of pushing
   the result buttons off the card. */
.whatif-row-player {
    font-weight: 600;
    min-width: 80px;
    max-width: 100%;
    overflow: hidden;
}
.whatif-vs-small {
    color: var(--color-text-muted);
    font-size: var(--fs-078);
}

.whatif-played-badge,
.whatif-unplayed-badge,
.whatif-won-badge,
.whatif-lost-badge,
.whatif-drew-badge {
    display: inline-block;
    padding: 1px 6px;
    border-radius: var(--radius-sm);
    font-size: var(--fs-068);
    font-weight: 700;
    letter-spacing: 0.5px;
}
.whatif-played-badge {
    background: rgba(34,197,94,0.15);
    color: var(--color-success, #22c55e);
}
/* Result pills (Player B, played fixtures) — themed win/loss tokens so they
   track every theme; bg derived from the same token via color-mix. */
.whatif-won-badge {
    background: color-mix(in srgb, var(--color-win) 15%, transparent);
    color: var(--color-win);
}
.whatif-lost-badge {
    background: color-mix(in srgb, var(--color-loss) 15%, transparent);
    color: var(--color-loss);
}
.whatif-unplayed-badge,
.whatif-drew-badge {
    background: rgba(148,163,184,0.15);
    color: var(--color-text-muted);
}

.whatif-result-group {
    display: inline-flex;
    gap: 2px;
    margin-left: auto;
}
.whatif-res {
    padding: 3px 10px;
    background: transparent;
    border: 1px solid var(--color-border);
    color: var(--color-text-muted);
    cursor: pointer;
    font-size: var(--fs-078);
    font-weight: 600;
}
.whatif-res:first-child { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.whatif-res:last-child { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
.whatif-res:hover { color: var(--color-text); }
.whatif-res.active {
    background: var(--color-accent);
    color: var(--color-bg);
    border-color: var(--color-accent);
}

.whatif-warn {
    color: var(--color-warning, #f59e0b);
    font-size: var(--fs-100);
}

.whatif-del {
    background: none;
    border: none;
    color: var(--color-text-muted);
    font-size: var(--fs-120);
    cursor: pointer;
    padding: 0 6px;
}
.whatif-del:hover { color: var(--color-danger, #ef4444); }

.whatif-actions {
    display: flex;
    gap: var(--space-sm);
    margin-top: var(--space-sm);
}

.whatif-output {
    position: relative;
    margin-top: var(--space-lg);
    padding: var(--space-md);
    border: 2px dashed var(--color-accent);
    border-radius: var(--radius-md);
    background: var(--color-surface);
}

.whatif-ribbon {
    display: inline-block;
    margin-bottom: var(--space-sm);
    padding: 3px 10px;
    background: var(--color-accent);
    color: var(--color-bg);
    border-radius: var(--radius-sm);
    font-size: var(--fs-072);
    font-weight: 700;
    letter-spacing: 1px;
    text-transform: uppercase;
}

/* Default state (nothing staged): the table is a plain projection of the
   league as it stands, so the ribbon drops the "simulation" emphasis. */
.whatif-ribbon.is-baseline {
    background: transparent;
    color: var(--color-text-muted);
    border: 1px solid var(--color-border);
}

.whatif-moe {
    font-size: var(--fs-082);
    color: var(--color-text-muted);
    margin-bottom: var(--space-sm);
    font-style: italic;
}

.whatif-table {
    font-style: italic;
}
.whatif-table thead th {
    white-space: nowrap;
}

/* The 45deg hatch keeps px stripes intentionally — pure decorative texture
   should not scale with font (would create moiré at fractional pixels). */
.whatif-pct-bar {
    position: relative;
    background:
        linear-gradient(to right, var(--bar-color, var(--tier-high)) var(--pct, 0%), transparent var(--pct, 0%)),
        repeating-linear-gradient(
            45deg,
            transparent,
            transparent 4px,
            rgba(255,255,255,0.06) 4px,
            rgba(255,255,255,0.06) 8px
        );
    border-radius: var(--radius-sm);
    padding: 0.15em 0.45em;
    font-weight: 600;
    font-size: inherit;
    text-align: left;
    min-width: 3em;
    font-style: italic;
}

.whatif-expand-btn {
    display: block;
    margin: var(--space-sm) auto var(--space-xl);
    padding: 0.3em 1.1em;
    font-size: var(--fs-085);
    font-family: var(--font-main);
    color: var(--color-text);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    cursor: pointer;
    transition: background 0.15s;
}
.whatif-expand-btn:hover {
    background: var(--color-hover);
}

/* ── Mobile: compact predictor & what-if bar padding (no min-width — iron rule 1) ──────── */
@media (max-width: 640px) {
    .predictor-pct-bar {
        padding: 0.15em 0.3em;
    }

    .whatif-pct-bar {
        padding: 0.1em 0.2em;
    }
}


/* --- Scroll wrappers — sticky # and Player columns (all league types, mobile + desktop) --- */
.predictor-scroll-wrap,
.whatif-scroll-wrap {
    --col1-w: 36px; /* fallback replaced by JS (iron rule 12) */
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    min-width: 0;
    box-shadow: var(--shadow-sm);
}
.predictor-scroll-wrap table,
.whatif-scroll-wrap table {
    table-layout: auto;
}
.predictor-scroll-wrap tbody td:nth-child(1),
.whatif-scroll-wrap tbody td:nth-child(1) {
    position: sticky;
    left: 0;
    background: var(--color-inset);
    z-index: 3;
}
.predictor-scroll-wrap tbody td:nth-child(2),
.whatif-scroll-wrap tbody td:nth-child(2) {
    position: sticky;
    left: var(--col1-w);
    background: var(--color-inset);
    z-index: 2;
}
/* Sticky header cells — use header background so sticky header row matches the rest of the thead */
.predictor-scroll-wrap thead th:nth-child(1),
.whatif-scroll-wrap thead th:nth-child(1) {
    position: sticky;
    left: 0;
    background: var(--header-bg);
    z-index: 4;
}
.predictor-scroll-wrap thead th:nth-child(2),
.whatif-scroll-wrap thead th:nth-child(2) {
    position: sticky;
    left: var(--col1-w);
    background: var(--header-bg);
    z-index: 3;
    box-shadow: inset 0 -1px 0 var(--color-border);
}

/* B3/B4 sticky col-2 boundary drop-shadow — Google-style, appears only on scroll */
.predictor-scroll-wrap.is-scrolled-x thead th:nth-child(2),
.whatif-scroll-wrap.is-scrolled-x thead th:nth-child(2) {
    box-shadow: inset 0 -1px 0 var(--color-border), 6px 0 8px -4px rgba(0, 0, 0, 0.18);
}
.predictor-scroll-wrap.is-scrolled-x tbody td:nth-child(2),
.whatif-scroll-wrap.is-scrolled-x tbody td:nth-child(2) {
    box-shadow: inset 0 -1px 0 var(--color-border), 6px 0 8px -4px rgba(0, 0, 0, 0.18);
}
.predictor-scroll-wrap.is-scrolled-x tbody tr:last-child td:nth-child(2),
.whatif-scroll-wrap.is-scrolled-x tbody tr:last-child td:nth-child(2) {
    box-shadow: 6px 0 8px -4px rgba(0, 0, 0, 0.18);
}

/* B3/B4 sticky cell hover override (sticky bg has higher specificity than .dash-table tr:hover td) */
.predictor-scroll-wrap tbody tr:hover td:nth-child(1),
.predictor-scroll-wrap tbody tr:hover td:nth-child(2),
.whatif-scroll-wrap tbody tr:hover td:nth-child(1),
.whatif-scroll-wrap tbody tr:hover td:nth-child(2) {
    background: var(--color-hover);
}

/* --- B5 Played Matches + B6 Rounds — Player A (sticky) | Player B (sticky) | Score | PR A | PR B | Luck A | Luck B | Date ---
   Both tables share the .rounds-scroll-wrap chrome (identical columns + sticky settings).
   B5 = every played match, most-recent-first, capped at 10 with Show-all.
   B6 = round-by-round with prev/next/all nav. */
/* --col1-w and --col2-w are written by JS after render (iron rule 12) */
.rounds-scroll-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    box-shadow: var(--shadow-sm);
}
.rounds-scroll-wrap table {
    table-layout: auto;
}
/* Player A — sticky col 1 */
.rounds-scroll-wrap tbody td:nth-child(1) {
    position: sticky;
    left: 0;
    background: var(--color-inset);
    z-index: 3;
}
.rounds-scroll-wrap thead th:nth-child(1) {
    position: sticky;
    left: 0;
    background: var(--header-bg);
    z-index: 4;
}
/* Player B — sticky col 2 */
.rounds-scroll-wrap tbody td:nth-child(2) {
    position: sticky;
    left: var(--col1-w);
    background: var(--color-inset);
    z-index: 3;
}
.rounds-scroll-wrap thead th:nth-child(2) {
    position: sticky;
    left: var(--col1-w);
    background: var(--header-bg);
    z-index: 4;
    box-shadow: inset 0 -1px 0 var(--color-border);
}

/* B5/B6 winner name green / loser name red — played rows only.
   Specificity (0,4,2) beats .player-name-link (0,1,0); the 600 link weight
   (from td.player-cell) and hover-underline are preserved. */
.rounds-scroll-wrap td.player-cell.result-win,
.rounds-scroll-wrap td.player-cell.result-win .player-name-link { color: var(--color-win); }
.rounds-scroll-wrap td.player-cell.result-loss,
.rounds-scroll-wrap td.player-cell.result-loss .player-name-link { color: var(--color-loss); }

/* B5/B6 sticky col-2 boundary drop-shadow — Google-style, appears only on scroll */
.rounds-scroll-wrap.is-scrolled-x thead th:nth-child(2) {
    box-shadow: inset 0 -1px 0 var(--color-border), 6px 0 8px -4px rgba(0, 0, 0, 0.18);
}
.rounds-scroll-wrap.is-scrolled-x tbody td:nth-child(2) {
    box-shadow: inset 0 -1px 0 var(--color-border), 6px 0 8px -4px rgba(0, 0, 0, 0.18);
}
.rounds-scroll-wrap.is-scrolled-x tbody tr:last-child td:nth-child(2) {
    box-shadow: 6px 0 8px -4px rgba(0, 0, 0, 0.18);
}

/* B5/B6 sticky cell hover override (sticky bg has higher specificity than .dash-table tr:hover td) */
.rounds-scroll-wrap tbody tr:hover td:nth-child(1),
.rounds-scroll-wrap tbody tr:hover td:nth-child(2) {
    background: var(--color-hover);
}

/* B5/B6 unplayed row — sticky cells must match the row tint (iron rule 11) */
.rounds-scroll-wrap tbody tr.unplayed-row td:nth-child(1),
.rounds-scroll-wrap tbody tr.unplayed-row td:nth-child(2) {
    background: var(--color-unplayed-bg);
}

/* B7 Remaining Matches sub-tabs use the shared accordion sub-tabs
   (.subtab--accordion / .subtab-panel in css/subtabs.css, via mountAccordionTabs). */

/* B7a — All Remaining. (Class stems keep the legacy .rem-b6* naming — see TABLE-DESIGN.md.)
   Deliberate per-table override of the MF shared "width:100%; max-width:1100px"
   wrapper cap: this table is full display width (no max-width) with columns
   stretched to fill, and is NOT sticky (Round column removed; only Player A / B). */
.rem-b6a-wrap {
    width: 100%;
    max-width: none;
    margin: 0 auto;
    overflow-x: auto;
    box-shadow: var(--shadow-sm);
}
/* Stretch the two columns to fill the full width (MF default sizes to content). */
.rem-b6a-wrap .dash-table {
    width: 100%;
    table-layout: fixed;
}
/* Override MF/dash sticky on the player cell — B7a is explicitly non-sticky. */
.rem-b6a-wrap .dash-table td.player-cell {
    position: static;
}

/* B7b — Remaining Report */
.rem-b6b-wrap {
    width: 60%;
    margin: 0 auto;
    overflow-x: auto;
    box-shadow: var(--shadow-sm);
    background: var(--color-surface);
}
.rem-b6b-wrap .player-remaining-table {
    margin-top: 0;
}

/* B7c — Per Player */
.rem-b6c-outer {
    display: flex;
    justify-content: center;
}
.rem-b6c-content {
    width: 60%;
}
.rem-b6c-wrap {
    width: 100%;
    overflow-x: auto;
    box-shadow: var(--shadow-sm);
    background: var(--color-surface);
}
.rem-b6c-search-row {
    margin-bottom: var(--space-sm);
}
/* Layout only — the box is canonical in .app-search-input (layout.css). This
   one also used --color-bg rather than the surface every other search field
   sits on; it now matches them. */
.rem-b6c-input {
    width: 100%;
}
.rem-b6c-header {
    margin-bottom: var(--space-xs);
    font-size: var(--fs-088);
    color: var(--color-text-muted);
}
.rem-b6c-player-name {
    font-weight: 700;
    color: var(--color-text);
}
.rem-b6c-rem-count {
    color: var(--color-accent);
}
.rem-b6c-export-row {
    display: flex;
    justify-content: flex-end;
    margin-bottom: var(--space-xs);
}
.rem-b6c-table {
    width: 100%;
}

/* Shared remaining table */
.player-remaining-table {
    margin-top: var(--space-sm);
}

.player-remaining-divider td {
    padding: 4px 12px !important;
    border-top: 2px dashed var(--color-border) !important;
    border-bottom: none !important;
    font-size: var(--fs-075);
    color: var(--color-text-muted);
    font-style: italic;
    text-align: center;
    background: transparent !important;
}
.player-remaining-divider--bold td {
    font-weight: 700 !important;
    color: var(--color-text) !important;
}



/* ── Platform credit footer ──────────────────────────────────────────── */
.platform-credit {
    margin-top: 56px;
    padding: 10px 0 14px;
    text-align: center;
    font-size: var(--fs-060);
    font-weight: 300;
    letter-spacing: 0.12em;
    color: color-mix(in srgb, var(--color-bg) 95%, var(--color-text) 5%);
    user-select: none;
    pointer-events: none;
}
.platform-credit + .platform-credit {
    margin-top: 0;
    padding-top: 0;
}

/* ── Platform footer bar (chess.com-style link row, themed) ──────────── */
.site-platform-footer {
    margin-top: 56px;
    padding: 14px 16px 18px;
    border-top: 1px solid color-mix(in srgb, var(--color-bg) 88%, var(--color-text) 12%);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 6px 12px;
    font-size: var(--fs-060);
    font-weight: 300;
    letter-spacing: 0.08em;
    color: color-mix(in srgb, var(--color-bg) 70%, var(--color-text) 30%);
    user-select: none;
}
/* Clickable transparency link — the ONE interactive item in the footer. */
.site-footer-link {
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    font: inherit;
    letter-spacing: inherit;
    color: color-mix(in srgb, var(--color-bg) 55%, var(--color-text) 45%);
    text-decoration: none;
    transition: color 0.15s ease;
}
.site-footer-link:hover,
.site-footer-link:focus-visible {
    color: var(--color-text);
    text-decoration: underline;
    text-underline-offset: 3px;
}
.site-footer-sep { opacity: 0.5; }
/* Non-interactive credit (© is the all-rights-reserved mark). */
.site-footer-credit { user-select: none; }

/* The What-If picker's bulk row ("Add all not played"). An ACTION sitting in a
   list of people, so it reads in the accent colour rather than as body text —
   the one visual cue that distinguishes it from the names below it. Everything
   else about the row is canonical: the same `.app-combo-option` frame, and the
   same `.sf-badge--unplayed` pill the individual NOT PLAYED rows already wear,
   here carrying the count it will stage. */
.whatif-bulk-name {
    color: var(--color-accent);
    font-weight: 600;
}

/* Title Race point stepper. Sits IMMEDIATELY under the X axis, above the detail
   panel it drives (the DOM move is in renderTitleRace) — the panel grows with
   the number of plotted players, and below it the buttons drifted away from the
   axis they scrub.

   Centred rather than left-aligned: the Iron Rule's left-align governs table
   text, not a symmetric pair of steppers around a readout. Tight margins on
   both sides so the bar reads as part of the chart rather than as a control
   block of its own. */
.titlerace-step {
    justify-content: center;
    gap: var(--space-xs);
    margin-top: 2px;
    margin-bottom: var(--space-xs);
}
/* Deliberately BELOW the 44px both platforms publish as their touch minimum.
   Sitting hard against the axis, this is the only tap target in that band —
   nothing adjacent to hit by mistake — and the bar should read as part of the
   chart, not as a control block competing with it. 28px is still more than 20x
   the ~1.3px a single point occupies on a phone, and THAT is the comparison
   that matters: this exists to replace an impossible target, not to be a large
   one. */
.titlerace-step button {
    min-width: 28px;
    min-height: 28px;
    padding: 0;
    font-size: var(--fs-090);
    line-height: 1;
}
.titlerace-step button:disabled {
    opacity: 0.4;
    cursor: default;
}
/* The readout is a quiet caption under the axis, not a label competing with it.
   `min-width` still reserves the widest reading ("300 / 301") so the arrows hold
   still while stepping instead of shuffling a pixel per digit. */
.titlerace-step .round-label {
    min-width: 5em;
    /* --fs-085, not the smaller --fs-075. The root font on a 427px phone is
       13.28px, not 16, so the tokens compute far below their nominal rem: 075
       lands at 8.65px there, which is past reading and into decoration. 085
       gives 9.8px — still a caption, still legible. Measured, not assumed;
       picking by token name alone is what hides this. */
    font-size: var(--fs-085);
    font-weight: 500;
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
}
