/* ========================================
   FONT FACES
   ----------------------------------------
   Noto Sans (body) + JetBrains Mono (numbers), latin + latin-ext subsets.
   Variable axes mean one file covers weights 400→800 (Noto) / 400→600
   (JetBrains). Unicode-ranges let the browser skip the larger latin-ext
   file until an athlete name actually needs accented characters.
   ======================================== */
@font-face {
    font-family: 'Noto Sans';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('../fonts/noto-sans-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
        U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
        U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'Noto Sans';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('../fonts/noto-sans-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
        U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
        U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
        U+A720-A7FF;
}
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 100 800;
    font-display: swap;
    src: url('../fonts/jetbrains-mono-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
        U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
        U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 100 800;
    font-display: swap;
    src: url('../fonts/jetbrains-mono-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
        U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
        U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
        U+A720-A7FF;
}

/* ========================================
   DESIGN TOKENS
   ----------------------------------------
   One source of truth. Change a token here → every page updates.
   Mobile values are the baseline; the @media block at the bottom of this
   section bumps the type scale on desktop in one place rather than per
   selector. Spacing/colour/radius/shadow tokens are viewport-invariant.
   ======================================== */
:root {
    /* ── Text size follows the reader (27 Sep 2026) ──
       Every type token is its design size in px times --u, the reader's text
       scale: 1/16 of the root, which the browser sets from its text-size
       setting (and iOS from Dynamic Type, below). At default settings every
       token is exactly its old px value. Each is CAPPED at 1.5x: past that the
       layouts, not the text, are what break (docs/TODO.md, "Large text"), and
       browser zoom, which scales everything together, is the way to go bigger.
       Chart text has its own lower cap: --chart-* below. */
    /* x the ☰ menu's Text size (--text-user, set in index.html's first script) */
    --u: calc(1rem / 16 * var(--text-user, 1));

    /* ── Type scale ────────────────────────
       Modular-ish 1.18 ratio. Mobile values; desktop scales below. */
    --text-nano:    min(calc(8 * var(--u)), 12px);    /* the smallest marks: sort arrows, tier tags, forecast leg labels */
    --text-micro:   min(calc(9 * var(--u)), 13.5px);    /* badge eyebrows, leg-num tags */
    --text-xs:     min(calc(10 * var(--u)), 15px);    /* axis ticks, ap-rd, ap-of */
    --text-sm:     min(calc(11 * var(--u)), 16.5px);    /* meta, captions */
    --text-base:   min(calc(12 * var(--u)), 18px);    /* body in compact contexts (cards) */
    --text-md:     min(calc(13 * var(--u)), 19.5px);    /* body, race-row titles */
    --text-lg:     min(calc(15 * var(--u)), 22.5px);    /* section titles in cards */
    --text-xl:     min(calc(18 * var(--u)), 27px);    /* page subhead, athlete name */
    --text-2xl:    min(calc(22 * var(--u)), 33px);    /* stat val */
    --text-3xl:    min(calc(28 * var(--u)), 42px);    /* big stat */
    --text-display: min(calc(36 * var(--u)), 54px);   /* hero number (h2h score) */

    /* Uppercase eyebrow used by every "section title" in the app —
       distinguishes structural headings from value labels. */
    --text-eyebrow:  min(calc(10 * var(--u)), 15px);
    /* iOS Safari zooms the page when a text field under 16px is tapped, so
       touch-screen text inputs are exactly this, at every width. */
    --text-input:    max(16px, min(calc(16 * var(--u)), 24px));

    /* Chart text: the same design sizes, capped at 1.25x on a phone instead of
       1.5x. A phone's charts are fixed geometry in 360px (a podium's boxes, an
       axis's width, a map's labels) and past ~1.25x their text collides with
       itself. A desktop has the room, so there they keep up with the page at
       1.5x (the desktop block below; her call, 27 Sep 2026). Use these for text
       drawn INSIDE a chart (SVG), the page tokens for everything around it. */
    --chart-text-nano: min(calc(8 * var(--u)), 10px);
    --chart-text-micro: min(calc(9 * var(--u)), 11.25px);
    --chart-text-xs: min(calc(10 * var(--u)), 12.5px);
    --chart-text-sm: min(calc(11 * var(--u)), 13.75px);
    --chart-text-base: min(calc(12 * var(--u)), 15px);
    --chart-text-md: min(calc(13 * var(--u)), 16.25px);
    --chart-text-lg: min(calc(15 * var(--u)), 18.75px);
    --chart-text-xl: min(calc(18 * var(--u)), 22.5px);

    --line-tight: 1.25;
    --line-body:  1.5;

    /* ── Type family ───────────────────────
       Noto Sans for body, JetBrains Mono for any tabular number. */
    --font-family: 'Noto Sans', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
    --font-family-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
    --font-weight-normal: 400;
    --font-weight-medium: 500;
    --font-weight-semibold: 600;
    --font-weight-bold: 700;
    --font-weight-black: 800;

    /* Legacy aliases kept so old rules still work while we migrate. */

    /* ── Greys (ink + surface) ──────────────
       7-step ink ramp roughly matches Tailwind's slate but named by use. */

    /* Underline under a tappable athlete name. Derived from the name's OWN ink so
       it works on every surface and in dark mode, but held well back: a full-ink
       underline under every name in a results list striped the page. */
    --link-underline: color-mix(in srgb, currentColor 30%, transparent);
    /* Rim drawn around every flag image. Deliberately a mid grey, not a tinted
       black: it has to stay visible against a white flag edge on a light surface
       AND a black flag edge on a dark one. */
    --flag-ring:     #94a3b8;


    /* ── Brand + semantic ──────────────────
       --primary-color and its variants are NOT set here. tokens.css loads last
       and is the single source for the brand accent; setting a value here would
       shadow-mismatch the live theme. (This note used to point at
       saas-theme.css, which was folded into this file, and at CT.bands for the
       discipline colours, which are now the --seg-* tokens.) */


    /* Discipline colours, and the medal trio that used to sit here, are defined
       in tokens.css — the single source. Both notes above them there. */

    /* ── Spacing (px) ──────────────────────
       Geometric-ish ladder. Existing rem-based --spacing-* kept as aliases. */
    --space-1:  2px;
    --space-2:  4px;
    --space-3:  6px;
    --space-4:  8px;
    --space-5: 10px;
    --space-6: 12px;
    --space-7: 14px;
    --space-8: 16px;
    --space-10: 20px;
    --space-12: 24px;
    --space-16: 32px;

    --spacing-lg: var(--space-12);

    /* Radius, shadow and --bg-app are NOT defined here. css/tokens.css defines
       them and loads last, so a copy in this file is inert. Five of these had
       silently drifted apart (--radius-lg read 8px here while the app rendered
       tokens.css's 10px, and all three shadows carried different rgba). Removed
       2026-09-03; edit css/tokens.css. */


    /* ── Chip primitive ──────────────────────
       One geometry for every toggle chip in the app (discipline readout,
       distance filter, country chip, preset chip, gap mode). Padding sized
       so the chip clears the 28px finger-friendly floor without becoming a
       button. A chip NARROWS a list; a group of mutually exclusive lenses is
       a switcher (below), not a row of chips. */
    --chip-py:     4px;
    --chip-px:    10px;
    --chip-radius: var(--radius-pill);
    --chip-text:  var(--text-sm);
    --chip-min-h: 28px;

    /* ── Tab primitive ───────────────────────
       Page-level tabs (race tabs, prediction view). Underline-marked, never
       filled — the fill is the switcher's, and keeping them distinct is what
       tells you whether a control changes the panel or the whole page. */
    --tab-py:      var(--space-3);
    --tab-px:      var(--space-5);
    --tab-gap:     var(--space-2);
    --tab-text:    var(--text-md);
    --tab-min-h:   36px;
    --tab-ink:     var(--text-muted);
    --tab-on-ink:  var(--text);
    --tab-on-line: var(--accent);

    /* ── Switcher primitive ──────────────────
       One geometry for every "pick exactly one lens" control: relay Teams /
       Athletes + leg filter, summary and profile units, T100 gender, replay
       Course / Timeline, H2H metric + detail modes, Elite / U23, race-history
       division / distance / trend. These used to be four different looks
       (joined rings at two radii, loose pills, an iOS-style padded track)
       doing the same job — one primitive so a switcher is recognisable
       wherever it appears. Active is the same neutral dark fill as .chip and
       .ctrl-pill, which keeps --accent free for the watch-list selection. */
    --switch-py:      5px;
    --switch-px:      var(--space-5);
    --switch-radius:  var(--radius-md);
    --switch-text:    var(--text-sm);
    --switch-min-h:   32px;
    --switch-bg:      var(--surface);
    --switch-ink:     var(--text-muted);
    --switch-on-bg:   var(--text);
    --switch-on-ink:  var(--surface);

    /* ── Motion ────────────────────────────  */
    /* ── Breakpoints ─────────────────────────────────────────────────────
       Custom properties do not work inside a media query, so this comment is
       the source of truth and the numbers below are written out by hand. There
       were five accidental secondary widths (480 / 520 / 540 / 620 / 640); the
       ladder is four DELIBERATE steps now, each with a stated job:

         max-width  480px  phone — tighten, stack, drop to one column
         min-width  520px  chart step (chart-theme.css only): tick + label type
         max-width  640px  narrow — hide a column, swap long labels for short
         min-width  769px  THE primary boundary; 77 of the file's 91 queries

       Anything else is drift. Prefer 769px unless the rule is specifically
       about running out of horizontal room before then. */

    --transition-fast:   0.15s ease;
    --transition-normal: 0.25s ease;

    /* ── Layout ────────────────────────────  */
    --container-max-width: 1400px;
}

/* Desktop scale-up — bump the whole type ladder at one breakpoint. Spacing,
   colours, radii intentionally don't change here; if a specific layout
   needs to widen on desktop it does so in its own @media block. */
@media (min-width: 769px) {
    :root {
        --text-nano:     min(calc(9 * var(--u)), 13.5px);
        --text-micro:   min(calc(10 * var(--u)), 15px);
        --text-xs:      min(calc(11 * var(--u)), 16.5px);
        --text-sm:      min(calc(12 * var(--u)), 18px);
        --text-base:    min(calc(13 * var(--u)), 19.5px);
        --text-md:      min(calc(15 * var(--u)), 22.5px);
        --text-lg:      min(calc(17 * var(--u)), 25.5px);
        --text-xl:      min(calc(22 * var(--u)), 33px);
        --text-2xl:     min(calc(28 * var(--u)), 42px);
        --text-3xl:     min(calc(34 * var(--u)), 51px);
        --text-display: min(calc(44 * var(--u)), 66px);
        --text-eyebrow: min(calc(11 * var(--u)), 16.5px);
        --chart-text-nano: min(calc(9 * var(--u)), 13.5px);
        --chart-text-micro: min(calc(10 * var(--u)), 15px);
        --chart-text-xs: min(calc(11 * var(--u)), 16.5px);
        --chart-text-sm: min(calc(12 * var(--u)), 18px);
        --chart-text-base: min(calc(13 * var(--u)), 19.5px);
        --chart-text-md: min(calc(15 * var(--u)), 22.5px);
        --chart-text-lg: min(calc(17 * var(--u)), 25.5px);
        --chart-text-xl: min(calc(22 * var(--u)), 33px);
    }
}

/* Larger desktops get a final small bump. */
@media (min-width: 1200px) {
    :root {
        --text-lg:      min(calc(18 * var(--u)), 27px);
        --text-xl:      min(calc(24 * var(--u)), 36px);
        --text-2xl:     min(calc(30 * var(--u)), 45px);
        --chart-text-lg: min(calc(18 * var(--u)), 27px);
        --chart-text-xl: min(calc(24 * var(--u)), 36px);
    }
}

/* ========================================
   RESET + BODY
   ======================================== */
* { margin: 0; padding: 0; box-sizing: border-box; }

html {
    /* Stop iOS Safari auto-inflating font sizes on some blocks (it made long
       race titles render huge on iPhone, but not in any desktop emulator). */
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}
/* Anything scrolled into view (standings rows, a share-card's focused row)
   has to clear the pinned header. One scrollport-level rule beats an offset
   at every scrollIntoView call site. --header-h is measured in main.js. */
html { scroll-padding-top: var(--header-h, 56px); }

/* iPhone and iPad: the root takes the reader's Dynamic Type size (Settings >
   Display & Brightness > Text Size), whose default body is 17px, so the unit is
   1/17 of it and the default renders exactly as before. -webkit-touch-callout
   exists only on iOS: desktop Safari also knows -apple-system-body, but its
   body style is 13px, which would shrink everything. body re-sets the family
   and line height, so only the size is taken from here. */
@supports (font: -apple-system-body) and (-webkit-touch-callout: none) {
    html { font: -apple-system-body; }
    :root { --u: calc(1rem / 17 * var(--text-user, 1)); }
}

body {
    font-family: var(--font-family);
    font-size: var(--text-md);
    line-height: var(--line-body);
    color: var(--text);
    min-height: 100vh;
    padding: var(--space-4);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    /* folded up from the SaaS layer 2026-09-03 */
    background: var(--bg-app);
}

@media (max-width: 768px) {
    body { padding: var(--space-2); }
}

h1 { display: none; }

/* Any node that opts into tabular numbers also opts into the mono face —
   keeps tables (sub-splits, leaderboards, time deltas) aligned cleanly. */
[style*="tabular-nums"],
.podium-time, .podium-paces, .ms-sub, .ms-val, .seg-time, .ap-rtot, .podium-stagger,
[class*="time-cell"], [class*="pace-cell"] {
    font-family: var(--font-family-mono);
}

/* Discipline label tag — colored typography that pairs with chart legends. */
.disc-tag {
    font-weight: var(--font-weight-bold);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 0.92em;
}

/* ========================================
   LAYOUT
   ======================================== */
.container {
    max-width: var(--container-max-width);
    margin: 0 auto;
    background: var(--surface);
    padding: var(--space-6);
    /* folded up from the SaaS layer 2026-09-03: the page shell is a hairline
       border, not a floating card. The --radius-lg that used to sit in the
       max-width:768 rule below was dead -- this unconditional rule came later
       and always won -- so mobile was already --radius-xl. */
    box-shadow: none;
    border: 1px solid var(--border);
    border-radius: var(--radius-xl);
}
@media (max-width: 768px) {
    .container { padding: var(--space-3); }
}

/* Top-level content sections share one centred max-width so on wide
   monitors header, tabs, and chart content stay aligned in a readable
   column instead of stretching edge-to-edge. */
#resultsSection,
#athleteProfileSection,
#raceHistorySection,
#predictionSection,
#classFormSection {
    max-width: var(--container-max-width);
    margin-left: auto;
    margin-right: auto;
}

.page-content { padding: var(--space-4); background: var(--surface); border-radius: var(--radius-lg); }
@media (max-width: 768px) { .page-content { padding: var(--space-3); } }

/* ========================================
   LOADING / ERROR
   ======================================== */
.loading { display: none; text-align: center; padding: var(--spacing-lg); }
.loading.active { display: block; }
.spinner {
    border: 4px solid var(--surface-tint);
    border-top: 4px solid var(--accent);
    border-radius: 50%; width: 40px; height: 40px;
    animation: spin 1s linear infinite; margin: 0 auto;
}
@keyframes spin { 0% { transform: rotate(0); } 100% { transform: rotate(360deg); } }

.error-message {
    color: var(--danger);
    margin-top: var(--space-5);
    padding: var(--space-5);
    background: var(--danger-bg);
    border-radius: var(--radius-lg);
    display: none;
    font-size: var(--text-md);
}

/* ========================================
   SHARED PRIMITIVES
   ----------------------------------------
   Reused across athlete-profile, race-history, head-to-head, summary.
   Namespace classes below (.ap-*, .rh-*, .h2h-*) inherit these via
   token references so they all look like the same component family.
   ======================================== */

/* ── Athlete name → profile: the app-wide name cue ──
   One rule for every name that opens a profile, wherever it renders (see
   js/ui/athlete-link.js). A thin SOLID underline reads as a link; a dotted one
   read like an <abbr> tooltip cue. The name keeps its surrounding ink, weight
   and size rather than turning into a coloured link, so a results table still
   reads as a table.

   The counterpart rule is the absence of this class: a name that is a selection
   control (rank-chart labels, splits-table rows) carries no underline, because
   tapping it watches the athlete rather than navigating.

   It is an INLINE span, never a button: a button is an atomic inline box, so any
   cell that truncates its own text (the standings name column, the spider name
   cell) would swallow the whole name into a bare "…". See js/ui/athlete-link.js. */
.athlete-link {
    color: inherit; cursor: pointer;
    text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px;
    text-decoration-color: currentColor;                /* fallback */
    text-decoration-color: var(--link-underline);
}
.athlete-link:hover { color: var(--accent); }
.athlete-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
/* SVG chart labels opt into the same cue directly. */
text.athlete-link { cursor: pointer; text-decoration: underline; text-decoration-color: var(--link-underline); }

/* Section card: a panel grouping related content. Composes .card for the
   surface (--surface + 1px --border + --radius-lg) and keeps only its own box
   metrics. Folding the SaaS layer up on 2026-09-03 showed that base+override
   together WERE exactly the card triple, split across two rules 3,500 lines
   apart -- which is why no counter had ever seen it. */
.ap-section {
    margin-bottom: var(--space-7);
    padding: var(--space-5);
}
@media (min-width: 769px) {
    .ap-section { padding: var(--space-8); }
}

/* ── Section title (eyebrow + optional sub) ── */
.ap-section-title,
.summary-section-title,
.settings-section-title {
    font-size: var(--text-eyebrow);
    font-weight: var(--font-weight-bold);
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: var(--space-4);
    display: flex; align-items: center; gap: var(--space-3);
}
@media (min-width: 769px) {
    .ap-section-title,
    .summary-section-title, .settings-section-title { font-size: var(--text-sm); }
}

/* ── Chart host inside a section card ── */
.ap-chart-host,
.xo-chart-host {
    background: var(--surface);
    border-radius: var(--radius-md);
    padding: var(--space-2);
    border: 1px solid var(--border-soft);
    overflow: hidden;
}
.ap-chart-host svg,
.xo-chart-host svg { display: block; max-width: 100%; }
/* #apSegmentChart is filled after an async fetch (ability tracks wait on
   getAbility()). Empty it is ~10px tall and full it is ~545px, so everything
   below it — Best at each tier, Race history — jumped half a screen once the
   data landed, which is why it was reported as a layout-shift source. Reserve
   a lane stack's worth for the duration of the await; the reservation is
   released the moment content arrives. Sized to the common 3-lane, non-split
   stack, so the usual case settles within a few px.
   ONLY .ap-chart-host: this host always ends up filled (the no-data path
   writes .ap-empty into it). #xoTimeline is a .xo-chart-host that legitimately
   stays empty when an athlete has no crossover, and reserving there would
   leave a permanent blank box. */
.ap-chart-host:empty { min-height: 540px; }
@media (min-width: 769px) { .ap-chart-host:empty { min-height: 640px; } }

/* ── Empty state ── */
.ap-empty,
.spider-empty {
    padding: var(--space-10);
    text-align: center;
    color: var(--text-faint);
    font-size: var(--text-base);
}
@media (min-width: 769px) {
    .ap-empty, .spider-empty { font-size: var(--text-md); }
}

/* ── Back control — ONE canonical style, used app-wide (.btn-back) ── */
.btn-back {
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: var(--space-3) var(--space-5);
    font-size: var(--text-base);
    color: var(--text-muted);
    cursor: pointer;
    margin-bottom: var(--space-5);
    transition: background var(--transition-fast), border-color var(--transition-fast);
}
.btn-back:hover {
    background: var(--surface-tint);
    border-color: var(--text-faint);
}
/* One Back on screen, never two. The pinned header carries it whenever the nav
   stack has somewhere to go, so a view's own control sits a few pixels under an
   identical one; it stands down there. Set from _updateBackAffordances().
   .ab-back is exempt: it steps between panes INSIDE the athlete browser, which
   is not the journey the header button walks. */
:root.nav-has-back .btn-back:not(.ab-back) { display: none; }

/* ── Pill: outlined chip used for tags, filters, status ── */
.ap-pos, .cond-chip,
.rh-leg, .ap-legend-chip, .rh-rec-year {
    font-variant-numeric: tabular-nums;
}

/* ========================================
   APP HEADER
   ======================================== */
.app-header {
    /* Pinned. Every global affordance lives in here (brand -> home, the WT /
       NCAA / Athlete browsers, the breadcrumb), and a profile runs to ~8700px
       on a phone, so a static header meant no navigation on screen at all
       after the first swipe. Session traces showed people walking Back to the
       landing just to reach the search they already knew about. */
    position: sticky;
    top: 0;
    z-index: var(--z-header);
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-4);
    /* Brand + the ⓘ/☰ control pair anchor the top line; the data-source (browse
       buttons + breadcrumb) wraps within its own column between them rather than
       pushing the pair onto a second row. The pair lives in .app-menu-wrap and
       sits flush in the top-right corner, so no right pad is reserved. */
    padding: var(--space-3) 0;
    flex-wrap: nowrap;
    margin-bottom: var(--space-2);
    max-width: var(--container-max-width);
    margin-left: auto;
    margin-right: auto;
    width: 100%;
}
/* The pinned bar needs an opaque ground that reaches the shell's edges, or
   content scrolls visibly through the container's padding gutter beside and
   above it. A pseudo-element does the bleed so the header's own box model
   (and its centred max-width) stays untouched. z-index:-1 keeps it behind the
   header's own children while the whole stacking context still sits at
   --z-header, above the page. */
.app-header::before {
    content: '';
    position: absolute;
    top: calc(-1 * var(--space-6));
    bottom: 0;
    left: calc(-1 * var(--space-6));
    right: calc(-1 * var(--space-6));
    z-index: -1;
    pointer-events: none;
    background: var(--surface);
    border-bottom: 1px solid transparent;
    transition: border-color var(--transition-fast);
}
/* Hairline only once it's actually pinned — at rest the header should still
   read as part of the page, not as a toolbar bolted to it. */
.app-header.is-pinned::before { border-bottom-color: var(--border); }
@media (max-width: 768px) {
    .app-header::before {
        top: calc(-1 * var(--space-3));
        left: calc(-1 * var(--space-3));
        right: calc(-1 * var(--space-3));
    }
}

.app-brand {
    font-size: var(--text-lg);
    color: var(--text-2);
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    gap: 0.38em;
    /* folded up from the SaaS layer 2026-09-03 */
    font-weight: 700;
    letter-spacing: -0.01em;
}
.app-brand-mark { height: 0.92em; width: auto; flex: 0 0 auto; }
@media (min-width: 769px) { .app-brand { font-size: var(--text-xl); } }
@media (min-width: 1200px) { .app-brand { font-size: var(--text-2xl); } }

.app-data-source {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    font-size: var(--text-sm);
    /* folded up from the SaaS layer 2026-09-03 */
    gap: var(--space-3);
}
.app-header.has-data .app-data-source { font-size: var(--text-xs); }
@media (min-width: 769px) {
    .app-data-source { font-size: var(--text-md); }
    .app-header.has-data .app-data-source { font-size: var(--text-sm); }
}

/* ── App menu (☰): always-accessible home for app-level prefs (theme, units)
   and the CSV utility. Race-specific config stays in the Setup tab. ── */
/* The ⓘ help + ☰ menu form one aligned control pair, flush top-right. The ☰
   stays put whether or not the ⓘ is showing (it's per-tab), so the corner
   anchor never shifts. */
.app-menu-wrap { position: relative; margin-left: auto; flex: 0 0 auto; margin-top: var(--space-1);
    display: inline-flex; align-items: center; gap: var(--space-2); }
.app-menu-btn {
    display: inline-flex; align-items: center; justify-content: center;
    position: relative;
    width: 28px; height: 28px; padding: 0;
    border: 1px solid var(--border-strong); border-radius: var(--radius-md);
    background: var(--surface); color: var(--text-2); cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}
.app-menu-btn:hover { background: var(--surface-hover); border-color: var(--text-faint); color: var(--text); }
.app-menu-btn[aria-expanded="true"] { background: var(--surface-hover); border-color: var(--accent); color: var(--accent); }
.app-menu {
    position: absolute; top: calc(100% + var(--space-2)); right: 0; z-index: var(--z-menu);
    min-width: 244px; max-width: min(300px, calc(100vw - 24px));
    background: var(--surface-raised); border: 1px solid var(--border);
    border-radius: var(--radius-lg); box-shadow: var(--shadow-3);
    padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-5);
}
.app-menu[hidden] { display: none; }
.app-menu-sec { display: flex; flex-direction: column; gap: var(--space-3); }
.app-menu-sec + .app-menu-sec { border-top: 1px solid var(--border-soft); padding-top: var(--space-4); }
.app-menu-title {
    font-size: var(--text-xs); font-weight: var(--font-weight-bold);
    text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted);
}
.app-menu-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); }
.app-menu-label { font-size: var(--text-sm); color: var(--text-2); font-weight: var(--font-weight-medium); }
.app-menu-item {
    display: block;
    padding: var(--space-3) var(--space-4);
    color: var(--text-2);
    font-size: var(--text-sm);
    font-weight: var(--font-weight-semibold);
    cursor: pointer;
    text-align: center;
}
.app-menu-item:hover { background: var(--surface-hover); border-color: var(--text-faint); }
button.app-menu-item { width: 100%; font-family: inherit; }
/* .app-menu-sec is display:flex, which beats the hidden attribute's UA rule:
   the Browse section showed with the header's own buttons still on screen. */
.app-menu-sec[hidden] { display: none; }
.app-menu-sec[hidden] + .app-menu-sec { border-top: 0; padding-top: 0; }
/* Large text: the header's browse buttons would wrap, so they live in the ☰
   menu's Browse section instead (main.js _setupStickyHeader). Back stays. */
.app-header.hdr-compact #dataSourceArea > .header-btn:not(.header-btn--back),
.app-header.hdr-compact .header-sep { display: none; }
.app-menu-sub {
    background: none; border: none; padding: 0; cursor: pointer;
    font-size: var(--text-xs); color: var(--accent); text-align: left;
    font-weight: var(--font-weight-medium);
}
.app-menu-sub:hover { text-decoration: underline; }

/* Quiet surface button: the bg/border/color/weight live in the shared
   .header-btn group below; the accent-filled modal-footer
   variant lives at .src-modal-foot .header-btn. Base = geometry only. */
.header-btn {
    padding: var(--space-2) var(--space-5);
    border-radius: var(--radius-md);
    font-size: var(--text-sm);
    cursor: pointer;
    transition: background var(--transition-fast);
}
.header-btn:disabled { opacity: 0.4; cursor: default; }
/* Back sits with the source buttons rather than in the page, so the nav stack
   stays reachable from anywhere in a long view — and in the installed PWA,
   where there is no browser Back at all, from anywhere full stop. */
.header-btn--back { padding: var(--space-2) var(--space-6); font-weight: var(--font-weight-bold); line-height: 1; }
/* Back is added to the header only once the nav stack has depth, i.e. AFTER the
   race has loaded. Toggling it with `display` changed how many rows
   .app-data-source wrapped onto, which changed the height of a POSITION:STICKY
   bar, which moved the whole page under it — #dataSourceArea was the app's
   top reported layout-shift source. Reserving the slot from the start costs a
   hidden 29px box (invisible at >=360px, where nothing wrapped either way) and
   makes the header's height constant across the load. JS owns .is-hidden here,
   NOT `display`, so the box is always in flow. */
.header-btn--back.is-hidden { visibility: hidden; pointer-events: none; }
.app-header .header-btn { position: relative; }

/* ── Tap areas: 44px on a phone, without making the control bigger ──
   A small control keeps its look and gets an invisible area centred on it
   (min(0, 50% - 22px) grows a side only while the box is under 44). ONE list,
   so the rule is one place to read and to extend. An area only ever grows into
   EMPTY space: two areas that overlap send a tap to whichever sits on top,
   which is a worse bug than a small target. tests/tap-targets.spec.js measures
   what a tap actually hits, overlaps included.

   Full 44x44 where there is room around the control: */
.cs-clear::after {
    content: ''; position: absolute;
    top: min(0px, calc(50% - 22px)); bottom: min(0px, calc(50% - 22px));
    left: min(0px, calc(50% - 22px)); right: min(0px, calc(50% - 22px));
}
/* The pinned header: 44px TALL, and only as wide as the gaps allow. At 375px
   the brand and six controls need ~365px at 44 wide each and the row has
   ~353, so full width would wrap the pinned bar (doubling it over every page)
   or overlap the areas. Height is free: nothing sits above or below them. The
   ⓘ/☰ pair split the 2px gap between them rather than each reaching across
   it (each used to take -6px all round, so ☰ won taps aimed at ⓘ's edge). */
.app-header .header-btn::after,
.app-menu-btn::after,
.app-info-btn::after {
    content: ''; position: absolute;
    top: min(0px, calc(50% - 22px)); bottom: min(0px, calc(50% - 22px));
    left: 0; right: 0;
}
.app-menu-btn::after { left: -1px; }
.app-info-btn::after { right: -1px; }

/* ── The phone sweep (26 Sep 2026): every control on every surface ──
   Measured on 26 surfaces by the audit behind tests/tap-targets.spec.js. Three
   kinds of fix, in this order of preference:
     1. an invisible area, the control keeps its look;
     2. a visible 44px only for FORM FIELDS (and 16px text in them: below that
        iOS Safari zooms the whole page when one is tapped);
     3. names and race links inside tables and lists are EXEMPT and keep their
        row: 44px rows there doubled a table's length and read as a big visual
        change (the forecast's Next 10, rejected 26 Sep). Where such a row has
        one target the whole row is its area instead. */
@media (max-width: 768px) {
    /* 1a. Full 44x44 where the control stands clear of its neighbours. */
    .sc-close, .src-modal-close, .adjust-reset, .ap-fav-btn, .ap-map-link,
    .share-btn, .sc-style-prev, .sc-style-next, .btn-back,
    .privacy-note > summary { position: relative; }
    .sc-close::after, .src-modal-close::after, .adjust-reset::after, .ap-fav-btn::after, .ap-map-link::after,
    .share-btn::after, .sc-style-prev::after, .sc-style-next::after, .btn-back::after,
    .privacy-note > summary::after {
        content: ''; position: absolute;
        top: min(0px, calc(50% - 22px)); bottom: min(0px, calc(50% - 22px));
        left: min(0px, calc(50% - 22px)); right: min(0px, calc(50% - 22px));
    }
    /* 1b. Height only: controls that sit shoulder to shoulder in a row
       (pills, chips, checkboxes, the joined switcher segments), whose sideways
       areas would overlap the next one. */
    .ctrl-check, .ctrl-pill, .filter-options-btn, a.chip, button.chip, .share-cta,
    .sc-ghost, .sc-primary, .landing-primary, .app-menu-sub, .ap-relay-tog,
    .t100-scoring-summary, .src-modal-title-link, .para-class-open,
    .rh-sub .np-race-link, .switcher-btn, .np-show-all, .src-modal-foot .header-btn,
    .sc-modes button, .lf-race-gender { position: relative; }
    .ctrl-check::after, .ctrl-pill::after, .filter-options-btn::after, a.chip::after,
    button.chip::after, .share-cta::after, .sc-ghost::after, .sc-primary::after,
    .landing-primary::after, .app-menu-sub::after, .ap-relay-tog::after,
    .t100-scoring-summary::after, .src-modal-title-link::after, .para-class-open::after,
    .rh-sub .np-race-link::after, .switcher-btn::after, .np-show-all::after,
    .src-modal-foot .header-btn::after, .sc-modes button::after, .lf-race-gender::after,
    .header-file-name.clickable::before {
        content: ''; position: absolute; left: 0; right: 0;
        top: min(0px, calc(50% - 22px)); bottom: min(0px, calc(50% - 22px));
    }
    /* A checkbox label's area must not sit OVER its own box: the box is lifted
       above it, so a tap on the box hits the box and anywhere else on the label
       still toggles it. */
    .ctrl-check > input, .ap-relay-tog > input { position: relative; z-index: 1; }
    /* A switcher clipped its segments to its rounded ring (overflow: hidden),
       which clipped their areas too. The ring's corners go on the end segments
       instead, so each segment's area can reach above and below. */
    /* :not() lifts it over the base .switcher rule further down the file; the
       --scroll variant must keep scrolling, so it keeps its clip. */
    .switcher:not(.switcher--scroll) { overflow: visible; }
    .switcher-btn:first-child { border-radius: calc(var(--switch-radius) - 1px) 0 0 calc(var(--switch-radius) - 1px); }
    .switcher-btn:last-child { border-radius: 0 calc(var(--switch-radius) - 1px) calc(var(--switch-radius) - 1px) 0; }
    .switcher-btn:only-child { border-radius: calc(var(--switch-radius) - 1px); }
    /* The ⚙ Options row scrolls sideways, and a scroller clips; alone in it,
       Options needs no scrolling, so its row stops clipping it. */
    .filter-chip-row:not(:has(> :not(.filter-options-btn, .share-slot))) { overflow: visible; }
    .h2h-detail-close { position: relative; }
    .h2h-detail-close::after {
        content: ''; position: absolute;
        top: min(0px, calc(50% - 22px)); bottom: min(0px, calc(50% - 22px));
        left: min(0px, calc(50% - 22px)); right: min(0px, calc(50% - 22px));
    }
    .tab-btn { min-height: 44px; }
    /* The share sheet's layout arrows flank the dots; 44 wide, same glyph. */
    .sc-style-prev, .sc-style-next { min-width: 44px; min-height: 44px; }
    /* A modal's close sat in a 42px head, which cut its area short. */
    .src-modal-head { min-height: 44px; }
    /* Rows of controls closer than 44 centre to centre cut each other's areas
       short, so these open up by a few px: the ☰ menu's Theme/Units rows, the
       watch strip over the ⚙ Options row, a standings page's venue chip over
       its gender switcher. */
    .app-menu-sec { gap: var(--space-5); }
    /* The watch strip's chip areas reach 3px below it; 4px more space under it
       keeps the Options row's area whole. */
    .chip-strip { margin-bottom: var(--space-4); }
    /* The filter row sits in a clipping chart container: 3px more above it
       (the sweep had 6) is what its Options button's area needs. */
    div.inline-filters { padding-top: calc(var(--space-2) + 3px); }
    .t100-venue-link { margin-bottom: var(--space-3); }
    /* div. lifts it over the base .inline-filters rule further down. */
    /* The race title's ::after is its "›", so its area is ::before. */
    .header-file-name.clickable { position: relative; }
    /* The landing's race pills stay 32 and reach 44 invisibly (the list above);
       a round dropdown's select is the 44 over its pill. A relay row wraps
       under the genders, so the rows sit 44 apart centre to centre. */
    .lf-race-slot .lf-race-round { height: 44px; }
    div.lf-race-genders { row-gap: var(--space-6); }

    /* 27 Sep 2026, her call: the LOOK from before this sweep wins. Visible
       44s came back out (tab and segment widths, scrolling switchers, the
       watch strip, form fields); what could stay invisible stayed. The
       controls left under 44 are listed in tests/tap-targets.spec.js EXEMPT. */

    /* Sliders: the track stays 6px and the input grows to the 44px row. The
       forecast's rows are 42px apart track to track, so the row gap opens 2px
       and the areas meet without overlapping. */
    .adjust-range { height: 44px; margin: -14px 0; }
    .adjust { row-gap: calc(var(--space-5) + 2px); }
}

/* Phone: the pinned row has to stay ONE row — wrapping doubles its height,
   which is the whole thing we're buying back. Adding Back put the race view
   over at 375px (the race view also shows the ⓘ, which the landing hides), so
   the gaps and Back's own padding tighten below 430px. Verified one row at
   360 / 375 / 390 / 414 / 430 / 768. 320px still wraps to two, and wrapping is
   the right degradation there.
   NOTE the doubled .app-header and the descendant selector: the redesign
   section near the end of this file re-sets .app-data-source's gap at equal
   specificity, so bare selectors here lose on source order. */
@media (max-width: 430px) {
    .app-header.app-header { gap: var(--space-2); }
    .app-header .app-data-source { gap: var(--space-2); }
    .header-btn--back { padding: var(--space-2) var(--space-4); }
    .app-menu-wrap { gap: var(--space-1); }
}
@media (min-width: 769px) { .header-btn { font-size: var(--text-md); padding: var(--space-3) var(--space-7); } }

.header-sep { color: var(--text-faint); font-size: var(--text-xs); }
@media (min-width: 769px) { .header-sep { font-size: var(--text-md); } }
/* Lives at the top of the race view now, not in the pinned header — a race
   title wrapped to two lines there cost 26px of every screen for the whole
   scroll. It keeps its id and classes: several call sites read
   #fileName.textContent as the race label. */
.race-title-bar { display: flex; margin-bottom: var(--space-4); }
.header-file-name {
    font-size: var(--text-sm);
    color: var(--text-muted);
}
@media (min-width: 769px) { .header-file-name { font-size: var(--text-md); } }

/* When the loaded race has a history bucket, the filename becomes the entry
   point: link-styled (dotted underline), accent colour, pointer cursor. */
.header-file-name.clickable {
    cursor: pointer;
    color: var(--accent-ink);
    font-weight: var(--font-weight-semibold);
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-md);
    text-decoration: underline dotted;
    text-underline-offset: 3px;
    transition: background var(--transition-fast), color var(--transition-fast);
}
/* NBSP, not a plain space: on its own title line a long race name wraps, and a
   breaking space stranded the chevron alone on line two. */
.header-file-name.clickable::after { content: '\00a0›'; font-weight: 700; text-decoration: none; }
.header-file-name.clickable:hover,
.header-file-name.clickable:focus {
    background: var(--accent-subtle);
    color: var(--accent-hover);
    outline: none;
}
.header-file-name.clickable:focus-visible { box-shadow: 0 0 0 2px var(--focus-ring); }

/* ── Race-tier binding ────────────────────────────────────────────────────
   One class per tier, binding an element to that tier's tokens. This exists
   because a resolved colour copied into a JS record freezes at whatever theme
   it was built under: the profile's tier chips kept their light-mode ink in
   dark, and re-rendering could not fix it because the record still held the old
   hex. main.js says it plainly next to setupThemeRedraw — "colours that CAN
   live in CSS should, and then they need none of this".

     --tier-c   the hue, for a tint or a swatch
     --tier-i   the ink, for text and dots on the app surface
   Both follow the theme with no repaint at all. */
.tier-olympics { --tier-c: var(--tier-olympics); --tier-i: var(--tier-olympics-ink); }
.tier-world-champs { --tier-c: var(--tier-world-champs); --tier-i: var(--tier-world-champs-ink); }
.tier-wtcs-final { --tier-c: var(--tier-wtcs-final); --tier-i: var(--tier-wtcs-final-ink); }
.tier-wtcs { --tier-c: var(--tier-wtcs); --tier-i: var(--tier-wtcs-ink); }
.tier-t100 { --tier-c: var(--tier-t100); --tier-i: var(--tier-t100-ink); }
.tier-world-cup { --tier-c: var(--tier-world-cup); --tier-i: var(--tier-world-cup-ink); }
.tier-olympic-qual { --tier-c: var(--tier-olympic-qual); --tier-i: var(--tier-olympic-qual-ink); }
.tier-conti-champs { --tier-c: var(--tier-conti-champs); --tier-i: var(--tier-conti-champs-ink); }
.tier-conti-cup { --tier-c: var(--tier-conti-cup); --tier-i: var(--tier-conti-cup-ink); }
.tier-multisport { --tier-c: var(--tier-multisport); --tier-i: var(--tier-multisport-ink); }
.tier-regional-champs { --tier-c: var(--tier-regional-champs); --tier-i: var(--tier-regional-champs-ink); }
.tier-games { --tier-c: var(--tier-games); --tier-i: var(--tier-games-ink); }
.tier-junior-u23 { --tier-c: var(--tier-junior-u23); --tier-i: var(--tier-junior-u23-ink); }
.tier-para-series { --tier-c: var(--tier-para-series); --tier-i: var(--tier-para-series-ink); }
.tier-para-cup { --tier-c: var(--tier-para-cup); --tier-i: var(--tier-para-cup-ink); }
.tier-para-conti-cup { --tier-c: var(--tier-para-conti-cup); --tier-i: var(--tier-para-conti-cup-ink); }
.tier-ncaa-nationals { --tier-c: var(--tier-ncaa-nationals); --tier-i: var(--tier-ncaa-nationals-ink); }
.tier-ncaa-regionals { --tier-c: var(--tier-ncaa-regionals); --tier-i: var(--tier-ncaa-regionals-ink); }
.tier-ncaa-cup { --tier-c: var(--tier-ncaa-cup); --tier-i: var(--tier-ncaa-cup-ink); }
.tier-other { --tier-c: var(--tier-other); --tier-i: var(--tier-other-ink); }

/* Marks that carry a tier: the dot in the career-arc legend, and the dots and
   rung labels drawn into SVG. */
.tier-dot { background: var(--tier-i); }
.tier-fill { fill: var(--tier-i); }
.tier-stroke { stroke: var(--tier-i); }

/* ── Icon button: ONE box for every square/circular glyph control ──────────
   These were three independent definitions — a 22px ghost circle, a 28px
   bordered square and a 30px bordered square — so the same affordance read
   differently depending on which screen you were on. The geometry lives here
   once; an instance sets only what makes it itself, via the two custom
   properties. Bare glyph buttons (no box at all) are the group below. */
.btn--icon, .share-btn, .h2h-detail-close {
    display: inline-flex; align-items: center; justify-content: center;
    width: var(--icon-btn-size); height: var(--icon-btn-size);
    padding: 0; flex-shrink: 0;
    border: 1px solid transparent; border-radius: var(--icon-btn-radius);
    background: transparent; color: var(--text-faint);
    font-size: var(--text-sm); line-height: 1; cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}
.btn--icon { --icon-btn-size: 22px; --icon-btn-radius: 50%; }
.btn--icon:hover, .h2h-detail-close:hover { background: var(--surface-hover); color: var(--text-2); }

/* ── Bare glyph button: a control with no box, only the mark ──────────────
   Three of these existed independently (modal dismiss, share-sheet dismiss,
   remove-from-comparison). Size, placement and hover intent differ; nothing
   else does. */
.src-modal-close, .sc-close, .spider-remove-btn, .spider-expand-btn {
    background: none; border: 0; padding: 0; line-height: 1; cursor: pointer;
    color: var(--text-muted);
    transition: background var(--transition-fast), color var(--transition-fast);
}
/* ── CSV-format help (ⓘ next to "Upload CSV") ── */
.csv-help-tip {
    position: fixed; z-index: var(--z-tooltip);
    background: var(--surface); border: 1px solid var(--border-strong);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-3);
    padding: var(--space-6) var(--space-7);
    max-width: 380px; font-size: var(--text-base); color: var(--text-2);
}
.csv-help-title { font-weight: var(--font-weight-bold); font-size: var(--text-md); color: var(--text-2); margin-bottom: var(--space-3); }
.csv-help-body p { margin: var(--space-3) 0; }
.csv-help-body code {
    font-family: var(--font-family-mono);
    font-size: var(--text-sm); background: var(--surface-tint);
    padding: 1px var(--space-2); border-radius: var(--radius-sm);
}
.csv-help-body ul { margin: var(--space-3) 0 0 18px; padding: 0; }
.csv-help-body li { margin: var(--space-2) 0; }
.csv-help-note { color: var(--text-muted); }

/* ── App-level info button: the ⓘ half of the top-right control pair, sat
   just left of the ☰ menu inside .app-menu-wrap (not fixed) ── */
.app-info-btn {
    position: relative;
    width: 28px; height: 28px; font-size: var(--text-md);
}
.app-info-btn:hover { border-color: var(--text-faint); }
.app-info-btn.active { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
.app-info-tip {
    position: fixed; z-index: var(--z-tooltip);
    background: var(--surface-inverse); color: var(--on-surface-inverse);
    padding: var(--space-6) var(--space-7); border-radius: var(--radius-lg);
    font-size: var(--text-base); line-height: 1.6;
    max-width: 320px; min-width: 220px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
    max-height: 80vh; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
}
.app-info-tip strong { color: var(--warning); display: block; margin-bottom: var(--space-2); }
.app-info-tip ul { margin: var(--space-2) 0 0; padding-left: var(--space-8); }
.app-info-tip li { margin-bottom: var(--space-2); }
@media (min-width: 769px) {
    .app-info-tip { font-size: var(--text-md); max-width: 380px; }
}

/* ========================================
   TAB NAVIGATION
   ========================================
   `.tab-btn` is the PAGE-LEVEL tab: it switches the whole panel below it and
   there is at most one bar of them on screen (the race tabs, the prediction
   view's `.pvx-tabs`). Underline-marked, no fill — deliberately a different
   shape from `.switcher`, which picks a lens WITHIN one panel. Don't reach for
   `.tab-btn` for an in-panel toggle; that's what `.switcher` is for. */
.tab-navigation {
    display: flex;
    gap: var(--tab-gap);
    border-bottom: 1px solid var(--border);
    padding: 0;
    margin-bottom: var(--space-3);
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
.tab-navigation::-webkit-scrollbar { display: none; }
.tab-btn {
    flex-shrink: 0;
    padding: var(--tab-py) var(--tab-px);
    background: transparent;
    color: var(--tab-ink);
    border: none;
    border-radius: 0;
    border-bottom: 2px solid transparent;
    cursor: pointer;
    font-family: inherit;
    font-size: var(--tab-text);
    font-weight: var(--font-weight-medium);
    transition: color var(--transition-fast), border-color var(--transition-fast);
    white-space: nowrap;
    -webkit-tap-highlight-color: transparent;
    min-height: var(--tab-min-h);
}
.tab-btn:hover { background: transparent; color: var(--text-2); }
.tab-btn.active {
    background: transparent;
    color: var(--tab-on-ink);
    font-weight: var(--font-weight-semibold);
    border-bottom-color: var(--tab-on-line);
}
/* Tabs gated by state (e.g. Head-to-Head needs exactly 2 athletes selected). */
.tab-btn.hidden { display: none; }
/* A gated tab appears MID-SESSION, in a bar that scrolls on a phone — so it can
   arrive off-screen, or on-screen and simply unnoticed. Three indigo pulses say
   "this is new" without moving anything or stealing the tap. Applied by
   _revealTab in js/main.js and removed when the animation ends, so it re-fires
   every time the tab comes back. */
.tab-btn--flash {
    animation: tab-flash 1.2s ease-in-out 1;
    border-radius: var(--radius-sm);
}
/* The whole tab fills, rather than just the label changing colour — a filled
   block is what catches the eye in peripheral vision on a phone. The label flips
   to --on-accent so it stays readable at the peak. ONE pulse: it announces the
   tab without turning into a blinking thing beside the content. */
@keyframes tab-flash {
    0%, 100% { background: transparent; color: var(--tab-ink); }
    22%, 55% { background: var(--accent); color: var(--on-accent); }
}
@media (prefers-reduced-motion: reduce) {
    /* Still say it, just don't blink: hold the filled state. */
    .tab-btn--flash { animation: none; background: var(--accent); color: var(--on-accent); }
}
@media (min-width: 769px) {
    .tab-btn { font-size: var(--text-lg); padding: var(--space-5) var(--space-10); min-height: 42px; }
}
@media (max-width: 480px) {
    /* Tighten so the 7-tab bar fits a 390px viewport without crowding the edge;
       the 8th (Head-to-Head) tab still scrolls, with trailing room to hint it. */
    .tab-navigation { gap: 1px; padding-right: var(--space-3); scroll-padding-right: var(--space-3); }
    .tab-btn { padding: var(--space-2); font-size: var(--text-sm); min-height: 44px; }
}

/* ========================================
   CHART SECTIONS
   ======================================== */
.chart-section { display: none; }
.chart-section.active { display: block; }
.chart-container {
    margin: var(--space-2) 0; padding: var(--space-3);
    background: var(--surface-tint); border-radius: var(--radius-lg);
    display: none; width: 100%; overflow: hidden;
}
.chart-container.active { display: block; }
.chart-container svg { display: block; margin: 0 auto; max-width: 100%; height: auto; }
.chart-title {
    font-size: var(--text-md);
    color: var(--text);
    padding: var(--space-3) var(--space-5);
    text-align: center;
    font-weight: var(--font-weight-semibold);
    background: var(--surface);
    border-bottom: 1px solid var(--border);
}
@media (min-width: 769px) { .chart-title { font-size: var(--text-xl); padding: var(--space-4) var(--space-6); } }
@media (min-width: 1200px) { .chart-title { font-size: var(--text-2xl); } }
@media (max-width: 768px) {
    .chart-title { display: none; }
    .chart-container { padding: var(--space-1); margin: 0; }
}

/* Rank chart owns its scroll container — let it overflow its parent. */
#rankChart svg { max-width: none !important; }
#rankChart .scroll-container { -webkit-overflow-scrolling: touch; }
#rankChart foreignObject div { font-family: var(--font-family); }

/* SVG chart primitives (.axis, .grid, .athlete-…, .radar-…, spotlight) live in
   chart-theme.css — the single source. Don't re-add them here.
   NB: never write a comment-closer inside a comment. A CSS comment ends at the
   FIRST one, so the old wording (which spelled out .athlete-<star><slash>) closed
   here and silently swallowed the .sel-dot rule below. */
.sel-dot { transition: background var(--transition-fast); }

/* ========================================
   TOOLTIP
   ======================================== */
.tooltip {
    position: absolute; text-align: left;
    padding: var(--space-5); font-size: var(--text-xs);
    background: var(--scrim-tooltip); color: var(--on-surface-inverse);
    border-radius: var(--radius-md);
    pointer-events: none; opacity: 0; transition: opacity var(--transition-normal);
    z-index: var(--z-tooltip); max-width: 250px;
}
/* Set by TooltipManager's `center` option — a label hanging over a scatter
   point reads centred; one beside a table row does not. */
.tooltip-center { text-align: center; }
@media (min-width: 769px) {
    .tooltip { font-size: var(--text-md); padding: var(--space-6) var(--space-7); max-width: 320px; }
}
@media (max-width: 768px) {
    .tooltip { position: fixed !important; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.15); }
}

/* ========================================
   GENERIC BUTTON
   ======================================== */
.btn {
    padding: var(--space-3) var(--space-5);
    color: white;
    border: none;
    cursor: pointer;
    font-size: var(--text-md);
    transition: all var(--transition-normal);
    /* folded up from the SaaS layer 2026-09-03 */
    background: var(--accent);
    border-radius: var(--radius-md);
    font-weight: 600;
}
.btn:hover { background: var(--accent-hover); }
/* Start-list → prediction CTA: shared .btn appearance, full-width block. */
.ap-sl-pred-cta { display: block; width: 100%; margin-top: var(--space-8); font-weight: var(--font-weight-bold); }
/* The full-width save on a phone is the lg button (SYSTEM.md 6.2: 44); at the
   .btn default this one was ~26px, the "shallow" button at the foot of a start
   list (26 Sep 2026). */
@media (max-width: 768px) { .ap-sl-pred-cta { min-height: 44px; } }

/* ========================================
   SHARED CONTAINERS — the card primitive
   ======================================== */
/* One source for every bordered surface in the app.

   Before this existed, each feature grew its own (.rh-dd-card, .h2h-chart-card,
   .t100-drill-card, .filter-options-panel, ...): the SAME three declarations
   written eight times over. That is how two of them drifted to --radius-md while
   the other six sat at --radius-lg, and how one concept ended up carrying four
   different paddings. Nobody chose that; it is what happens when eight authors
   each pick plausibly. The token ratchet never caught it because every value WAS
   a good token -- the duplication was the bug, not the values.

   THE MIGRATION IS FINISHED (2026-09-03). It landed in two halves, and the first
   half on its own was a trap: the surfaces were converged onto one rule by
   listing every feature class as an ALIAS of `.card`. That removed the
   duplication but left nine names for one concept, and the markup still said
   `.h2h-chart-card` rather than `.card`, so nothing on screen announced that
   these were the same thing. A reader could not tell a real surface from a
   bespoke one, and the surface-budget counter could not either: it counts rules
   that RE-DERIVE the triple, and an alias re-derives nothing.

   Now the JS composes the primitive directly -- `class="card card--clip ..."` --
   and a feature class survives only where it carries something genuinely its own
   (a grid, a min-width, its own padding). `.h2h-detail-card`, `.t100-drill-card`
   and `.lv-card` had nothing of their own and are gone.

   A new panel composes `.card` and adds ONLY what is genuinely different about
   it. tests/style-surface-budget.spec.js counts bespoke container classes and
   fails on a new one, so this cannot silently re-accrete.

   design/SYSTEM.md principle 2 -- borders define surfaces, shadows mean
   "floats" -- so the base card is elevation 0 and every shadow is opt-in. */
.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
}

/* The traits. Each has TWO OR MORE call sites, which is the bar this file sets
   for having a modifier at all. A trait with one user stays a feature rule --
   see .rh-record-card's leading stripe, which is why there is no --marked here.
   There is no --float either: it resolved to --shadow-card, which tokens.css
   sets to `none`, so it painted nothing -- and design/SYSTEM.md section 5 puts
   cards, sections and chart frames at elevation 0 with no shadow, so a floating
   CARD is off-system by construction. A dropdown that genuinely floats reaches
   for --shadow-2 directly.
   They are deliberately declaration-thin -- one property each -- so composing
   two of them can never be ambiguous about which wins. A feature rule later in
   this file overrides a trait at equal specificity, which is the intended
   cascade: take the trait, then say what is different. */

/* Standard interior. A card that clips a table or chart to its own edge stays
   unpadded and lets the child own the inset. Two surfaces deliberately do NOT
   take this and set their own: .rh-record-card is tighter (--space-3/4) and
   .rh-dd-card is roomier (--space-5/6). Those are design calls, not drift. */
.card--pad { padding: var(--space-4) var(--space-5); }

/* Clip a chart, table or image to the rounded corner. */
.card--clip { overflow: hidden; }

/* The small surface: list rows, menu items, tappable items. design/SYSTEM.md
   section 5 assigns --radius-md to "buttons, inputs, segmented controls" and
   --radius-lg to "cards, sections, chart frames", so these are two SIZES of one
   recipe, not two recipes. Naming the small one is what stopped nineteen rows
   from each re-deriving it. */
.card--sm { border-radius: var(--radius-md); }

/* A leading stripe down the left edge: this card is set apart from its
   neighbours. Width and style here; the COLOUR is the user's (a discipline on a
   race-history record, the accent on a World Championships landing tile). Promoted
   from .rh-record-card on 24 Sep 2026 when the landing became its second user. */
.card--marked { border-left: 3px solid var(--border-strong); }

/* ========================================
   INLINE FILTERS / CHIP STRIP / SEARCH
   ======================================== */
.inline-filters {
    display: flex; flex-direction: column; gap: var(--space-2);
    margin-bottom: var(--space-3); padding: var(--space-2) var(--space-3);
    background: var(--surface-sunken); border-radius: var(--radius-md);
}
@media (min-width: 769px) {
    .inline-filters { gap: var(--space-3); padding: var(--space-3) var(--space-4); }
}

.filter-chip-row {
    display: flex; gap: var(--space-2); align-items: center;
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
}
.filter-chip-row::-webkit-scrollbar { display: none; }
/* The filter bar is a flex COLUMN, so a control dropped straight into it would
   stretch to the full bar width. Controls size to their content instead — this
   is the bar's job, not an inline `alignSelf` at each call site. */
.inline-filters > .switcher,
.inline-filters > .ctrl-pill,
.inline-filters > .ctrl-check { align-self: flex-start; }

/* ── Switcher: the shared segmented control ──────────────────────────────────
   `.switcher` wraps 2+ `.switcher-btn`s and marks the current one `.on`. Reach
   for it whenever a control picks ONE of a small set of lenses; use `.chip` for
   filters that narrow a list and `.tab-btn` for page-level tabs. One shared
   border ring, so the per-button radius collapses inside the group.

   Variants:
     .switcher--scroll  the group may outgrow a phone (5+ options) and scrolls.
   (--soft and --fill were declared here too and adopted by nothing; removed
    2026-09-03. Same bar as the card traits: a SECOND user, not a good idea.) */
.switcher {
    display: inline-flex; flex: none; align-items: stretch;
    max-width: 100%;
    border: 1px solid var(--border-strong); border-radius: var(--switch-radius);
    overflow: hidden; min-height: var(--switch-min-h);
    background: var(--switch-bg);
}
.switcher-btn {
    /* Segments size to their own label. Growing them to fill the row was tried
       and reads as a heavy full-width bar next to the rest of the controls. */
    flex: 0 0 auto;
    display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
    padding: var(--switch-py) var(--switch-px);
    border: 0; background: transparent; color: var(--switch-ink);
    font-family: inherit; font-size: var(--switch-text);
    font-weight: var(--font-weight-semibold);
    min-height: var(--switch-min-h); white-space: nowrap; cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background var(--transition-fast), color var(--transition-fast);
}
.switcher-btn + .switcher-btn { border-left: 1px solid var(--border); }
.switcher-btn:hover:not(.on):not([disabled]) { background: var(--surface-tint); color: var(--text-2); }
.switcher-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.switcher-btn.on { background: var(--switch-on-bg); color: var(--switch-on-ink); }
.switcher-btn[disabled] { opacity: 0.4; cursor: not-allowed; }
/* A count / unit that rides inside a segment (leg counts, race counts). */
.switcher-btn .switcher-n {
    font-size: var(--text-micro); font-weight: var(--font-weight-bold);
    opacity: 0.7; font-variant-numeric: tabular-nums;
}
.switcher-btn.on .switcher-n { opacity: 0.85; }
/* Every switcher's active segment is the same inverse fill (--switch-on-bg).
   Segments keyed to a chart series once took that series' colour instead, which
   put two active-states on one page (race history's header vs its sections) —
   the series colour lives on the line and its legend, not on the control. */

.switcher--scroll { overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.switcher--scroll::-webkit-scrollbar { display: none; }
/* Several switchers are hosts filled from JS (race-history division rotation,
   the H2H metric group) and are emptied when the race has nothing to switch
   between — an empty group must not leave a bare border ring behind. */
.switcher:empty { display: none; }

/* Filter/toggle chip — one geometry source (outline-quiet, dark-filled active). */
.chip {
    flex-shrink: 0;
    display: inline-flex; align-items: center; gap: var(--space-2);
    padding: var(--chip-py) var(--chip-px);
    border: 1px solid var(--border); border-radius: var(--chip-radius);
    background: var(--surface); color: var(--text-muted);
    font-size: var(--chip-text); font-weight: 500;
    min-height: var(--chip-min-h); cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    /* A chip reads as a control whatever element it is. The first <a class="chip">
       (the race page's link to its season index) arrived underlined, because the
       underline is the app-wide cue for a link sitting in running text — which a
       pill with a border never is. */
    text-decoration: none;
}
/* The dark-fill ON state. Every pick-one/filter control shares it — a second
   copy of these four declarations lived down in the surfaces section and had
   to be kept in step by hand. */
.chip.active, .chip:active { transform: scale(0.95); }

/* ⚙ Options button — collapses Quick-picks + Countries/Teams behind a panel.
   `.ctrl-pill` is the SHARED chart-control pill: any chart toggle (Race median,
   Pacing impact, …) uses it so every control reads as one component. Do not
   hand-roll button styling — add `.ctrl-pill` (+ `.on` when it's a toggle).
   It carries a <select> too, for a pick-one with too many options to sit in a
   `.switcher` (a championship's dozen-plus age bands). */
/* `.ctrl-check` — a chart control that is simply ON or OFF, as a checkbox.
   The pill next to it (`.ctrl-pill`) reads as a button you press to DO
   something, which is wrong for a state you are including or excluding; a
   checkbox says which it is without being read. Introduced 22 Sep 2026 and
   adopted by every boolean chart toggle (the para ranking's previous period,
   the spider's race median, the development chart's pacing impact, the
   replay's focus). `.ctrl-pill` keeps the commands and the <select>. */
.ctrl-check {
    display: inline-flex; align-items: center; gap: var(--space-3);
    min-height: var(--switch-min-h); padding: 0 var(--space-2);
    font-size: var(--text-sm); color: var(--text-muted); cursor: pointer;
    user-select: none;
}
.ctrl-check input { accent-color: var(--accent); width: 15px; height: 15px; margin: 0; cursor: pointer; }
.ctrl-check:hover { color: var(--text); }
.ctrl-check:has(input:checked) { color: var(--text); }

.filter-options-btn, .ctrl-pill {
    flex-shrink: 0; display: inline-flex; align-items: center; gap: var(--space-2);
    padding: var(--space-2) var(--space-5);
    border: 1px solid var(--border-strong); border-radius: var(--radius-xl);
    background: var(--surface); cursor: pointer;
    font-size: var(--text-sm); font-weight: var(--font-weight-semibold);
    color: var(--text-2); -webkit-tap-highlight-color: transparent;
    transition: all var(--transition-fast);
}
.filter-options-btn .fo-icon { font-size: var(--text-md); line-height: 1; }
.filter-options-btn .fo-text { white-space: nowrap; }
.filter-options-btn .fo-summary {
    color: var(--accent); font-weight: var(--font-weight-bold);
    max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
select.ctrl-pill {
    /* The platform caret stays: it is the only thing that says "menu" rather
       than "button", and on a phone the native picker is the right control for
       a dozen-plus options. Only the box is restyled to match the pills. */
    padding-right: var(--space-3);
    max-width: min(11rem, 264px);   /* follows text size, capped like the type scale */
}
.filter-options-btn:hover, .ctrl-pill:hover { border-color: var(--text-faint); background: var(--surface-tint); }
.filter-options-btn.active {
    background: var(--accent); color: white; border-color: var(--accent);
}
.filter-options-btn.active .fo-summary { color: var(--surface); }
.filter-options-btn.has-selection:not(.active) { border-color: var(--accent); }

.filter-options-panel {
    display: none;
    flex-direction: column;
    gap: var(--space-3);
    margin-top: var(--space-3);
}
.filter-options-panel.open { display: flex; }
.fop-section { display: flex; flex-direction: column; gap: var(--space-2); }
.fop-section-title {
    font-size: var(--text-eyebrow); text-transform: uppercase; letter-spacing: 0.5px;
    font-weight: var(--font-weight-bold); color: var(--text-muted);
}
.fop-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.fop-chips.country-chip-row { display: flex; max-height: 120px; overflow-y: auto; }

/* .country-chip geometry from .chip; keeps nowrap + transition + hover, and a
   bold + ✓ active (the dark fill itself comes from .chip.active). */
.country-chip { white-space: nowrap; transition: all var(--transition-fast); }
.country-chip:hover { border-color: var(--text-faint); background: var(--surface-tint); }
.country-chip.active { font-weight: var(--font-weight-bold); }
.country-chip.active::before { content: '✓ '; font-weight: 900; }
/* flag / team-logo inside a filter chip (replaces the abbreviation text) */
.country-chip img, .country-chip .flag-img {
    width: 22px; height: 15px; object-fit: contain; border-radius: 2px; display: block;
}

/* Inline search (lives inside the Options panel) */
.fop-search-wrap { position: relative; }
.inline-search-input {
    width: 100%; padding: var(--space-2) var(--space-4);
    border: 1px solid var(--border-strong); border-radius: var(--radius-md);
    font-size: var(--text-md); background: var(--surface);
    -webkit-appearance: none; box-sizing: border-box;
}
.inline-search-input:focus { outline: none; border-color: var(--accent); }
.inline-search-results {
    position: absolute; top: 100%; left: 0; right: 0; z-index: var(--z-dropdown);
    background: var(--surface); border: 1px solid var(--border-strong);
    border-radius: 0 0 var(--radius-md) var(--radius-md);
    box-shadow: var(--shadow-3); max-height: 180px; overflow-y: auto; display: none;
}
.inline-search-results.open { display: block; }
@media (min-width: 769px) {
    .inline-search-input { font-size: var(--text-lg); padding: var(--space-4) var(--space-6); }
}
/* iOS Safari zooms the page in on focus of any text field under 16px, and never
   zooms back out, so the view behind it (an athlete profile) opens magnified.
   Touch only: desktop keeps the type ladder. :is() carries attribute
   specificity so this outranks single-class rules like .settings-num. */
@media (pointer: coarse) {
    input:is([type="search"], [type="text"], [type="number"], :not([type])) { font-size: var(--text-input); }
}

.sr-row {
    display: flex; align-items: center; gap: var(--space-3);
    padding: var(--space-3) var(--space-4); cursor: pointer;
    border-bottom: 1px solid var(--surface-tint); font-size: var(--text-sm);
}
.sr-row:active { background: var(--accent-subtle); }
.sr-rank { font-weight: var(--font-weight-bold); color: var(--accent); min-width: 22px; }
.sr-name { flex: 1; }
.sr-toggle { font-weight: var(--font-weight-bold); font-size: var(--text-md); }
.sr-toggle.on { color: var(--success); }
.sr-toggle.off { color: var(--border-strong); }
.sr-no-results { padding: var(--space-5); color: var(--text-faint); text-align: center; font-size: var(--text-sm); }
@media (min-width: 769px) {
    .sr-row { font-size: var(--text-md); padding: var(--space-5) var(--space-7); }
    .sr-rank { font-size: var(--text-md); min-width: 30px; }
}

/* Mobile collapsible controls (dev chart header reset button) */
.dev-header-row, .rank-header-row { position: relative; }
.dev-header-row .chart-header,
.rank-header-row .chart-header { display: block; }
.chart-reset {
    display: none;                     /* hidden until a segment is zoomed */
    position: absolute; left: 0; top: 4px;
    height: 26px; min-width: 30px; padding: 0 var(--space-2);
    background: var(--surface-hover); border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    font-size: var(--text-xs); font-weight: var(--font-weight-bold);
    color: var(--text-muted); cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    white-space: nowrap; line-height: 1;
    transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
    z-index: 1;
}
.chart-reset.show {
    display: block; background: var(--text-2); color: var(--bg-app); border-color: var(--text-2);
}
.chart-reset.show:hover { background: var(--text); border-color: var(--text); }
@media (min-width: 769px) {
    .chart-reset { height: 30px; padding: 0 var(--space-5); font-size: var(--text-md); min-width: 40px; }
}

/* ========================================
   CHIP STRIP — global watched-athletes bar
   ======================================== */
.chip-strip {
    display: flex; align-items: center;
    gap: var(--space-2); padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border); border-radius: var(--radius-md);
    margin-bottom: var(--space-2);
    min-height: 30px;
    /* folded up from the SaaS layer 2026-09-03 */
    background: var(--surface-sunken);
    border: 1px solid var(--border);
}
@media (min-width: 769px) {
    .chip-strip { padding: var(--space-3) var(--space-5); min-height: 36px; }
}
.cs-label {
    font-size: var(--text-micro);
    font-weight: var(--font-weight-bold); color: var(--text-faint);
    white-space: nowrap; text-transform: uppercase; letter-spacing: 0.5px;
    flex-shrink: 0;
    /* `.switcher-btn.on` uses the same --text fill (see the switcher primitive); don't add a --primary-color active anywhere — it drifts from this family and collides with the accent that marks a watched athlete. */
    /* folded up from the SaaS layer 2026-09-03 */
    color: var(--text-muted);
}
@media (min-width: 769px) { .cs-label { font-size: var(--text-sm); } }
.cs-scroll {
    display: flex; gap: var(--space-2); overflow-x: auto; flex: 1;
    /* The chips' 44px boxes overhang the strip instead of stretching it: the
       strip keeps its old ~30px look (27 Sep 2026). A sideways scroller clips
       vertically too, so the scroller is the chips' 44 tall box itself and
       the negative margins give back 24 of it: 20px of layout. */
    margin-block: -12px;
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.cs-scroll::-webkit-scrollbar { display: none; }
.cs-chip {
    /* Two targets, each 44px tall: the body opens the profile, the × zone
       removes. The BOX is the 44px tap area and carries no paint; the pill you
       see is ::before, inset to its old size, so the chip looks as it did while
       a thumb has the whole strip height to land in. */
    position: relative; isolation: isolate;
    display: inline-flex; align-items: center; gap: var(--space-2);
    min-height: 44px;
    padding: 0 0 0 var(--space-2);
    border: 0; background: none;
    font-size: var(--text-xs); font-weight: var(--font-weight-semibold);
    color: var(--chip-color, var(--text-2));
    cursor: pointer; white-space: nowrap; flex-shrink: 0;
    -webkit-tap-highlight-color: transparent;
    line-height: 1.4;
}
.cs-chip::before {
    content: ''; position: absolute; z-index: -1;
    /* 12px top and bottom leaves the old 20px pill; the right inset leaves the
       outer part of the × zone outside the pill, as space between chips. */
    inset: 12px 0;
    /* Watched-athlete chips: keep --chip-color (the data color!) but quiet the frame */
    border: 1px solid var(--chip-color, var(--accent));
    border-radius: 12px;
    background: var(--surface);
    transition: background var(--transition-fast);
}
.cs-chip:hover::before { background: var(--surface-hover); }
/* The name is the tap target for the athlete's profile — see .athlete-link, the
   app-wide rule. Colour identity is already carried by the name + pill here, so
   the chip needs no separate dot/glyph. */
.cs-name {
    text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px;
    text-decoration-color: currentColor;                /* fallback */
    text-decoration-color: var(--link-underline);
}
.cs-x {
    /* Its own zone at the chip's end, the full 44 tall, so a thumb aimed at ×
       cannot land on the name. 26 wide: a 44 wide zone spread the chips apart
       (27 Sep 2026), so it is exempt in width. */
    align-self: stretch; width: 26px; margin-left: calc(var(--space-2) * -1);
    display: inline-flex; align-items: center; justify-content: flex-end;
    padding-right: var(--space-3); box-sizing: border-box;
    font-size: var(--text-md); font-weight: var(--font-weight-normal);
    color: var(--text-faint); cursor: pointer; line-height: 1;
}
.cs-x:hover { color: var(--danger); }
/* The squad key (chip-strip.js _renderTeams): crest + count + ×, one row. The
   body is a target (it names the school), so it keeps the pointer; the pill
   stays at rest on hover because nothing about the chip changes state. */
.cs-chip-team:hover::before { background: var(--surface); }
.cs-team-n {
    font-size: var(--text-xs); font-weight: var(--font-weight-semibold);
    color: var(--text-faint);
}
.cs-clear {
    /* position + margin: its 44px area (the tap-area list) must not reach over
       the last chip's × zone, which ends at the scroller's edge. */
    position: relative; margin-left: 11px;
    flex-shrink: 0; width: 22px; height: 22px; border-radius: 50%;
    border: 1px solid var(--border); background: var(--surface);
    color: var(--text-faint); font-size: var(--text-xs); cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    -webkit-tap-highlight-color: transparent;
}
.cs-clear:hover { background: var(--danger-bg); color: var(--danger); border-color: var(--danger); }
@media (min-width: 769px) {
    .cs-chip { font-size: var(--text-md); padding-left: var(--space-3); }
    .cs-x { font-size: var(--text-lg); }
    .cs-clear { width: 28px; height: 28px; font-size: var(--text-md); }
}

/* ========================================
   MODALS — source picker, dialog, athlete browser
   ======================================== */
.src-overlay {
    position: fixed; inset: 0; background: var(--scrim); z-index: var(--z-modal);
    display: flex; align-items: center; justify-content: center; padding: var(--space-8);
    /* folded up from the SaaS layer 2026-09-03 */
    background: rgba(15, 23, 42, 0.4);
    backdrop-filter: blur(2px);
}
.src-modal {
    background: var(--surface); border-radius: var(--radius-lg);
    width: min(420px, 100%); max-height: 90vh; position: relative;
    display: flex; flex-direction: column; overflow: hidden;
    /* folded up from the SaaS layer 2026-09-03 */
    border-radius: var(--radius-xl);
    box-shadow: var(--shadow-3);
    border: 1px solid var(--border);
}
.src-modal-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: var(--space-5) var(--space-7);
    border-bottom: 1px solid var(--border);
    background: var(--surface-hover);
}
.src-modal-title { font-weight: var(--font-weight-bold); font-size: var(--text-md); color: var(--text-2); }
/* Clickable variant — the start-list modal uses this on its title so the
   user can jump from "who's racing" to "how has this race evolved". */
.src-modal-title-link {
    background: none; border: 0; padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-md);
    cursor: pointer; color: var(--warning);
    font-weight: var(--font-weight-bold); font-size: var(--text-md);
    font-family: inherit;
    transition: background var(--transition-fast), color var(--transition-fast);
}
.src-modal-title-link:hover,
.src-modal-title-link:focus-visible {
    background: var(--warning-bg); color: var(--warning); outline: none;
}
.src-modal-sub { font-size: var(--text-sm); color: var(--text-muted); margin-left: var(--space-2); }
.src-modal-close { font-size: var(--text-xl); padding: 0 var(--space-2); }
.src-modal-close:hover { color: var(--text-2); }
.src-modal-body {
    padding: var(--space-7);
    display: flex; flex-direction: column; gap: var(--space-5);
    overflow-y: auto;
}
.src-field { display: flex; flex-direction: column; gap: var(--space-2); }
.src-field > span { font-size: var(--text-sm); font-weight: var(--font-weight-semibold); color: var(--text-muted); }
.src-field select {
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--border-strong); border-radius: var(--radius-md);
    background: var(--surface); font-size: var(--text-md);
}
.src-field select:disabled { background: var(--surface-tint); color: var(--text-faint); }
.src-status { font-size: var(--text-sm); color: var(--text-muted); min-height: 14px; }
.src-modal-foot {
    padding: var(--space-5) var(--space-7); border-top: 1px solid var(--border);
    background: var(--surface-hover);
    display: flex; justify-content: flex-end; gap: var(--space-3);
}

/* Reel maker (js/ui/reel-maker.js): the source-picker modal, a 9:16 preview
   on top and the choices under it. The offscreen replay it renders from sits
   off the page at the size it draws at. */
.reel-modal { width: min(460px, 100%); }
.reel-preview { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); }
.reel-preview canvas, .reel-preview video {
    height: min(44vh, 440px); aspect-ratio: 9 / 16; width: auto; max-width: 100%;
    border-radius: var(--radius-lg); border: 1px solid var(--border); background: var(--surface-sunken);
}
.reel-preview canvas[hidden] { display: none; }
.reel-meta { font-size: var(--text-sm); color: var(--text-muted); }
.reel-form { display: flex; flex-direction: column; gap: var(--space-5); }
.reel-leg { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3) 0; border-top: 1px solid var(--border); }
.reel-leg-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.reel-leg-name { font-weight: var(--font-weight-semibold); color: var(--text-2); }
.reel-laps { color: var(--text-muted); font-size: var(--text-sm); font-weight: var(--font-weight-normal); }
.reel-end { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); color: var(--text-muted); }
/* One row per lap: the checkbox, then (when it is in) how much, the view and
   the speed, wrapping onto a second line on a phone. */
.reel-lap-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); }
.reel-lap-row .ctrl-check { min-width: 5.5rem; }
.reel-lap-row.on { padding-bottom: var(--space-2); }
.reel-athletes { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.reel-athletes:empty { display: none; }
/* The team chips and "Find an athlete" reuse the Options panel's country
   chips and search rows; in the sheet the list sits in the flow, not over it. */
.reel-teams { max-height: 132px; }
.reel-count { color: var(--text-muted); font-weight: var(--font-weight-normal); }
.reel-find .inline-search-results { position: static; max-height: 260px; border-radius: var(--radius-md); box-shadow: none; margin-top: var(--space-2); }
.reel-find .sr-row img, .reel-find .sr-row .flag-img { width: 22px; height: 15px; object-fit: contain; border-radius: 2px; flex: none; }
.reel-checks { display: flex; flex-wrap: wrap; gap: var(--space-5); padding-top: var(--space-2); }
.reel-foot { align-items: center; flex-wrap: wrap; }
.reel-foot .reel-status { margin-right: auto; }
.reel-foot a.ctrl-pill { text-decoration: none; }
.reel-busy .reel-form { opacity: 0.55; }
.reel-offscreen { position: fixed; left: -20000px; top: 0; width: 512px; pointer-events: none; }

/* Generic dialog (used by chart configuration etc.) */
.dialog-overlay { position: fixed; inset: 0; background: var(--scrim-strong); z-index: var(--z-modal); display: flex; align-items: center; justify-content: center; }

.summary-podium { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-4); }
.podium-card {
    display: flex; align-items: flex-start; gap: var(--space-4);
    padding: var(--space-4) var(--space-6);
    background: var(--surface); border-radius: var(--radius-md);
    border-left: 3px solid var(--border-strong);
    font-size: var(--text-base); line-height: 1.4;
}
.podium-1 { border-color: var(--gold); background: linear-gradient(to right, var(--warning-bg), var(--surface)); }
.podium-2 { border-color: var(--silver); }
.podium-3 { border-color: var(--bronze); }
.podium-medal { font-size: var(--text-base); margin-top: 0; }
.podium-body { flex: 1; display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.podium-row { display: flex; align-items: baseline; gap: var(--space-3); }
.podium-name { flex: 1; font-weight: var(--font-weight-semibold); }
.podium-time { font-weight: var(--font-weight-bold); color: var(--accent); font-size: var(--text-md); }
.podium-paces { font-size: var(--text-micro); color: var(--text-muted); }
@media (min-width: 769px) {
    .podium-card { font-size: var(--text-md); padding: var(--space-5) var(--space-7); gap: var(--space-5); }
    .podium-medal { font-size: var(--text-md); }
    .podium-time { font-size: var(--text-lg); }
}

.share-cta-row { display: flex; justify-content: flex-end; gap: var(--space-3); margin-bottom: var(--space-3); flex-wrap: wrap; }
.share-cta {
    display: inline-flex; align-items: center; gap: var(--space-3);
    padding: var(--space-4) var(--space-7); border: 1px solid var(--border); border-radius: var(--radius-pill);
    background: transparent; color: var(--text-2); cursor: pointer;
    font-size: var(--text-sm); font-weight: var(--font-weight-bold);
    transition: color var(--transition-fast), border-color var(--transition-fast), background var(--transition-fast);
}
.share-cta:hover { color: var(--accent); border-color: var(--accent); background: var(--surface); }
.share-cta svg { width: 14px; height: 14px; }

.share-btn {
    --icon-btn-size: 30px; --icon-btn-radius: var(--radius-md);
    align-self: center; border-color: var(--border); color: var(--text-muted);
}
.share-btn:hover { color: var(--accent); border-color: var(--accent); background: var(--surface); }
.share-btn-sm { --icon-btn-size: 24px; border-color: transparent; }
.seg-entry:hover .share-btn-sm { border-color: var(--border); }

/* A view's one Share control, at the right end of its first row: level with
   Options, clear of the chart and its table. */
.share-slot { margin-left: auto; }
.share-slot::before {
    content: ''; width: 16px; height: 16px; background: currentColor;
    -webkit-mask: var(--share-icon) center / contain no-repeat;
    mask: var(--share-icon) center / contain no-repeat;
}
.share-slot[hidden] { display: none; }


.summary-stats {
    display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-2); margin-bottom: var(--space-4);
}
.mini-stat { text-align: center; padding: var(--space-2) var(--space-1); background: var(--surface); border-radius: var(--radius-md); }
.ms-val { display: block; font-weight: var(--font-weight-bold); font-size: var(--text-md); color: var(--accent); }
.ms-label { display: block; font-size: var(--text-micro); color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.4px; }
.ms-sub { display: block; font-size: var(--text-micro); color: var(--text-muted); margin-top: 1px; }
@media (min-width: 769px) {
    .ms-val { font-size: var(--text-2xl); }
    .ms-label { font-size: var(--text-sm); letter-spacing: 0.6px; }
    .mini-stat { padding: var(--space-4) var(--space-1); }
}
@media (min-width: 1200px) {
    .ms-val { font-size: var(--text-3xl); }
}

/* A fixed `repeat(3, 1fr)` was the bug behind the vanishing Run column: a `1fr`
   track keeps an `auto` MINIMUM, so each column refused to shrink below the
   longest name in it, three of them overflowed the container, and the third was
   clipped out of sight at desktop widths.
   auto-fit + a real floor instead: three columns when they fit, two in the
   middle band, one on a phone. `min(100%, …)` keeps the floor from overflowing
   a container narrower than the floor itself. */
.summary-segments {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
    gap: var(--space-2);
}
.seg-col { background: var(--surface); border-radius: var(--radius-md); padding: var(--space-4) var(--space-5); }
.seg-header {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
    font-weight: var(--font-weight-bold); font-size: var(--text-sm); color: var(--text-2);
    margin-bottom: var(--space-2); padding-bottom: var(--space-1); border-bottom: 1px solid var(--surface-tint);
}
/* Discipline-podium share button lives in the header (no row-hover to reveal it) */
.seg-header .seg-podium-share { border-color: var(--border); color: var(--text-muted); }
.seg-header .seg-podium-share:hover { color: var(--accent); border-color: var(--accent); background: var(--surface); }
/* ── Para: one block per sport class ──────────────────────
   A para race is several races sharing a course, scored within class, so the
   summary repeats podium + discipline leaders per class instead of showing one
   field-wide ranking. Same surface/radius vocabulary as .seg-col. */
.para-class-block { background: var(--surface); border-radius: var(--radius-md); padding: var(--space-4) var(--space-5); margin-bottom: var(--space-3); }
.para-class-head {
    display: flex; align-items: center; gap: var(--space-3);
    font-weight: var(--font-weight-bold); font-size: var(--text-sm); color: var(--text-2);
    margin-bottom: var(--space-3); padding-bottom: var(--space-1); border-bottom: 1px solid var(--surface-tint);
}
.para-class-name { color: var(--text); }
/* PTVI guide, named under the athlete: they race the whole race together. */
.podium-guide { color: var(--text-muted); font-size: var(--text-sm); }
/* Staggered start: elapsed + the head start conceded, so the finish time above
   reconciles with splits that deliberately sum to less than it. */
.podium-stagger { color: var(--text-muted); font-size: var(--text-sm); }
.para-class-count { color: var(--text-muted); font-weight: var(--font-weight-normal); margin-right: auto; }
.para-class-head .share-btn { border-color: var(--border); color: var(--text-muted); }
.para-class-head .share-btn:hover { color: var(--accent); border-color: var(--accent); background: var(--surface); }
@media (min-width: 769px) {
    .para-class-block { padding: var(--space-5); }
    .para-class-head { font-size: var(--text-md); }
}

/* The para form-spread box plot (.para-spread / .ps-*) used to live here. It was
   the cross-course normalisation experiment, which measured as a coin flip and
   was replaced by the per-race class chart (see scripts/para/EXPERIMENTS.md and
   js/charts/para-class-chart.js). Nothing has rendered those classes since. */

/* Gender group heading inside a fastest-sub-splits column (mixed relays) */
.seg-gender-label { font-weight: var(--font-weight-bold); color: var(--text-muted); font-size: var(--text-sm); line-height: 1.7; margin: var(--space-2) 0 1px; }
.seg-gender-group:first-of-type .seg-gender-label { margin-top: 1px; }

/* Mixed relay: one block per gender, and the blocks line up ACROSS the three
   discipline columns. Without this each column packs its own way, so a tie in
   the bike's first block (four rows instead of three) pushed the bike's second
   block a row below the swim's and the run's, and three columns of the same
   thing read as three unrelated lists.
   Subgrid gives every column the same three rows — header, first gender, second
   gender — and each block is placed in the row its gender owns, so a discipline
   with no entry for one gender leaves that row empty instead of moving the other
   gender into it. Stacked (one column per row band) nothing is shared, so the
   phone keeps its tight blocks. */
.summary-segments--gendered { grid-template-rows: repeat(3, auto); }
.summary-segments--gendered .seg-col {
    display: grid;
    grid-row: span 3;
    grid-template-rows: subgrid;
}
.summary-segments--gendered .seg-header { grid-row: 1; }
.summary-segments--gendered .seg-gender-group[data-gi="0"] { grid-row: 2; }
.summary-segments--gendered .seg-gender-group[data-gi="1"] { grid-row: 3; }
@media (min-width: 769px) {
    .seg-col { padding: var(--space-5); }
    .seg-header { font-size: var(--text-md); }
}

/* Single-row per-rank entry inside .seg-col */
.seg-entry {
    display: flex; flex-wrap: nowrap; align-items: baseline;
    gap: var(--space-2); padding: var(--space-2) 0;
    border-bottom: 1px solid var(--surface-tint);
    font-size: var(--text-sm);
}
.seg-entry .seg-rank { font-weight: var(--font-weight-bold); color: var(--accent); min-width: 14px; flex-shrink: 0; }
.seg-entry .seg-time { font-weight: var(--font-weight-semibold); flex-shrink: 0; }
.seg-entry .seg-pace { color: var(--text-muted); font-size: var(--text-micro); flex-shrink: 0; }
/* Wraps rather than ellipsing: three columns share the row, so at narrow desktop
   widths the name gets little space and an ellipsis ate most of it. A second
   line costs a few pixels of height and keeps the name whole. */
.seg-entry .seg-name {
    flex: 1; min-width: 0; overflow-wrap: break-word;
    font-weight: var(--font-weight-semibold);
}
/* Relay only: the team behind a member, where the flag alone can't say it (a
   second national team, a club). Muted — the name is what the row is about. */
.seg-entry .seg-team { color: var(--text-muted); }
/* A split podium ranks the leg, so a non-finisher can place in it. The status
   rides with the name rather than replacing the rank: the swim happened, the
   race didn't finish, and the row has to say both. */
.seg-entry .seg-dnf {
    margin-left: var(--space-2);
    padding: 0 var(--space-2);
    border-radius: var(--radius-sm);
    background: var(--surface-tint);
    color: var(--danger);
    font-size: var(--text-micro);
    font-weight: var(--font-weight-semibold);
    white-space: nowrap;
}
@media (min-width: 769px) {
    .seg-entry { font-size: var(--text-md); gap: var(--space-2); padding: var(--space-2) 0; }
    .seg-entry .seg-rank { min-width: 18px; }
    .seg-entry .seg-pace { font-size: var(--text-sm); }
}
@media (min-width: 1200px) { .seg-entry { font-size: var(--text-lg); } }

.summary-section-title {
    margin: var(--space-5) 0 var(--space-2);
    font-size: var(--text-sm);
}
@media (min-width: 769px) {
    .summary-section-title { font-size: var(--text-md); margin: var(--space-7) 0 var(--space-3); }
}

/* Relay member breakdown inside podium card */
.relay-members {
    display: flex; flex-direction: column; gap: 1px;
    margin-top: var(--space-2); padding: var(--space-2) 0 0 var(--space-3);
    border-top: 1px dashed rgba(0, 0, 0, 0.08);
}
.relay-member {
    display: flex; align-items: baseline; gap: var(--space-3);
    font-size: var(--text-sm); color: var(--text-muted); padding: 1px 0;
}
.relay-leg-num { font-weight: var(--font-weight-bold); color: var(--text-muted); min-width: 18px; font-size: var(--text-micro); text-transform: uppercase; }
.relay-gender { color: var(--text-faint); font-size: var(--text-xs); min-width: 10px; }
/* Holds a flag <img> (a fixed 16x12 .flag-img) or the .flag-code chip that
   stands in for a code with no flag — never an emoji glyph, so it is sized
   as a BOX. min-width keeps the name column aligned across the legs when a
   member's NOC is unknown and there is nothing at all to paint. */
.relay-flag { display: inline-flex; align-items: center; flex: none; min-width: 16px; }
/* Wraps, never truncates: the leg list carries FULL names, so it is where a long
   one lands. The row is baseline-aligned, so the time stays on the first line. */
.relay-member-name { flex: 1; min-width: 0; overflow-wrap: break-word; }
.relay-member-time { font-size: var(--text-xs); color: var(--text-muted); }
@media (min-width: 769px) {
    .relay-member { font-size: var(--text-md); padding: var(--space-1) 0; gap: var(--space-4); }
    .relay-leg-num { font-size: var(--text-base); min-width: 22px; }
    .relay-gender { font-size: var(--text-md); min-width: 12px; }
    .relay-member-time { font-size: var(--text-md); }
}

/* The same track as .summary-segments above, deliberately spelled the same way:
   a bare 160px minimum fit TWO columns at the phone width, so the leg cards came
   up half as wide as the sub-split cards directly above them and wrapped every
   name onto three lines. The min(100%, …) is what makes auto-fit stack instead
   of cramming — the track can never be asked to be wider than the container. */
.summary-legs {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
    gap: var(--space-2);
}
@media (min-width: 769px) {
    .summary-legs { gap: var(--space-3); }
    /* Four legs is the count auto-fit gets wrong: it spends a desktop row on
       four cards a third the width of the three sub-split cards above them, and
       every name wraps onto a second line under its own rank. 2x2 instead — legs
       1 and 2 over legs 3 and 4 — which is the column width a name, a gender
       mark, a flag and a time need to sit on one row, and it lines the section
       up with the grid above rather than cutting across it. Three legs or two
       already stack neatly and keep auto-fit. */
    .summary-legs[data-legs="4"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.summary-headbar { display: flex; align-items: center; justify-content: space-between; gap: var(--space-5); flex-wrap: wrap; margin-bottom: var(--space-3); }
.summary-distances { font-size: var(--text-base); color: var(--text-muted); }
.summary-distances .dist-seg { white-space: nowrap; }
.summary-distances .dist-leg { color: var(--text-faint); font-weight: var(--font-weight-normal); }
.summary-distances .dist-km { color: var(--text-2); font-weight: var(--font-weight-semibold); }
.summary-distances .dist-sep { color: var(--text-faint); margin: 0 var(--space-2); }
.summary-dist-note { color: var(--text-faint); font-weight: var(--font-weight-normal); }
/* Whose number an estimated leg distance is. One line under the distances, not
   a repeat beside every pace it feeds. */
.summary-est-note { color: var(--text-faint); font-size: var(--text-sm); margin: calc(var(--space-1) * -1) 0 var(--space-3); }
/* The sport class of a race loaded as one class on its own. It sits opposite
   the distances because it is the same kind of fact about the race, and it is
   the door to the class over two years (.para-class-open). */
.summary-class { font-size: var(--text-base); font-weight: var(--font-weight-semibold); color: var(--text-2); }
/* Theme / units pickers in the app menu use the shared `.switcher`. */
.summary-conditions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin: var(--space-2) 0 var(--space-5); }
.cond-chip { font-size: var(--text-sm); background: var(--surface-tint); border: 1px solid var(--border); border-radius: var(--radius-xl); padding: 3px var(--space-4); color: var(--text-2); }
.cond-chip small { color: var(--text-faint); }
.cond-chip.cond-duathlon { background: var(--warning-bg); border-color: var(--warning); color: var(--warning); }
/* Provenance, not a race fact: the organiser's timing, or our own reading of
   the live timing mats, standing in until World Triathlon posts. Amber whether
   or not it links anywhere: the live-timing one has no timer page to link to,
   and a grey chip was easy to miss on a result that can still change. A link
   keeps the chip ink rather than turning into a coloured link, same rule as
   .athlete-link. */
.cond-chip.cond-provisional { color: var(--warning); background: var(--warning-bg); border-color: var(--warning); text-decoration: none; }
.cond-chip.cond-provisional small { color: inherit; opacity: 0.75; }

/* ========================================
   SETTINGS
   ======================================== */
.settings-card { max-width: 560px; margin: 0 auto; padding: var(--space-5); }
.settings-section { margin-bottom: var(--space-8); }
.settings-section-title {
    letter-spacing: 0.08em;
    color: var(--text-faint);
    margin-bottom: var(--space-3); padding-left: var(--space-1);
}
.settings-presets {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4);
}
.settings-preset {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    padding: var(--space-3) var(--space-3);
    cursor: pointer;
    text-align: center;
    transition: all var(--transition-fast);
}
.settings-preset:hover { border-color: var(--border-strong); }
.settings-preset.active { border-color: var(--accent); background: var(--accent-subtle); }
.settings-preset-label { font-size: var(--text-md); font-weight: var(--font-weight-semibold); color: var(--text-2); }
.settings-preset-sub { font-size: var(--text-xs); color: var(--text-faint); }
.settings-preset.active .settings-preset-label { color: var(--accent-hover); }

.settings-row { margin-bottom: var(--space-4); }
.settings-row-head {
    display: flex; justify-content: space-between; align-items: baseline;
    margin-bottom: var(--space-2);
}
.settings-label { font-size: var(--text-md); font-weight: var(--font-weight-semibold); color: var(--text-2); }
.settings-value { font-size: var(--text-md); font-weight: var(--font-weight-bold); color: var(--accent); }
.settings-num {
    width: 4.2em; text-align: right; font: inherit; color: var(--accent);
    border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
    padding: 1px var(--space-2); margin-right: var(--space-1); background: var(--surface);
}
.settings-num:focus { outline: none; border-color: var(--accent); }
.settings-row-foot {
    display: flex; justify-content: space-between;
    font-size: var(--text-xs); color: var(--text-faint); margin-top: var(--space-2);
}
/* ── Unified range slider (settings, prediction, replay — one look) ──
   The track is drawn by the runnable-track pseudo at a fixed 6px, so an input
   can keep a taller element height for a bigger touch target (e.g. the replay
   scrubber) without thickening the visible track. Accent = primary in both
   themes. Any per-component accent-color is a no-op once appearance is none. */
input[type="range"] {
    -webkit-appearance: none; appearance: none;
    width: 100%; margin: 0; background: transparent;
    cursor: pointer; outline: none; touch-action: pan-y;
}
input[type="range"]::-webkit-slider-runnable-track {
    height: 6px; border-radius: var(--radius-pill); background: var(--border);
}
input[type="range"]::-moz-range-track {
    height: 6px; border-radius: var(--radius-pill); background: var(--border);
}
input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none; appearance: none; margin-top: -5px;
    width: 16px; height: 16px; border-radius: 50%;
    background: var(--accent); border: 2px solid var(--surface);
    box-shadow: 0 1px 3px rgba(15, 23, 42, 0.28); cursor: pointer;
}
input[type="range"]::-moz-range-thumb {
    width: 16px; height: 16px; border-radius: 50%;
    background: var(--accent); border: 2px solid var(--surface);
    box-shadow: 0 1px 3px rgba(15, 23, 42, 0.28); cursor: pointer;
}
input[type="range"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.settings-save {
    width: 100%; padding: var(--space-6);
    font-size: var(--text-md); font-weight: var(--font-weight-semibold);
    margin-top: var(--space-4);
}
.settings-save.saved { background: var(--success); }
.settings-note { margin: var(--space-6) 0 0; font-size: var(--text-sm); color: var(--text-faint); text-align: center; }

/* ========================================
   TEAM RANKINGS
   ======================================== */
.team-rankings, .country-rankings { display: flex; flex-direction: column; gap: var(--space-2); }
.team-division-group { margin-bottom: var(--space-5); }
.team-division-title {
    margin: var(--space-4) 0 var(--space-2);
    font-size: var(--text-sm); font-weight: var(--font-weight-bold);
    text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted);
    padding-bottom: var(--space-1); border-bottom: 2px solid var(--accent);
}
.team-row { background: var(--surface); border-radius: var(--radius-sm); overflow: hidden; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); }
.team-row.podium { background: linear-gradient(to right, var(--warning-bg), var(--surface)); }
/* Relay: a country's squads grouped under one heading */
.relay-country-group { background: var(--surface); border-radius: var(--radius-sm); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); overflow: hidden; }
.relay-country-group .relay-country-header { font-weight: var(--font-weight-bold); font-size: var(--text-md); padding: var(--space-3) var(--space-5) 0; color: var(--text); }
.relay-country-group .team-row { box-shadow: none; border-radius: 0; background: transparent; }
.relay-country-group .team-row + .team-row { border-top: 1px solid var(--surface-tint); }
.team-status-badge { font-size: var(--text-xs); font-weight: var(--font-weight-bold); color: var(--text-muted); letter-spacing: 0.03em; }
/* The summary is a grid, not a flow of flex items: a spelled-out school name is
   long enough that it needs a cell of its own, and the grid lets the points sit
   beside the places (rather than on a line of their own) without either of them
   moving in the markup. Areas are re-mapped per breakpoint below. */
.team-summary { display: grid; align-items: center; column-gap: var(--space-4); row-gap: var(--space-1); padding: var(--space-2) var(--space-5); cursor: pointer; user-select: none; }
.team-summary:hover { background: var(--surface-hover); }
.team-info { display: contents; }   /* its two children are the real grid items */
.team-rank { grid-area: rank; font-size: var(--text-md); font-weight: var(--font-weight-bold); min-width: 28px; color: var(--text-2); }
.team-name { grid-area: name; min-width: 0; font-weight: var(--font-weight-bold); font-size: var(--text-md); display: flex; align-items: center; gap: var(--space-2); }
/* Standings spell the team out, so the name wraps instead of squeezing the
   figures beside it. */
.team-name-text { overflow-wrap: anywhere; line-height: 1.2; }
.team-score { grid-area: score; justify-self: start; font-size: var(--text-sm); color: var(--text-muted); font-weight: var(--font-weight-medium); }
/* Right-hand stack: scoring places, and the team's average paces under them.
   Points sit on the left of the row, these on the right. */
.team-meta { grid-area: meta; justify-self: end; display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-1); min-width: 0; }
/* Bare figures, no per-discipline tag — the tags ate the width the numbers
   need, most visibly for an NCAA squad with 5+ scoring places. Order is always
   swim, bike, run; the units tell them apart. */
.team-averages { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: baseline; gap: var(--space-1) var(--space-8); font-size: var(--text-xs); color: var(--text-muted); }
/* Fixed slot per discipline so swim/bike/run line up as columns down the
   standings instead of drifting with each row's digits. */
.avg { font-family: var(--font-family-mono); font-weight: var(--font-weight-semibold); white-space: nowrap; min-width: 5.6em; text-align: right; }
.avg:last-child { min-width: 0; }
.scoring-positions { display: flex; gap: var(--space-1); flex-wrap: wrap; justify-content: flex-end; }
.pos { padding: 1px var(--space-2); border-radius: var(--radius-sm); font-size: var(--text-micro); font-weight: var(--font-weight-semibold); }
.pos.scoring, .pos.counting { color: white; }
.pos.displacer { background: var(--border-strong); color: var(--text-muted); }
/* A team's own Share rides beside its name, in the muted ink of a control
   rather than the team's colour. */
.team-name .team-share { color: var(--text-muted); border-color: var(--border); }
.expand-icon { grid-area: chev; color: var(--text-faint); font-size: var(--text-xs); transition: transform var(--transition-normal); }
.team-row.expanded .expand-icon { transform: rotate(180deg); }
.team-details { display: none; padding: var(--space-3) var(--space-5) var(--space-5); background: var(--surface-sunken); border-top: 1px solid var(--surface-tint); }
.team-row.expanded .team-details { display: block; }
@media (min-width: 769px) {
    /* Desktop keeps the name/points column on the left and the whole stack on
       the right, side by side. */
    .team-summary {
        padding: var(--space-4) var(--space-6);
        column-gap: var(--space-5);
        grid-template-columns: auto minmax(0, 1fr) auto auto;
        grid-template-areas:
            "rank name  meta chev"
            "rank score meta chev";
    }
    .team-rank { font-size: var(--text-lg); min-width: 34px; }
    .team-name { font-size: var(--text-lg); gap: var(--space-3); }
    .team-score { font-size: var(--text-md); }
    .team-averages { font-size: var(--text-base); gap: var(--space-1) var(--space-8); }
    .pos { font-size: var(--text-base); padding: 3px var(--space-4); }
    .expand-icon { font-size: var(--text-md); }
}
@media (min-width: 1200px) { .team-name { font-size: var(--text-xl); } }
@media (max-width: 768px) {
    /* Phone: the name gets the full width of its own line (schools spell out
       long), and the points move down to sit at the left of the places/paces
       stack instead of taking a line of their own — one line saved per row
       without crowding the name. */
    .team-summary {
        padding: var(--space-2) var(--space-4);
        grid-template-columns: auto auto minmax(0, 1fr) auto;
        grid-template-areas:
            "rank name  name chev"
            "rank score meta chev";
    }
    .team-meta { align-self: start; }
    .team-score { align-self: end; }   /* points level with the paces line */
}
/* Larger text on a phone (html.text-lg, main.js): the points and the three
   paces no longer fit one line, and wrapping put the run pace under the
   points. The places and paces take a line of their own instead. */
@media (max-width: 768px) {
    .text-lg .team-summary {
        grid-template-columns: auto minmax(0, 1fr) auto;
        grid-template-areas:
            "rank name  chev"
            "rank score chev"
            "rank meta  chev";
    }
    .text-lg .team-meta { justify-self: stretch; align-items: stretch; }
    .text-lg .team-averages { justify-content: space-between; flex-wrap: nowrap; gap: var(--space-1) var(--space-4); }
    .text-lg .avg { min-width: 0; }
    .text-lg .scoring-positions { justify-content: flex-start; }
}

.athletes-table { width: 100%; border-collapse: collapse; font-size: var(--text-xs); }
.athletes-table th { text-align: left; padding: 3px var(--space-3); font-weight: var(--font-weight-semibold); color: var(--text-muted); border-bottom: 1px solid var(--border); font-size: var(--text-micro); text-transform: uppercase; }
.athletes-table td { padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--surface-tint); }
/* Cap the name column on phones so a long name wraps to ~2 lines instead of
   stealing width from the swim/bike/run pace columns (#6). overflow-wrap keeps
   even a long single token from forcing the cell wider. Lifted on desktop. */
.athletes-table .athlete-name-cell { font-weight: var(--font-weight-semibold); max-width: 8.5em; overflow-wrap: anywhere; line-height: 1.25; }
.athletes-table .time-cell { font-family: var(--font-family-mono); font-size: var(--text-xs); }
.athletes-table .pace-cell { font-size: var(--text-micro); color: var(--text-muted); }
.athletes-table tr.displacer { opacity: 0.7; background: var(--surface-tint); }
.divider-row td { font-size: var(--text-micro); font-weight: var(--font-weight-semibold); color: var(--text-faint); text-transform: uppercase; padding: var(--space-3); border-bottom: 1px dashed var(--border) !important; background: var(--surface-tint); }
.scoring-note { font-size: var(--text-xs); color: var(--text-muted); margin-top: var(--space-4); padding: var(--space-3) var(--space-5); background: var(--warning-bg); border-left: 3px solid var(--warning); border-radius: var(--radius-sm); }
.scoring-rules { margin-top: var(--space-4); padding: var(--space-4) var(--space-5); background: var(--surface-hover); border-radius: var(--radius-md); font-size: var(--text-xs); color: var(--text-muted); border: 1px solid var(--accent); }
.scoring-rules strong { color: var(--accent); }
@media (min-width: 769px) {
    .athletes-table { font-size: var(--text-md); }
    .athletes-table th { font-size: var(--text-base); padding: var(--space-3) var(--space-5); }
    .athletes-table td { padding: var(--space-3) var(--space-5); }
    .athletes-table .athlete-name-cell { max-width: none; }
    .athletes-table .time-cell { font-size: var(--text-md); }
    .athletes-table .pace-cell { font-size: var(--text-base); }
    .divider-row td { font-size: var(--text-base); padding: var(--space-4); }
    .scoring-note { font-size: var(--text-md); padding: var(--space-4) var(--space-7); }
    .scoring-rules { font-size: var(--text-md); padding: var(--space-5) var(--space-7); }
}

.spider-table-wrap {
    overflow-x: auto;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    min-width: 0;
}
.spider-tbl { width: 100%; border-collapse: collapse; font-size: var(--text-sm); min-width: 400px; }
.spider-tbl thead th {
    padding: var(--space-3) var(--space-2); font-size: var(--text-xs); font-weight: var(--font-weight-bold); color: var(--text-muted);
    background: var(--surface-hover); border-bottom: 2px solid var(--border); text-align: center;
    position: sticky; top: 0; z-index: 5; text-transform: uppercase; letter-spacing: 0.3px;
}
.spider-tbl thead th:first-child { text-align: left; z-index: 10; }
/* Opaque base so the sticky first column (background: inherit) always masks the
   scrolling value columns behind it — odd/unstriped rows were transparent, so
   text bled through the name on horizontal scroll. */
.spider-tbl tbody tr { border-bottom: 1px solid var(--surface-tint); background: var(--surface); }
.spider-tbl tbody tr:nth-child(even) { background: var(--surface-sunken); }
.spider-tbl tbody tr:hover { background: var(--surface-hover); }
/* 44px minimum cell height on mobile so each row is a finger-friendly
   target (the per-row ✕ button and athlete cell both inherit this). */
.spider-tbl td { padding: var(--space-4) var(--space-2); vertical-align: middle; text-align: center; }
.spider-tbl td:first-child { text-align: left; position: sticky; left: 0; background: inherit; z-index: 2; min-width: 110px; }
.spider-tbl .ath-cell { display: flex; align-items: center; gap: var(--space-2); }
.spider-tbl .cdot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
/* Names wrap rather than truncate: an ellipsis here hid text while the cell still
   had ~40px of spare width, and a half-name is worth less than a second line.
   The max-width is a brake on the column stealing room from the split columns
   (which start wrapping "42.3 km/h"), NOT a clip: anything past it wraps. */
.spider-tbl .aname {
    font-weight: var(--font-weight-semibold); font-size: var(--text-sm);
    white-space: normal; overflow-wrap: anywhere; line-height: 1.2;
    max-width: 124px;
}
.spider-tbl .acountry { font-size: var(--text-micro); color: var(--text-muted); }
/* Pace under each split. Was inline 9px, which was too small to read at a glance
   for the number people most want off this table; --text-sm matches the grid's
   own body size and scales up again on mobile. Also carries the relay member
   name in the Teams lens. */
.spider-tbl .tpace { font-size: var(--text-sm); color: var(--text-muted); line-height: 1.2; }
/* The unit, stated once per column under its name (cells hold bare numbers). */
.spider-tbl thead th .tunit {
    display: block; margin-top: 1px; font-size: var(--text-micro); font-weight: var(--font-weight-normal);
    color: var(--text-muted); text-transform: none; letter-spacing: 0;
}
/* White tile + hairline ring: most NCAA logos ship on a solid white background,
   so a bare <img> shows a white rectangle on coloured rows / dark mode. The tile
   normalises every logo (white-bg blends in, transparent gets a legible backing)
   into one cohesive badge. --logo-tile stays white in both themes (see tokens). */
/* The tile is a FIXED shape and every mark is contained inside it, so a row of
   badges reads as one set. `aspect-ratio` rather than a hard width, so the
   handful of rules that override only `height` still get a proportional tile.
   6/5 is the median shape of the trimmed logos.

   This has to be a fixed box. The source files used to be a uniform 800x640
   canvas, which normalised every badge for free — but the mark inside was
   floating in dead space, which is what made crests unreadable. Trimming the
   canvas to the mark (scripts/trim_team_logos.py) fixed the size but exposed a
   5.3x spread in aspect ratio, so `width: auto` then rendered badges anywhere
   from 0.66em to 2.75em wide. Trimmed source + fixed tile gives both: one tile
   shape, and each mark scaled up to fill as much of it as its shape allows. */
.team-logo {
    height: 1.3em; aspect-ratio: 6 / 5; width: auto;
    object-fit: contain; object-position: center; vertical-align: middle;
    background: var(--logo-tile); padding: 1px; border-radius: var(--radius-sm);
    box-shadow: 0 0 0 1px var(--logo-tile-ring);
}
/* Transfer athlete: one crest per school, sitting together with a hair of gap. */
.team-logos { display: inline-flex; align-items: center; gap: var(--space-1); vertical-align: middle; }
.spider-tbl .tval { font-weight: var(--font-weight-medium); }
.spider-tbl .rbadge {
    display: inline-block; padding: 1px var(--space-2); border-radius: var(--radius-lg);
    font-size: var(--text-micro); font-weight: var(--font-weight-bold); margin-top: 1px;
}
/* .rbadge tier colors live in chart-theme.css (single source). */
.spider-tbl .tpace { font-size: var(--text-nano); color: var(--accent-ink); margin-top: 1px; }
.spider-tbl .spider-remove-col { width: 24px; padding: 0; }

/* ── Splits grid (WT-style sortable results table) ───────────────────────── */
.spl-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; border: 1px solid var(--border); border-radius: var(--radius-lg); }
.spl-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); font-variant-numeric: tabular-nums; --spl-name-w: 7.5em; --spl-pos-w: 2.6em; }

/* Header scrolls away with the table (no top-sticky) — on a phone a pinned
   header row otherwise floats over the app chrome above as you scroll. */
.spl-table thead th {
    padding: var(--space-3) var(--space-2); font-size: var(--text-xs); font-weight: var(--font-weight-bold);
    color: var(--text-2); background: var(--surface-hover); border-bottom: 2px solid var(--border);
    text-align: center; white-space: nowrap; user-select: none;
}
.spl-th[data-key] { cursor: pointer; }
.spl-th[data-key]:hover { filter: brightness(0.97); }
/* Sorted-column underline + arrow colour are set inline per column (discipline
   colour for swim/bike/run, neutral otherwise) — never purple. */
/* Sort mark: ▲ over ▼ under the column name, so it adds height, not width.
   Faint on every sortable column; the sorted direction takes the column's ink. */
.spl-sort { display: flex; flex-direction: column; align-items: center; margin-top: 2px; font-size: var(--text-nano); line-height: 7px; color: var(--text-faint); }
.spl-th-name .spl-sort { align-items: flex-start; }
/* The name header starts where the names start: past the flag and the gap. */
.spl-table thead th.spl-th-name { padding-left: calc(var(--space-2) + 16px + var(--space-2)); }
.spl-th-name { text-align: left !important; }

/* The name is frozen (after the place, on a phone). It is always the full
   name: on a phone it wraps to the longest word (.spl-nm), so it never needs to
   shrink when you scroll. The divider marks the frozen edge. */
/* `1%` is the shrink-to-fit width in a table that is itself width:100% — the cell
   takes what its content needs and no more. It matters because SOMETHING has to
   absorb the leftover pixels on a wide desktop, and while this column was `auto`
   it was the only candidate: it grew into a band of dead space with the splits
   shoved against the right edge. The split columns share the slack instead (see
   .spl-splitcol). --spl-rest is then the column's real content width, measured
   after layout (_pinRestWidth). */
.spl-th-name, .spl-name {
    position: sticky; left: var(--spl-pos-w); z-index: 6;
    width: var(--spl-rest, 1%);
    /* A table cell's `width` is only a MINIMUM under auto layout — the column can
       still be handed slack — so the measured content width has to be a max-width
       as well, or the name column silently re-absorbs the leftover space. */
    max-width: var(--spl-rest, none);
    white-space: nowrap;
    border-right: 1px solid var(--border);
}
.spl-th-name, .spl-th-pos { z-index: 15; }

/* Row backgrounds MUST be opaque — the sticky columns inherit them, and any
   translucency lets scrolled-under cells show through the pinned columns. */
.spl-table tbody tr { background: var(--surface); border-bottom: 1px solid var(--border-soft); }
.spl-table tbody tr:nth-child(even) { background: var(--surface-sunken); }
.spl-table tbody tr:hover { background: var(--surface-hover); }
.spl-row { cursor: pointer; }
.spl-row-sel, .spl-row-sel:hover { background: var(--surface-selected) !important; }
.spl-row-dnf { color: var(--text-faint); }

.spl-cell { padding: var(--space-3) var(--space-2); text-align: center; vertical-align: middle; white-space: nowrap; }
/* Frozen name: opaque (inherits row bg) so scrolling splits pass cleanly beneath. */
.spl-name { background: inherit; text-align: left; font-weight: var(--font-weight-semibold); color: var(--text); vertical-align: middle; }
.spl-namewrap { display: flex; align-items: flex-start; gap: var(--space-2); }
/* Phone: first name beside the flag, surname under it beside the start number. */
.spl-first, .spl-last { display: block; }
.spl-nobr { white-space: nowrap; }
/* Phone shows the one family name the rest of the app uses; desktop the whole
   surname field (splits-table.js _nameLines). */
.spl-last-full { display: none; }
/* The flag, with the start number under it: the flag column is already as wide
   as a 3-digit bib, so the number costs no width at all. */
.spl-flag { display: inline-flex; flex-direction: column; align-items: center; gap: 2px; flex: none; }
.spl-flag > * { margin: 0; }
.spl-flag .spl-bib { margin: 0; }
/* Names WRAP rather than truncate: the column is as wide as the longest word, so
   "Candela Sánchez Touza" takes two lines instead of half the screen, and the
   whole name is always there. */
.spl-name .spl-nm { white-space: normal; max-width: var(--spl-name-w); line-height: 1.2; }
/* A split cell's time/rank halves: inert on a phone, two columns on a desktop. */
.spl-pair, .spl-half { display: contents; }
/* The phone layout (the default) shows neither of these: its pick box rides
   under the place and its start number under the flag. */
.spl-pickcol, .spl-th-bib, .spl-bibcell { display: none; }
.spl-bibcell { color: var(--text-muted); font-size: var(--text-xs); }

/* DESKTOP: width to spare and height to save. The pick box and the start number
   get columns of their own, every cell is one line (a split's rank in brackets
   beside its time), and the sort mark sits beside the heading. Only the name is
   frozen, since the columns to its left are not needed while scrolling. */
@media (min-width: 769px) {
    .spl-table .spl-name .spl-nm { white-space: nowrap; max-width: none; }
    .spl-table .spl-pickcol { display: table-cell; width: 1.6em; padding-left: var(--space-3) !important; padding-right: var(--space-1) !important; }
    .spl-table .spl-th-bib, .spl-table .spl-bibcell { display: table-cell; width: 3.2em; }
    .spl-table .spl-pos .spl-box, .spl-table .spl-flag .spl-bib { display: none; }
    .spl-table .spl-pickcol .spl-box { margin: 0 auto; }
    .spl-table .spl-th-pos, .spl-table .spl-pos { position: static; width: 2.8em; min-width: 2.8em; max-width: none; padding-left: var(--space-2) !important; }
    .spl-table .spl-th-name, .spl-table .spl-name { left: 0; }
    .spl-table .spl-cell { padding-top: var(--space-2); padding-bottom: var(--space-2); }
    .spl-table .spl-time, .spl-table .spl-rk, .spl-table .spl-posnum { display: inline; }
    .spl-table .spl-rk { margin: 0; font-size: var(--text-xs); }
    .spl-table .spl-pair { display: grid; align-items: center; }
    /* A split cell: the times right-aligned in a slot as wide as the column's
       widest time (measured, _fitTimeSlots), starting at the cell's padding
       exactly like the header above it; then the place, right-aligned in a
       slot as wide as the column's widest place and centred in the rest of
       the column, so it has its own space rather than trailing the time. */
    .spl-table { --spl-tw: 4.4em; --spl-rw: 2ch; }
    .spl-table .spl-th.spl-splitcol, .spl-table .spl-cell.spl-splitcol { width: var(--spl-colw, auto); }
    .spl-table .spl-pair { grid-template-columns: var(--spl-tw) 1fr; column-gap: var(--space-2); }
    .spl-table .spl-half { display: block; text-align: center; }
    .spl-table .spl-half:first-child { text-align: right; }   /* times line up on the last digit */
    .spl-table .spl-half .spl-rk { display: inline-block; width: var(--spl-rw); text-align: right; }
    /* Pace lens: the unit sits at the far right of the header, not between
       the name and its sort mark, so it never widens the column. */
    .spl-table thead th.spl-splitcol { position: relative; }
    .spl-table thead th.spl-splitcol .spl-unit { position: absolute; right: var(--space-2); top: 50%; transform: translateY(-50%); margin: 0; }
    .spl-table .spl-first, .spl-table .spl-last { display: inline; }
    .spl-table .spl-last-short { display: none; }
    .spl-table .spl-last-full { display: inline; }
    .spl-table .spl-namewrap { align-items: center; }
    /* Every header starts where its numbers start: the same left edge, the
       same padding. The name header is pushed past the flag (16px + the gap)
       so it sits over the names. */
    .spl-table thead th, .spl-table .spl-cell { text-align: left; }
    .spl-table .spl-sort { display: inline-flex; vertical-align: middle; margin: 0 0 0 var(--space-1); }
}
/* The place: the first frozen column, as narrow as a 2-digit place in its ring
   allows. Its width is the name column's sticky offset, so the two are one token. */
.spl-th-pos, .spl-pos {
    position: sticky; left: 0; z-index: 6;
    width: var(--spl-pos-w); min-width: var(--spl-pos-w); max-width: var(--spl-pos-w);
    padding-left: var(--space-1) !important; padding-right: var(--space-1) !important;
}
.spl-pos { background: inherit; color: var(--text-muted); font-weight: var(--font-weight-semibold); }
/* Start number under the name: muted, small, never a column of its own. */
.spl-bib { display: block; margin-top: 1px; font-size: var(--text-micro); font-weight: var(--font-weight-normal); color: var(--text-faint); line-height: 1; }
/* Pick affordance: a small square checkbox under the place (straight lines,
   like the grid), filled with the athlete's chart colour and ticked once picked. */
.spl-posnum { display: block; line-height: 1.15; }
.spl-box {
    position: relative; display: block; margin: 3px auto 0; width: 12px; height: 12px; box-sizing: border-box;
    border: 1.5px solid var(--border-strong); border-radius: 2px;
}
.spl-row-sel .spl-box { background: var(--pick, var(--accent)); border-color: var(--pick, var(--accent)); }
.spl-row-sel .spl-box::after {
    content: ''; position: absolute; left: 2.5px; top: -0.5px; width: 3px; height: 6px;
    border: solid var(--on-accent); border-width: 0 1.5px 1.5px 0; transform: rotate(45deg);
}
/* Every split column shares one width so the grid reads with even spacing (T1/T2
   would otherwise sit narrower than swim/bike/run and stagger the gaps). `auto`
   over a floor rather than a fixed width, so on a wide desktop the leftover space
   is shared out across the numbers instead of pooling in one dead column. */
.spl-th.spl-splitcol, .spl-cell.spl-splitcol { width: auto; min-width: 3.6em; }
/* Pace lens: the unit is stated once under the column name, so the cells hold a
   bare number ("3:52") and the columns need only a little more room than a time. */
.spl-scroll.spl-pace .spl-th.spl-splitcol,
.spl-scroll.spl-pace .spl-cell.spl-splitcol { min-width: 4.2em; }
.spl-unit {
    display: block; margin-top: 1px; font-size: var(--text-micro); line-height: 1;
    font-weight: var(--font-weight-normal); color: var(--text-faint); text-transform: none;
    letter-spacing: 0;
}
.spl-status { font-size: var(--text-micro); font-weight: var(--font-weight-bold); color: var(--text-faint); }

.spl-time { display: block; font-weight: var(--font-weight-medium); line-height: 1.15; }
.spl-rk { display: block; margin-top: 1px; font-size: var(--text-micro); line-height: 1; color: var(--text-faint); }
/* Sorted-column tint + fastest-split colour are applied inline per column
   (discipline colour), so there are no purple .spl-best / .spl-cell-active rules. */
.spl-empty { color: var(--text-faint); }
/* An estimated split reads as qualified, not measured: muted, and carrying the
   ** the legend under the grid explains. */
.spl-est .spl-time { color: var(--text-muted); }
/* The ** on an estimated value. Sized against the number it marks, so it scales
   with whatever cell it lands in. */
.spl-estmark { font-size: 0.85em; letter-spacing: -0.5px; margin-left: var(--space-1); }
/* The ** hangs past the last digit rather than widening the time, so an
   estimate is centred (phone) or right-aligned (desktop) exactly like the
   measured times around it. */
.spl-est .spl-time { display: inline-block; position: relative; }
.spl-est .spl-estmark { position: absolute; left: 100%; top: -0.2em; margin-left: 1px; }
/* A time penalty served in this segment. Sits under the split like the rank
   does, in the warning ink so it reads as a note on the time and never as a
   second time. */
.spl-penmark {
    display: block;
    margin-top: 1px;
    font-size: var(--text-micro);
    line-height: 1;
    font-family: var(--font-family-mono);
    color: var(--warning);
}
/* A quiet one-line note under a chart, grid or table: what a mark means, or how
   a table was ranked. Never a boxed callout, so it explains without competing
   with the numbers above it. */
.chart-note, .chart-estimate-note {
    display: flex; align-items: center; justify-content: center;
    gap: var(--space-1); text-align: center;
    font-size: var(--text-xs); color: var(--text-muted);
    padding: var(--space-3) var(--space-4) var(--space-1);
    line-height: 1.4;
}
.ce-mark { font-weight: var(--font-weight-medium); }
/* ── Relay lens toggle + leg filter — both are shared `.switcher`s ────────── */
.spl-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); }
/* The leg filter can run to 5+ options, so it scrolls rather than clipping. */
.spl-legfilter { margin-left: auto; }

/* Leg column (Athletes mode): narrow, muted, sortable — groups the grid by leg. */
.spl-th-leg, .spl-legcell { width: 2.6em; min-width: 2.6em; }
.spl-legcell { color: var(--text-muted); font-size: var(--text-xs); font-weight: var(--font-weight-semibold); }

/* Athlete row in a relay: the team name rides under the runner's name so the
   frozen column still answers "whose leg was that?" when sorted by bike. */
.spl-name .spl-nm { line-height: 1.15; }
.spl-team { display: block; font-size: var(--text-micro); font-weight: var(--font-weight-normal); color: var(--text-muted); }

/* What the timer logged against the athlete: why a race ended, or a penalty
   served during it. Rides under the name like the relay team line. */
.spl-note { display: block; font-size: var(--text-micro); font-weight: var(--font-weight-normal); color: var(--danger); white-space: normal; }

@media (max-width: 480px) {
    /* Phone: the leg filter drops to its own row under the lens toggle rather
       than fighting it for the same line. */
    .spl-legfilter { margin-left: 0; }
}
.spider-tbl .spider-remove-cell { padding: var(--space-1); width: 24px; }
.spider-remove-btn {
    padding: var(--space-1) var(--space-2); border-radius: var(--radius-sm);
    color: var(--border-strong); font-size: var(--text-sm);
}
.spider-remove-btn:hover { background: var(--danger-bg); color: var(--danger); }
/* Spider Teams mode (NCAA): a team row and its opened athletes are controls
   (tap to put that squad or athlete on the radar), so they read as tappable.
   Rows outside the scoring five are dimmed, not hidden: they are the depth the
   "All" average is taken over. */
.spider-tbl tr.spider-team-row, .spider-tbl tr.spider-team-athlete { cursor: pointer; }
.spider-tbl tr.spider-team-athlete td:first-child { padding-left: var(--space-5); }
.spider-tbl tr.nonscoring td { opacity: 0.62; }
/* The Teams-lens tables size their columns instead of letting the auto layout
   hand the slack to the name: with a 110px name floor and a 400px table floor,
   Run was pushed past the right edge of a phone and only reachable by scrolling
   a table most people never scroll. Percentages, so the same rule holds in the
   narrow desktop column beside the radar. */
.spider-tbl--fit { table-layout: fixed; min-width: 0; }
.spider-tbl--fit td, .spider-tbl--fit th { padding-left: var(--space-1); padding-right: var(--space-1); }
.spider-tbl--fit td:first-child { min-width: 0; }
.spider-tbl--fit .aname { max-width: none; }
.spider-tbl--fit col.sc-name { width: 32%; }
.spider-tbl--fit col.sc-leg { width: 14%; }
.spider-tbl--fit col.sc-tran { width: 9%; }
.spider-tbl--fit col.sc-act { width: 6%; }
.spider-expand-btn { padding: var(--space-1) var(--space-2); font-size: var(--text-sm); }
.spider-expand-btn:hover { color: var(--accent); }
@media (min-width: 769px) {
    /* Desktop: the name has room to spell a school out. */
    .spider-tbl--fit col.sc-name { width: 34%; }
    .spider-tbl--fit col.sc-leg { width: 14%; }
    .spider-tbl--fit col.sc-tran { width: 9%; }
    .spider-tbl--fit col.sc-act { width: 6%; }
    .spider-tbl { font-size: var(--text-md); }
    .spider-tbl thead th { font-size: var(--text-base); padding: var(--space-4); }
    .spider-tbl td { padding: var(--space-3); }
    .spider-tbl .aname { font-size: var(--text-md); max-width: 200px; }
    .spider-tbl .acountry { font-size: var(--text-base); }
    .spider-tbl .tval { font-size: var(--text-md); }
    .spider-tbl .rbadge { font-size: var(--text-sm); padding: 3px var(--space-4); }
    .spider-tbl .tpace { font-size: var(--text-sm); }
    .spider-tbl td:first-child { min-width: 140px; }
    .spider-tbl .spider-remove-col, .spider-tbl .spider-remove-cell { width: 32px; }
    .spider-remove-btn { font-size: var(--text-lg); padding: var(--space-3) var(--space-5); }
    .spider-expand-btn { font-size: var(--text-md); }
}
@media (min-width: 1200px) {
    .spider-tbl { font-size: var(--text-lg); }
    .spider-tbl .aname { font-size: var(--text-lg); }
}

/* ========================================
   CHECKPOINT NAV
   ======================================== */
.checkpoint-navigation { display: flex; align-items: center; justify-content: center; gap: var(--space-6); padding: var(--space-6); background: linear-gradient(135deg, var(--accent), var(--accent-hover)); border-radius: var(--radius-xl); margin-bottom: var(--space-5);
    /* Checkpoint nav (Teams): gradient hero → toolbar. Always-dark chrome so its white-text children stay legible in both themes (--ink-2 would flip light). */
    /* folded up from the SaaS layer 2026-09-03 */
    background: var(--surface-inverse);
    border-radius: var(--radius-lg);
}
.checkpoint-nav-btn { width: 36px; height: 36px; border-radius: 50%; background: var(--surface); border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.checkpoint-nav-btn.disabled { opacity: 0.3; cursor: not-allowed; }
.checkpoint-nav-btn .arrow { font-size: var(--text-lg); color: var(--accent); font-weight: bold; }
.checkpoint-info { text-align: center; color: white; min-width: 160px; }
.checkpoint-label { font-size: var(--text-xs); opacity: 0.9; text-transform: uppercase; letter-spacing: 1px; }
.checkpoint-name { font-size: var(--text-xl); font-weight: var(--font-weight-bold); margin: var(--space-2) 0; }
.checkpoint-indicator { display: flex; gap: var(--space-3); justify-content: center; margin-top: var(--space-3); }
.checkpoint-indicator .dot { width: 7px; height: 7px; border-radius: 50%; background: rgba(255, 255, 255, 0.3); }
.checkpoint-indicator .dot.active { background: white; transform: scale(1.5); }
.checkpoint-indicator .dot.passed { background: rgba(255, 255, 255, 0.6); }
@media (min-width: 769px) {
    .checkpoint-label { font-size: var(--text-md); letter-spacing: 1.2px; }
    .checkpoint-name { font-size: var(--text-2xl); }
    .checkpoint-info { min-width: 200px; }
}
@media (max-width: 768px) {
    .checkpoint-navigation { padding: var(--space-5); gap: var(--space-4); }
    .checkpoint-name { font-size: var(--text-lg); }
    .checkpoint-info { min-width: 120px; }
}

/* ========================================
   ATHLETE BROWSER (.ab-*)
   ======================================== */
.ab-results { display: flex; flex-direction: column; gap: var(--space-2); max-height: 50vh; overflow-y: auto; }
.ab-section-title { font-size: var(--text-eyebrow); font-weight: var(--font-weight-bold); color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.5px; padding: var(--space-3) var(--space-2) var(--space-1); }
.ab-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-5);
    font-size: var(--text-md);
    text-align: left;
    cursor: pointer;
    min-height: 44px;
    -webkit-tap-highlight-color: transparent;
    position: relative;
}
.ab-row:active { background: var(--accent-subtle); }
.ab-name { flex: 1; font-weight: var(--font-weight-semibold); color: var(--text-2); }
.ab-sub { font-size: var(--text-sm); color: var(--text-muted); }
.ab-races-head { font-size: var(--text-md); padding: var(--space-1) 0 var(--space-3); color: var(--text-2); }

.ab-star {
    font-size: var(--text-lg); color: var(--border-strong);
    padding: var(--space-2) var(--space-3); cursor: pointer; line-height: 1;
    flex-shrink: 0; border-radius: var(--radius-sm);
    transition: color var(--transition-fast), background var(--transition-fast);
    -webkit-tap-highlight-color: transparent;
}
.ab-star:hover { color: var(--warning); background: var(--surface-hover); }
.ab-star.on { color: var(--major-accent); }
.ab-icon {
    flex-shrink: 0; min-width: 22px;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: var(--text-lg);
    text-align: center;
}
/* Shape + fit come from .team-logo; only the scale is local here. */
.ab-icon img.team-logo { height: 1.4em; }
.ab-yob { font-size: var(--text-sm); color: var(--text-faint); font-weight: var(--font-weight-normal); }
/* Start-list race number. Tabular + fixed width so the names stay in a column. */
.ab-bib {
    flex: none; min-width: 2.2em; text-align: right;
    font-family: var(--font-family-mono); font-variant-numeric: tabular-nums;
    font-size: var(--text-sm); color: var(--text-muted);
}
/* No number assigned yet (a late entry, added after the organiser drew the
   numbers). Fainter than a real one so the column still reads as numbers. */
.ab-bib-none { color: var(--text-faint); }
/* Entered but not yet confirmed. Neutral, not amber or red: a waitlist position
   is a fact about the entry, not a problem with the athlete. These are excluded
   from the forecast until they are official, so the badge is also the reason a
   name on the start list is absent from the form guide. */
.ab-wait {
    flex: none;
    font-size: var(--text-xs); font-weight: var(--font-weight-bold);
    padding: 1px var(--space-2); border-radius: var(--radius-sm);
    background: var(--badge-neutral-bg); color: var(--badge-neutral-ink);
}

/* ========================================
   ATHLETE PROFILE (.ap-*)
   ======================================== */
#athleteProfileSection { display: none; }
#athleteProfileSection[style*="block"] { display: block !important; }
#athleteProfileContent { background: var(--surface); border-radius: var(--radius-xl); padding: var(--space-4); }

.ap-header {
    display: flex; gap: var(--space-6); align-items: flex-start; padding: var(--space-5);
    background: linear-gradient(135deg, var(--accent-subtle), var(--surface-tint));
    border-radius: var(--radius-xl); margin-bottom: var(--space-6);
}
/* Race day: the athlete's race, live or about to be, above everything else
   on the profile (_liveMarkerHTML). A card with the danger stripe, and the
   landing's LIVE dot, still while it counts down and pulsing once it is on. */
.ap-live {
    display: flex; align-items: center; gap: var(--space-4);
    margin-bottom: var(--space-4); border-left-color: var(--danger);
    color: var(--text); text-decoration: none;
}
.ap-live:hover { border-color: var(--danger); }
.ap-live-body { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.ap-live-when { font-weight: 700; color: var(--danger); font-variant-numeric: tabular-nums; }
.ap-live-race { font-size: var(--text-sm); color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ap-live-go { flex: none; font-size: var(--text-sm); font-weight: 600; color: var(--accent); }
.ap-live--pre .lf-live-dot { animation: none; }
.ap-photo {
    width: 80px; height: 80px; border-radius: 50%;
    object-fit: cover; border: 3px solid var(--surface);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); flex-shrink: 0;
}
.ap-photo-blank {
    display: flex; align-items: center; justify-content: center;
    background: var(--border); font-size: 32px;
}
.ap-header-body { flex: 1; min-width: 0; }
.ap-name {
    font-size: var(--text-xl); font-weight: var(--font-weight-bold); color: var(--text-2);
    margin: 0 0 var(--space-2); display: flex; align-items: center; gap: var(--space-4);
    flex-wrap: wrap;
}
/* The door to the athlete's map (/a/{id}/map): an icon the size of the star, at
   the right end of the Discipline trend heading (her call, 5 Oct 2026). */
.ap-map-link {
    margin-left: auto;
    display: inline-flex; color: var(--border-strong); line-height: 1;
    transition: color var(--transition-fast);
    -webkit-tap-highlight-color: transparent;
}
.ap-map-link:hover { color: var(--accent); }
.ap-name-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.ap-flag { width: 22px; height: 22px; border-radius: 50%; flex-shrink: 0; }
.ap-meta { font-size: var(--text-sm); color: var(--text-muted); margin-bottom: var(--space-4); }
.ap-meta span + span { margin-left: var(--space-1); }
.ap-ranks { display: flex; gap: var(--space-5); flex-wrap: wrap; }
.ap-rank {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: var(--space-2) var(--space-5);
}
.ap-rank-val { font-size: var(--text-lg); font-weight: var(--font-weight-bold); color: var(--accent); }
.ap-rank-lbl { font-size: var(--text-micro); color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.5px; }
/* The para rank is the one of these that goes somewhere (its sport class), so
   it is a button. Same card, plus the app's "this opens something" cues. */
.ap-rank-open { cursor: pointer; font: inherit; text-align: center; }
.ap-rank-open:hover { border-color: var(--border-strong); background: var(--surface-hover); }
.ap-rank-open:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ap-rank-open .ap-rank-lbl { color: var(--accent-ink); text-decoration: underline; text-underline-offset: 2px; }
@media (min-width: 769px) {
    .ap-photo { width: 120px; height: 120px; }
    .ap-name { font-size: var(--text-3xl); }
    .ap-meta { font-size: var(--text-md); }
    .ap-rank-val { font-size: var(--text-2xl); }
    .ap-rank-lbl { font-size: var(--text-base); }
}

.ap-fav-btn {
    background: transparent; border: none; cursor: pointer;
    font-size: var(--text-2xl); color: var(--border-strong); padding: 0 var(--space-2);
    line-height: 1; flex-shrink: 0;
    transition: color var(--transition-fast), transform var(--transition-fast);
    -webkit-tap-highlight-color: transparent;
}
.ap-fav-btn:hover { color: var(--warning); }
.ap-fav-btn:active { transform: scale(0.9); }
.ap-fav-btn.on { color: var(--major-accent); text-shadow: 0 0 6px rgba(245, 158, 11, 0.25); }

.ap-stats {
    display: grid; grid-template-columns: repeat(3, 1fr);
    gap: var(--space-3); margin-bottom: var(--space-6);
}
.ap-stat {
    padding: var(--space-4) var(--space-2);
    text-align: center;
}
.ap-stat-val { display: block; font-size: var(--text-xl); font-weight: var(--font-weight-bold); color: var(--text-2); }
.ap-stat-lbl { display: block; font-size: var(--text-micro); color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.4px; margin-top: var(--space-1); }
@media (min-width: 769px) {
    .ap-stat-val { font-size: var(--text-3xl); }
    .ap-stat-lbl { font-size: var(--text-base); }
}

.ap-upcoming { display: flex; gap: var(--space-4); overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.ap-upcoming::-webkit-scrollbar { display: none; }
.ap-upcoming-card {
    flex: 0 0 auto;
    min-width: 180px;
}
.ap-up-date { font-size: var(--text-xs); color: var(--text-muted); }
.ap-up-title { font-size: var(--text-base); font-weight: var(--font-weight-semibold); color: var(--text-2); margin: var(--space-1) 0; }
.ap-up-prog { font-size: var(--text-xs); color: var(--text-faint); }
.ap-up-startlist { font-size: var(--text-xs); color: var(--accent); font-weight: var(--font-weight-bold); margin-top: var(--space-2); }
.ap-upcoming-card.startlist-ready { border-color: var(--accent); cursor: pointer; }
.ap-upcoming-card.startlist-ready:hover { background: var(--surface-hover); }

.ap-legend { display: flex; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-3); padding: 0 var(--space-2); }
.ap-legend-chip {
    font-size: var(--text-xs); padding: var(--space-1) var(--space-4);
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius-lg); color: var(--text-muted);
    display: inline-flex; align-items: center; gap: var(--space-2);
    max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ap-legend-dot { width: 7px; height: 7px; border-radius: 50%; }
.ap-legend-count { color: var(--text-faint); font-weight: var(--font-weight-bold); }

.ap-relay-tog {
    margin-left: auto; font-size: var(--text-sm); font-weight: 400; color: var(--text-faint);
    display: inline-flex; align-items: center; gap: var(--space-2); cursor: pointer;
}
.ap-relay-tog input { margin: 0; }

/* Best-of cards */
.ap-bests { display: flex; flex-direction: column; gap: var(--space-2); }
.ap-best-card {
    display: grid;
    grid-template-columns: auto auto 1fr;
    gap: var(--space-4);
    align-items: center;
    cursor: pointer;
    text-align: left;
    min-height: 44px;
    min-width: 0;
}
.ap-best-card:hover, .ap-best-card:active { background: var(--surface-hover); }
/* A WT card is an <a href>. It was a <button>, which takes the platform control
   font rather than the page's (docs/HISTORY.md, "Turning a <button> into an <a>"),
   so keep that look: zero specificity, the classes' own size and weight still win. */
a.ap-best-card { color: inherit; text-decoration: none; }
:where(a.ap-best-card) { font: -webkit-small-control; font: -moz-button; font-size: var(--text-md); }
.ap-best-tier, .ap-race-tier {
    font-size: var(--text-micro); font-weight: var(--font-weight-bold);
    /* A tinted pill with coloured ink, like .ab-badge and .ap-pos.pod. It was a
       SOLID tier colour under white text, which cannot clear 4.5:1 at 9px
       without going almost black: Olympics amber sat at 2.15:1. The tint keeps
       the hue legible as identity and lets the ink carry the contrast. */
    background: color-mix(in srgb, var(--tier-c, var(--text-muted)) 14%, var(--surface));
    color: var(--tier-i, var(--text-2));
    padding: 3px var(--space-4);
    border-radius: var(--radius-lg); text-transform: uppercase; letter-spacing: 0.5px;
    max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    box-sizing: border-box;
}
.ap-best-pos { font-size: var(--text-lg); font-weight: var(--font-weight-bold); color: var(--text-muted); min-width: 36px; }
.ap-best-pos.pod { color: var(--warning); }
.ap-best-info { display: flex; flex-direction: column; min-width: 0; }
/* Match .ap-race-title sizing so the same race appears at the same font
   weight + size whether it's in the best-of-tier card or the race list. */
/* ── Name label: a semibold name on one line, ellipsised if it must be ─────
   Five selectors carried byte-identical copies of this. Name styling is the
   thing that gets revisited most (surname convention, wrap-vs-truncate, tie
   ranking), and five copies meant five edits or five surfaces disagreeing.
   .stand-name sits in a denser table, so it takes the smaller step. */
.ap-best-title, .ap-race-title, .rh-pr-name, .racecard-venue, .stand-name {
    font-weight: var(--font-weight-semibold); color: var(--text-2);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ap-best-title, .ap-race-title, .rh-pr-name { font-size: var(--text-base); }
/* Beside the date in a drill card, so it takes the row's leftover width. */
.racecard-venue { font-size: var(--text-xs); min-width: 0; }
.stand-name { font-size: var(--text-md); }
.ap-best-sub { font-size: var(--text-xs); color: var(--text-faint); }
@media (min-width: 769px) {
    .ap-best-title { font-size: var(--text-md); }
}

/* Race list (groups + rows) */
.ap-race-list { display: flex; flex-direction: column; gap: var(--space-2); }
.ap-race-group { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.ap-race-group + .ap-race-group { margin-top: var(--space-5); }
.ap-race-group-head { display: flex; align-items: center; gap: var(--space-4); padding: var(--space-1) var(--space-2); margin-bottom: var(--space-1); min-width: 0; }
.ap-race-group-bar { width: 3px; height: 14px; border-radius: 2px; flex-shrink: 0; }
.ap-race-group-label {
    font-size: var(--text-sm); font-weight: var(--font-weight-bold);
    text-transform: uppercase; letter-spacing: 0.4px;
    max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ap-race-group-count { font-size: var(--text-xs); color: var(--text-faint); font-weight: var(--font-weight-bold); margin-left: auto; }
/* A season heading names a year, which is not an identity the way a tier is —
   it takes the neutral text roles instead of a tier hue, and the tier moves
   onto each row as a chip. */
.ap-race-group--season .ap-race-group-bar { background: var(--border-strong); }
.ap-race-group--season .ap-race-group-label { color: var(--text-2); font-variant-numeric: tabular-nums; }
/* Tier ⇄ Date, riding in the section title: the shared switcher, minus the
   title's uppercase eyebrow type, which the segments would otherwise inherit. */
.ap-order-switch { margin-left: auto; text-transform: none; letter-spacing: 0; }

.ap-race-row {
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
.ap-race-row:hover, .ap-race-row:active { background: var(--surface-hover); border-color: var(--border-strong); }
.ap-race-strip { display: flex; align-items: stretch; }
.ap-race-main {
    flex: 1; min-width: 0;
    display: flex; flex-direction: column; gap: var(--space-2);
    padding: var(--space-5) var(--space-6);
    background: transparent; border: 0; text-align: left; cursor: pointer;
    -webkit-tap-highlight-color: transparent; font: inherit; color: inherit;
    line-height: 1.35;
}
.ap-race-toggle {
    flex-shrink: 0; padding: 0 var(--space-6);
    background: transparent; border: 0; border-left: 1px solid var(--border);
    font-size: var(--text-md); color: var(--text-faint); cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: transform var(--transition-fast), color var(--transition-fast);
}
.ap-race-toggle:hover { color: var(--text-muted); }
.ap-race-toggle[aria-expanded="true"] { color: var(--text-2); transform: rotate(180deg); }
.ap-race-line1 { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; font-size: var(--text-xs); min-width: 0; }
/* Leads line 1 in the season view. It SHRINKS rather than wrapping the line:
   the longest tier name is twice the width of the shortest, and letting it wrap
   made two rows in ten a line taller with the finish time orphaned below. The
   cap is PHONE-ONLY (below) — a desktop row has room for the longest name and
   no reason to abbreviate it. */
.ap-race-tier { flex: 0 1 auto; min-width: 0; max-width: none; }
.ap-rd { color: var(--text-muted); }
.ap-pos {
    font-size: var(--text-sm); font-weight: var(--font-weight-bold); color: var(--text-muted);
    padding: 1px var(--space-2); border-radius: var(--radius-sm); background: var(--surface-tint);
}
.ap-pos.pod { background: var(--badge-amber-bg); color: var(--badge-amber-ink); }
.ap-pos-dnf { font-size: var(--text-xs); font-weight: var(--font-weight-bold); color: var(--badge-red-ink); padding: 1px var(--space-2); background: var(--badge-red-bg); border-radius: var(--radius-sm); }
.ap-of { font-size: var(--text-xs); color: var(--text-faint); }
.ap-rtot { margin-left: auto; font-weight: var(--font-weight-semibold); color: var(--text-2); }
@media (min-width: 769px) { .ap-race-title { font-size: var(--text-md); } }
.ap-rp { color: var(--text-faint); font-weight: var(--font-weight-normal); }
/* Race title reads differently by width: mobile shows only the venue (the group
   heading already names the tier), desktop shows the full cleaned title. */
.ap-rtt-place { display: none; }
@media (max-width: 768px) {
    .ap-rtt-full { display: none; }
    .ap-rtt-place { display: inline; }
    /* Widest the phone row can spare with the finish time still beside it, and
       enough for the letters that tell two tiers apart ("CONTINENTAL CHA…" is
       not "CONTINENTAL CUP"). The full name is on the chip's title attribute,
       and the Tier view spells it out in the heading. */
    .ap-race-tier { max-width: 128px; }
}

.ap-race-segs {
    display: none; gap: var(--space-3); flex-wrap: wrap;
    padding: var(--space-3) var(--space-5) var(--space-4); border-top: 1px solid var(--surface-tint);
}
.ap-race-row.expanded .ap-race-segs { display: flex; }
.ap-seg { display: inline-flex; align-items: baseline; gap: var(--space-2); font-size: var(--text-xs); }
.ap-si { font-size: var(--text-sm); }
.ap-st { color: var(--text-2); font-weight: var(--font-weight-semibold); }
.ap-sg { font-size: var(--text-micro); font-weight: var(--font-weight-bold); padding: 1px var(--space-2); border-radius: var(--radius-sm); }
.ap-sg.good { background: var(--badge-green-bg); color: var(--badge-green-ink); }
.ap-sg.mid  { background: var(--badge-amber-bg); color: var(--badge-amber-ink); }
.ap-sg.low  { background: var(--badge-amber-bg); color: var(--badge-amber-ink); }
.ap-sg.verylow { background: var(--badge-red-bg); color: var(--badge-red-ink); }
.ap-dash { color: var(--border-strong); }
@media (min-width: 769px) {
    .ap-st { font-size: var(--text-md); }
    .ap-sg { font-size: var(--text-sm); }
}

.ap-load-banner {
    padding: var(--space-4) var(--space-6); background: var(--accent-subtle); border: 1px solid var(--accent);
    border-radius: var(--radius-md); font-size: var(--text-base); color: var(--accent-ink); margin-bottom: var(--space-5);
}
.ap-load-banner.error { background: var(--danger-bg); border-color: var(--danger); color: var(--danger); }

.ap-tooltip {
    position: absolute; z-index: var(--z-tooltip); background: var(--scrim-tooltip);
    color: white; border-radius: var(--radius-md); padding: var(--space-4) var(--space-5);
    font-size: var(--text-sm);
    pointer-events: none; box-shadow: var(--shadow-3); max-width: 240px;
}
.ap-tt-date { font-size: var(--text-xs); color: var(--text-faint); }
.ap-tt-title { font-weight: var(--font-weight-bold); margin: var(--space-1) 0; }
.ap-tt-prog { font-size: var(--text-xs); color: var(--border-strong); }
.ap-tt-pos { font-size: var(--text-base); font-weight: var(--font-weight-bold); margin-top: var(--space-2); color: var(--warning); }
.ap-tt-hint { font-size: var(--text-micro); color: var(--text-faint); font-style: italic; margin-top: var(--space-2); }


@media (max-width: 480px) {
    .ap-best-tier { max-width: 100px; font-size: var(--text-micro); letter-spacing: 0.3px; padding: var(--space-1) var(--space-3); }
    .ap-race-group-label { font-size: var(--text-xs); letter-spacing: 0.3px; }
}

/* ========================================
   RACE HISTORY (.rh-*)
   ======================================== */
#raceHistorySection { display: none; }
#raceHistorySection[style*="block"] { display: block !important; }
#raceHistoryContent { background: var(--surface); border-radius: var(--radius-xl); padding: var(--space-4); }

.rh-header {
    padding: var(--space-6);
    background: linear-gradient(135deg, var(--warning-bg), var(--surface));
    border-radius: var(--radius-xl); margin-bottom: var(--space-6);
}
.rh-title { font-size: var(--text-xl); font-weight: var(--font-weight-black); color: var(--text-2); margin: 0 0 var(--space-1); }
.rh-sub { font-size: var(--text-base); color: var(--warning); font-weight: var(--font-weight-semibold); margin-bottom: var(--space-1); }
.rh-meta { font-size: var(--text-sm); color: var(--text-muted); }
/* .rh-divpill geometry + active come from the shared `.switcher`; only the
   spacing around the group is local. */
.rh-divfilter { margin-top: var(--space-4); }
/* Metric pills inside the trend section sit between title and chart. */
.rh-trendmodes { margin-top: 0; margin-bottom: var(--space-3); }
@media (min-width: 769px) {
    .rh-title { font-size: var(--text-3xl); }
    .rh-sub { font-size: var(--text-md); }
    .rh-meta { font-size: var(--text-md); }
}

/* Course-record cards. Discipline is signalled by:
   • a 3px left stripe in the discipline colour (.rh-record-card border-left)
   • the eyebrow label in the same colour (inline style on .rh-rec-label)
   No emoji — the time + colour + label do the work. */
.rh-records {
    /* 2×2 on phones so the four records don't stack into a tall column
       (punch-list #9); one row of four from tablet up. */
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); margin-bottom: var(--space-6);
}
/* Larger text on a phone: one record per row, so a holder's name stays whole
   (two columns pushed the page sideways at 150%). html.text-lg: main.js. */
@media (max-width: 768px) { .text-lg .rh-records { grid-template-columns: minmax(0, 1fr); } }
/* The leading stripe is .card--marked; the colour is per-record, set inline
   from the discipline. */
.rh-record-card {
    padding: var(--space-3) var(--space-4);
}
.rh-rec-body { display: flex; flex-direction: column; min-width: 0; }
.rh-rec-time { font-size: var(--text-lg); font-weight: var(--font-weight-black); color: var(--text-2); }
.rh-rec-label {
    font-size: var(--text-micro); color: var(--text-faint);
    text-transform: uppercase; letter-spacing: 0.5px; margin-top: 1px;
    font-weight: var(--font-weight-bold);
}
.rh-rec-who { font-size: var(--text-sm); color: var(--text-muted); margin-top: var(--space-1); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rh-rec-year { color: var(--text-faint); margin-left: var(--space-2); }
@media (min-width: 769px) {
    .rh-records { grid-template-columns: repeat(4, 1fr); }
    .rh-rec-time { font-size: var(--text-2xl); }
    .rh-rec-label { font-size: var(--text-sm); }
    .rh-rec-who { font-size: var(--text-md); }
}

/* Timeline drilldown card */
.rh-drilldown { margin-top: var(--space-4); }
.rh-dd-card {
    padding: var(--space-5) var(--space-6);
}
.rh-dd-head { display: flex; align-items: baseline; gap: var(--space-4); margin-bottom: var(--space-3); }
.rh-dd-year { font-size: var(--text-lg); font-weight: var(--font-weight-black); color: var(--danger); }
.rh-dd-venue { font-size: var(--text-base); color: var(--text-muted); }
.rh-dd-stats { display: flex; gap: var(--space-6); font-size: var(--text-sm); color: var(--text-muted); margin-bottom: var(--space-3); flex-wrap: wrap; }
.rh-dd-stats strong { color: var(--text-2); }
/* The drilldown podium/splits reuse the .rh-yr-podium / .rh-yr-splits display
   so they always match the year-by-year expansion. */

/* .rh-load-btn = .btn + a smaller load pill (hover from .btn:hover). */
.rh-load-btn { padding: var(--space-3) var(--space-6); font-size: var(--text-sm); }

/* Legends — shared visual language with ap-legend */
.rh-legend,
.rh-ladder-legend {
    display: flex; gap: var(--space-6); flex-wrap: wrap; margin-top: var(--space-3);
    padding: var(--space-2) var(--space-4);
    font-size: var(--text-xs); color: var(--text-muted);
}
.rh-leg { display: inline-flex; align-items: center; gap: var(--space-2); }
.rh-leg-line { width: 18px; height: 3px; border-radius: 2px; display: inline-block; }
.rh-leg-line.rh-dashed {
    /* Dash colour comes from the swatch's own --lc (set inline) so each dashed
       legend entry matches its line — currentColor made every dashed swatch
       identical, so Top 10 vs Median couldn't be told apart. */
    background-image: linear-gradient(to right, var(--lc, currentColor) 50%, transparent 50%);
    background-size: 6px 100%;
    background-color: transparent;
}
.rh-leg-box { width: 12px; height: 10px; border-radius: 2px; display: inline-block; }
.rh-dot-selected { stroke: var(--warning) !important; stroke-width: 2.5 !important; }

/* Podium-regulars rows */
.rh-podium-row {
    display: grid;
    grid-template-columns: auto 1fr auto;
    gap: var(--space-5);
    align-items: center;
    padding: var(--space-3) var(--space-5);
    margin-bottom: var(--space-2);
}
.rh-pr-medals { font-size: var(--text-md); line-height: 1;
    /* folded up from the SaaS layer 2026-09-03 */
    display: inline-flex;
    gap: var(--space-2);
    align-items: center;
    font-size: var(--text-sm);
}

.rh-pr-years { font-size: var(--text-xs); color: var(--text-faint); }
@media (min-width: 769px) {
    .rh-pr-name { font-size: var(--text-md); }
    .rh-pr-years { font-size: var(--text-base); }
}

/* Career-placings / team / repeat-starters grids.
   These ARE the season-standings matrix — competitors down, events across, a
   placing in the cell — so they wear `.t100-mx` (see the standings block) and
   only layer on the heat fill and the click affordance. The host supplies the
   panel and the height cap; `.t100-mx-scroll` handles sideways scroll and must
   not keep its edge-to-edge bleed once it's inside that bordered panel. */
/* No horizontal padding. A sticky column pins to the scrollport's PADDING edge
   while the table scrolls out to the BORDER edge, so any side padding leaves
   the pinned name column offset from the cells it's meant to sit against, with
   a sliver of the scrolled row showing past it. */
.rh-grid-host {
    overflow-x: auto; -webkit-overflow-scrolling: touch;
    background: var(--surface); border-radius: var(--radius-md);
    padding: 0; border: 1px solid var(--border-soft);
}
/* One scroll container, not two. `.t100-mx-scroll` is its own horizontal
   scroller for the standings page, but nested inside the host — which already
   scrolls vertically for the long list — it left two overlapping horizontal
   scrollers and a swipe could stall partway along the year columns. The host
   takes both axes; the inner wrapper keeps its bleed margin off in here too. */
.rh-grid-host .t100-mx-scroll { margin: 0; overflow: visible; }
/* Ink is NOT set here: the heat tier the cell is wearing owns its own colour
   (see the .heat-* block), and a colour on the cell would be a second opinion
   that happens to lose only because it is declared earlier in the file. */
.rh-cell {
    cursor: pointer;
    font-size: var(--text-xs); font-weight: var(--font-weight-bold);
}
.rh-cell:hover { outline: 2px solid var(--accent); }
.rh-cell-empty { cursor: default; background: transparent; color: var(--text-faint); }
.rh-cell-empty:hover { outline: none; }
/* Heat tiers live in the shared block near the season matrix — every grid in
   the app wears the same `.heat-*` classes now (see js/config/heat-scale.js).
   So is the cell's SIZE: these grids are the season matrix, so the column width
   comes from mxColumns() through the <colgroup> and the height from the shared
   `.mx-cell` padding. A desktop block here used to restate both (30x26, one
   type step up) — it lost to the colgroup and to `.mx-cell` further down the
   file, so it never took effect, but it read as a second opinion on a geometry
   that has one owner. */

/* Year-by-year accordion */
.rh-year-row {
    margin-bottom: var(--space-2);
    overflow: hidden;
}
/* Flex, not a fixed grid: the row carries a variable set of children (an
   optional distance tag, an optional major badge, one or two winner cells), and
   a fixed column count made the extras overflow — the chevron wrapped to a
   second row on the left and the stats got crushed into a sliver (punch-list
   #9). Flex lets the winner absorb the slack and pins the chevron on the right. */
.rh-yr-head {
    display: flex; align-items: center; gap: var(--space-4);
    padding: var(--space-4) var(--space-6);
    cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.rh-yr-head:hover { background: var(--surface-hover); }
.rh-yr-year { flex: none; font-size: var(--text-md); font-weight: var(--font-weight-black); color: var(--text-2); min-width: 36px; }
.rh-yr-winner { flex: 1 1 0; min-width: 0; font-size: var(--text-sm); color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rh-yr-stats { flex: none; margin-left: auto; font-size: var(--text-xs); color: var(--text-faint); white-space: nowrap; }
.rh-yr-chev { flex: none; font-size: var(--text-sm); color: var(--text-faint); }
.rh-yr-detail {
    display: none; padding: var(--space-4) var(--space-6) var(--space-6);
    border-top: 1px solid var(--surface-tint); background: var(--surface-sunken);
}
.rh-year-row.expanded .rh-yr-detail { display: block; }
.rh-yr-podium { width: 100%; font-size: var(--text-base); border-collapse: collapse; margin-bottom: var(--space-3); }
.rh-yr-podium td { padding: 3px var(--space-2); }
/* NCAA: an extra winner cell for the champion team (flexes alongside the
   individual winner) + a team-standings table in the detail. */
.rh-yr-team { font-weight: var(--font-weight-semibold); color: var(--text-2); }
.rh-yr-team-pts { color: var(--text-faint); font-weight: var(--font-weight-normal); font-variant-numeric: tabular-nums; }
.rh-yr-teams { width: 100%; font-size: var(--text-sm); border-collapse: collapse; margin-bottom: var(--space-3); }
.rh-yr-teams td { padding: 3px var(--space-2); }
.rh-yr-teams-cap { color: var(--text-faint); font-size: var(--text-xs); padding-bottom: var(--space-2); font-weight: var(--font-weight-semibold); }
.rh-yr-team-score { text-align: right; font-variant-numeric: tabular-nums; font-weight: var(--font-weight-bold); color: var(--text-2); }
.rh-yr-div-tag { display: inline-block; padding: 0 var(--space-2); border-radius: var(--radius-sm); background: var(--surface-hover); font-size: var(--text-xs); font-weight: var(--font-weight-bold); color: var(--text-muted); }
.rh-yr-incomplete { color: var(--warning); font-weight: var(--font-weight-bold); }
@media (max-width: 640px) {
    .rh-yr-team { display: none; }   /* shown in the expanded team-standings table */
}
.rh-yr-splits { display: flex; gap: var(--space-5); flex-wrap: wrap; margin-bottom: var(--space-3); }
.rh-yr-split {
    font-size: var(--text-sm); color: var(--text-2);
    display: inline-flex; align-items: baseline; gap: var(--space-2);
}
.rh-yr-split-who { color: var(--text-faint); font-weight: var(--font-weight-normal); }
@media (min-width: 769px) {
    .rh-yr-winner { font-size: var(--text-md); }
    .rh-yr-stats { font-size: var(--text-base); }
}

/* ── Tier ladder ("where this race sits") ── */
.rh-ladder-section { overflow: hidden; }
.rh-ladder { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.rh-ladder svg { display: block; }
/* Legend geometry is .rh-legend above; only the swatch shapes are local. */
.rh-leg-diamond {
    width: 9px; height: 9px; display: inline-block;
    transform: rotate(45deg); border: 1px solid var(--chart-halo);
}
.rh-leg-ring {
    width: 12px; height: 12px; display: inline-block; border-radius: 50%;
    border: 2px dashed var(--major-accent);
}

/* ── Ordered-category tints ──────────────────────────────────────────────
   A division or distance class sets --tint-c; the consumer decides whether that
   lands as text, a swatch or an SVG mark.

   `tint-`, not `rh-`: this is SHARED vocabulary. Race history wrote it, the
   NCAA power index's two charts use it now, and a class that wears one
   feature's prefix while another feature depends on it is the drift
   tests/shared-class-names.spec.js exists to catch. One definition builds the
   name for both (tintClass, js/config/chart-theme.js) so the mapping from a
   division to its step is stated once.

   These MUST resolve in CSS. Colours read through cssVar() in JS and written
   as inline style freeze at whichever theme was live when the section rendered,
   and switching theme does not re-render this page — which left DI wearing the
   light ramp's near-black purple on the dark ground, at about 1.2:1.

   The discipline colours alongside these (swim/bike/run on the record labels)
   stay inline on purpose: they're identical in both themes, so they cannot go
   stale the way a ramp step can. */
.tint-di   { --tint-c: var(--rh-div-3); }
.tint-dii  { --tint-c: var(--rh-div-2); }
.tint-diii { --tint-c: var(--rh-div-1); }
.tint-super-sprint { --tint-c: var(--rh-dist-1); }
.tint-sprint       { --tint-c: var(--rh-dist-2); }
.tint-standard     { --tint-c: var(--rh-dist-3); }
.tint-middle       { --tint-c: var(--rh-dist-4); }
.tint-long         { --tint-c: var(--rh-dist-5); }
.tint-none, .tint-unknown { --tint-c: var(--text-faint); }
/* Discipline tints use the INK role: these land on the records captions as
   small bold text, where the mark hues sit under 4.5:1 on white. */
.tint-overall { --tint-c: var(--warning); }
.tint-swim    { --tint-c: var(--seg-swim-ink); }
.tint-bike    { --tint-c: var(--seg-bike-ink); }
.tint-run     { --tint-c: var(--seg-run-ink); }

.tint-ink    { color: var(--tint-c); }
.tint-swatch { background: var(--tint-c); }
.tint-line   { stroke: var(--tint-c); }
.tint-mark   { fill: var(--tint-c); }

/* ── NCAA records as a division matrix ──
   Same signal as the single .rh-record-card (colour + label + time), laid out
   one column per division because each division crowns its own champion. The
   host is the bordered panel; the discipline colour arrives inline on the
   eyebrow, exactly as it does on the cards. */
.rh-records--table {
    display: block;
    overflow: hidden;
}
/* Equal thirds so the columns don't resize as names change length. */
.rh-rec-table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: var(--text-base); }
.rh-rec-table th, .rh-rec-table td {
    padding: var(--space-3); text-align: left;
    vertical-align: top; overflow-wrap: anywhere;
}
/* Each metric is a full-width caption ABOVE its row of divisions rather than a
   left-hand column. As a column it took a third of a 390px table, leaving ~90px
   per name — enough to wrap almost every surname and push the crest onto its
   own line, so some cells ran three lines deep and others one. */
.rh-rect-group th {
    padding-top: var(--space-6); padding-bottom: var(--space-1);
    border-top: 1px solid var(--border-soft);
}
.rh-rec-table tbody tr:first-child th { border-top: 0; padding-top: var(--space-2); }
/* Division headers + metric captions are eyebrows, matching .rh-rec-label. */
.rh-rec-table thead th,
.rh-rect-label {
    font-size: var(--text-micro); font-weight: var(--font-weight-bold);
    text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap;
}
.rh-rec-table thead th { padding-bottom: var(--space-2); }
.rh-rect-time {
    font-variant-numeric: tabular-nums;
    font-size: var(--text-md); font-weight: var(--font-weight-black); color: var(--text-2);
}
.rh-rect-who { font-size: var(--text-sm); color: var(--text-muted); margin-top: var(--space-1); }
.rh-rect-meta {
    display: flex; align-items: center; gap: var(--space-2);
    margin-top: var(--space-1); min-height: 1.3em;
}
.rh-rect-meta .rh-rec-year { margin-left: 0; }
.rh-rect-empty { color: var(--text-faint); }
@media (min-width: 769px) {
    .rh-rec-table th, .rh-rec-table td { padding: var(--space-4) var(--space-5); }
    .rh-rect-label, .rh-rec-table thead th { font-size: var(--text-sm); }
    .rh-rect-who { overflow-wrap: normal; }
    .rh-rect-time { font-size: var(--text-lg); }
    .rh-rect-who { font-size: var(--text-base); }
}

/* ── Heat / category strip in the loaded race view ──
   The buttons inside are the shared `.switcher`; only the strip is local. */
.heat-strip {
    display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
    margin: 0 0 var(--space-5); padding: var(--space-4) var(--space-5);
    background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--radius-lg);
}
/* Row of links out of a race to the pages it belongs to (#raceLinks). Layout
   only — the chips inside are plain .chip, so this adds a row and not a
   component. Deliberately not a .heat-strip: that is a bordered sunken panel
   for a control that changes what the race view is showing, and these change
   the page you are on. */
.race-links {
    display: flex; flex-wrap: wrap; gap: var(--space-3);
    margin: 0 0 var(--space-5);
}
.heat-strip-label {
    font-size: var(--text-eyebrow); font-weight: var(--font-weight-bold);
    letter-spacing: 0.5px; text-transform: uppercase; color: var(--text-muted);
}

/* ── Achievement / team place grids ──
   Ranked rows, cells = finishing place. Capped height + scroll so the leaders
   sit in view and the long tail is a scroll away; the header row stays pinned.
   Panel chrome comes from .rh-grid-host — only the cap is added here. */
.rh-heatdiv { margin-bottom: var(--space-4); }
.rh-grid-block { margin-bottom: var(--space-8); }
.rh-grid-block .rh-grid-host,
.rh-grid-host--capped { max-height: 320px; overflow: auto; }
/* Header pins while the list scrolls. z-index 4 clears the matrix's own sticky
   name/total columns (2), but the two header cells that are sticky in BOTH
   directions have to sit above their own row or the scrolling year headers
   slide over them and blank out "Won". */
.rh-grid-block thead th,
.rh-grid-host--capped thead th { position: sticky; top: 0; z-index: 4; }
.rh-grid-host .t100-mx thead .mx-name { z-index: 5; }
/* Cells here are the tap target (they load that year's race), unlike standings
   where the whole row is. Scrolling one into view must leave it clear of the
   pinned name/total columns — without this, a cell scrolled to the edge sits
   BEHIND the pinned column and swallows the tap. The width is the <colgroup>'s,
   which _renderPlaceGrid now computes from the card (mxColumns) and writes onto
   the table as this property; the floor below is only the fallback. */
.rh-grid-host .t100-mx { --mx-name-w: 168px; }
.rh-grid-host .t100-mx .mx-cell {
    scroll-margin-left: calc(var(--mx-name-w) + var(--space-3));
}
/* The athlete count sits where the standings say "Athlete" — same slot, so it
   keeps the eyebrow treatment rather than reading as a column name. */
.rh-grid-host .t100-mx thead .mx-name {
    font-weight: var(--font-weight-semibold); color: var(--text-muted);
}
/* Division caption above each grid. A plain eyebrow, NOT tinted by the division
   ramp: the cells below it are shaded purple by finishing quality and the ramp
   is purple by division rank, so a tinted caption put two different quantities
   in one hue, stacked. Only one division's grid is on screen at a time and the
   selected pill directly above already names it, so the tint said nothing the
   reader did not have. */
.rh-grid-head {
    font-size: var(--text-eyebrow); font-weight: var(--font-weight-bold);
    text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: var(--space-2);
    color: var(--text-muted);
}
.rh-grid--ach { font-variant-numeric: tabular-nums; }
/* A win. The gold medal fill already says so — the amber ring this used to add
   sat on top of gold, where it read as a smudge rather than a mark — so all
   that is left is the weight. */
.rh-cell-win { font-weight: var(--font-weight-black); }

/* ── Major-championship badge on a year row ── */
.rh-major-badge {
    display: inline-flex; align-items: center; gap: var(--space-2);
    font-size: var(--text-micro); font-weight: var(--font-weight-bold);
    border-radius: var(--radius-pill);
    padding: 1px var(--space-4); margin-left: var(--space-3); vertical-align: middle;
}
.rh-major-badge.rh-major-olympic { background: var(--badge-amber-bg);  color: var(--badge-amber-ink); }
.rh-major-badge.rh-major-gf      { background: var(--badge-violet-bg); color: var(--badge-violet-ink); }
.rh-major-badge.rh-major-worlds  { background: var(--badge-blue-bg);   color: var(--badge-blue-ink); }
.rh-major-badge.rh-major-olyqual { background: var(--badge-cyan-bg);   color: var(--badge-cyan-ink); }

/* ========================================
   HEAD-TO-HEAD (.h2h-*)
   ======================================== */
.h2h-page { padding: var(--space-4) var(--space-5) var(--space-16); display: flex; flex-direction: column; gap: var(--space-7); }

.h2h-pos-toggle { margin-bottom: var(--space-4); }
.h2h-win { font-weight: var(--font-weight-bold); }

.h2h-sr-splits-tbl { width: 100%; border-collapse: collapse; font-size: var(--text-base); }
.h2h-sr-splits-tbl th { font-size: var(--text-sm); font-weight: var(--font-weight-bold); padding: var(--space-2); text-align: center; }
.h2h-sr-splits-tbl td { padding: 3px var(--space-2); text-align: center; }
.h2h-srd-seg { text-align: left !important; font-weight: var(--font-weight-semibold); }
.h2h-srd-total td { border-top: 1px solid var(--border); font-weight: var(--font-weight-bold); }


.h2h-empty { text-align: center; padding: var(--space-16) var(--space-10); }
.h2h-empty-icon { font-size: 40px; margin-bottom: var(--space-4); }
.h2h-empty h2 { font-size: var(--text-xl); color: var(--text-2); margin: var(--space-4) 0; }
.h2h-empty p { font-size: var(--text-md); color: var(--text-muted); margin: var(--space-2) 0; }
.h2h-hint { font-size: var(--text-base); color: var(--text-faint); font-style: italic; }
.h2h-muted { color: var(--text-faint); font-size: var(--text-base); padding: var(--space-4) 0; text-align: center; }
.h2h-pulse { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: h2h-pulse-k 1s ease infinite; }
@keyframes h2h-pulse-k { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } }

.h2h-relay-details { margin-top: var(--space-6); }
.h2h-leg-panel {
    border: 1px solid var(--border); border-radius: var(--radius-lg);
    margin-bottom: var(--space-3); overflow: hidden;
}
.h2h-leg-head {
    display: flex; align-items: center; gap: var(--space-4);
    padding: var(--space-4) var(--space-6); cursor: pointer;
    background: var(--surface-hover); transition: background var(--transition-fast);
}
.h2h-leg-head:hover { background: var(--surface-tint); }
.h2h-leg-label { font-weight: var(--font-weight-bold); font-size: var(--text-md); min-width: 80px; }
.h2h-leg-athletes { flex: 1; font-size: var(--text-base); }
.h2h-leg-vs-txt { color: var(--text-faint); margin: 0 var(--space-2); font-size: var(--text-xs); }
.h2h-leg-delta { font-weight: var(--font-weight-bold); font-size: var(--text-base); }
.h2h-leg-chev { font-size: var(--text-md); color: var(--text-faint); transition: transform var(--transition-fast); user-select: none; }
.h2h-leg-body { display: none; padding: var(--space-4) var(--space-6) var(--space-6); }
.h2h-leg-panel.expanded .h2h-leg-body { display: block; }

.h2h-relay-rosters { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-6); margin-bottom: var(--space-6); }
@media (max-width: 480px) { .h2h-relay-rosters { grid-template-columns: 1fr; } }
.h2h-relay-roster { padding: var(--space-4) var(--space-5); background: var(--surface-hover); border-radius: var(--radius-md); }
.h2h-relay-roster-title { font-weight: var(--font-weight-bold); font-size: var(--text-md); margin-bottom: var(--space-2); }
.h2h-relay-member { font-size: var(--text-base); padding: var(--space-1) 0; }
.h2h-relay-leg-num {
    display: inline-block; font-weight: var(--font-weight-bold); font-size: var(--text-micro);
    background: var(--surface-tint); border-radius: var(--radius-sm);
    padding: 0 var(--space-2); margin-right: var(--space-2); letter-spacing: 0.04em;
}

.h2h-history-footer {
    display: flex; flex-wrap: wrap; gap: var(--space-4); align-items: center;
    margin-top: var(--space-6); padding-top: var(--space-5);
    border-top: 1px solid var(--surface-tint);
}
.h2h-more-btn {
    padding: var(--space-3) var(--space-6); font-size: var(--text-base);
    font-weight: var(--font-weight-semibold);
    color: var(--text-muted); background: var(--surface-hover);
    border: 1px solid var(--border); border-radius: var(--radius-md);
    cursor: pointer; transition: all var(--transition-fast);
}
.h2h-more-btn:hover:not(:disabled) { background: var(--surface-tint); border-color: var(--border-strong); }
.h2h-more-btn:disabled { opacity: 0.55; cursor: default; }
.h2h-history-summary { margin-left: auto; font-size: var(--text-sm); color: var(--text-faint); }

/* Cross-source link suggestions inside the Shared-Races section. Shown
   per athlete when a same-name candidate exists in the other registry
   but no confirmed link is on file yet. */
.h2h-link-suggestions {
    display: flex; flex-direction: column; gap: var(--space-2);
    margin-bottom: var(--space-5);
}
.h2h-link-suggest {
    display: flex; align-items: center; gap: var(--space-4);
    padding: var(--space-3) var(--space-5);
    background: var(--accent-subtle);
    border: 1px dashed var(--accent);
    border-radius: var(--radius-lg);
    font-size: var(--text-sm); color: var(--text-2);
}
.h2h-link-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.h2h-link-text { flex: 1; min-width: 0; }
.h2h-link-text strong { color: var(--text-2); }
.h2h-link-text em { font-style: normal; font-weight: var(--font-weight-semibold); color: var(--accent-ink); }
/* .h2h-link-confirm = .btn + compact inline pill; hover unified to accent-dark. */
.h2h-link-confirm {
    flex-shrink: 0;
    padding: var(--space-2) var(--space-5);
    font-size: var(--text-sm);
    transition: background var(--transition-fast);
}
.h2h-link-confirm:hover:not(:disabled) { background: var(--accent-hover); }
.h2h-link-confirm:disabled { opacity: 0.6; cursor: default; }
@media (min-width: 769px) {
    .h2h-link-suggest { font-size: var(--text-md); padding: var(--space-4) var(--space-6); }
    .h2h-link-confirm { font-size: var(--text-md); padding: var(--space-3) var(--space-6); }
}
@media (min-width: 769px) {
    .h2h-history-summary { font-size: var(--text-base); }
}

/* ════════════════════════════════════════════════════════════
   HEAD-TO-HEAD REDESIGN (one chart → drill-down)
   ------------------------------------------------------------
   Single shared-race timeline with a hero record above and a
   click-to-open detail drawer below. Reuses the chip, leg-panel,
   roster, split-table and link-suggestion primitives above; only
   the layout shell is new. All values come from design tokens so
   dark mode falls out for free.
   ════════════════════════════════════════════════════════════ */

.h2h-empty-icon { color: var(--text-faint); line-height: 0; }

/* ── Hero: two athlete columns flanking the win-loss record ── */
.h2h-hero {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: var(--space-4);
}
.h2h-hero-side {
    display: flex; flex-direction: column; align-items: center;
    text-align: center; gap: var(--space-1); min-width: 0;
}
.h2h-hero-flag { line-height: 0; }
.h2h-hero-flag .flag-img { width: 34px; height: 25px; border-radius: var(--radius-sm); }
.h2h-hero-name {
    font-size: var(--text-md); font-weight: var(--font-weight-black);
    line-height: var(--line-tight); overflow-wrap: anywhere;
}
.h2h-hero-noc {
    display: inline-flex; align-items: center; gap: var(--space-2);
    font-size: var(--text-sm); color: var(--text-muted);
}
.h2h-hero-mid { text-align: center; padding: 0 var(--space-3); }
.h2h-hero-record {
    display: flex; align-items: baseline; justify-content: center; gap: var(--space-2);
    font-size: var(--text-display); font-weight: var(--font-weight-black);
    font-variant-numeric: tabular-nums; line-height: 1;
}
.h2h-hero-dash { color: var(--border-strong); font-weight: var(--font-weight-normal); }
.h2h-hero-sub { font-size: var(--text-sm); color: var(--text-muted); margin-top: var(--space-2); }
@media (min-width: 769px) {
    .h2h-hero-name { font-size: var(--text-lg); }
    .h2h-hero-flag .flag-img { width: 42px; height: 31px; }
}

/* ── Chart card: header (metric chips + status) over a scrollable plot.
   min-width:0 + overflow-x on the scroller keep a wide SVG (many shared
   relay races) scrolling INSIDE the card instead of stretching the page
   — the flexbox blow-out that made relays impossible to scroll. ── */
.h2h-chart-card {
    min-width: 0;
}
.h2h-chart-head {
    display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
    padding: var(--space-4) var(--space-5);
    border-bottom: 1px solid var(--border-soft);
}
.h2h-metric-chips { flex-shrink: 1; }
.h2h-chart-status {
    display: inline-flex; align-items: center; gap: var(--space-2);
    margin-left: auto; font-size: var(--text-sm); color: var(--text-muted);
}
.h2h-chart-status:empty { display: none; }
/* Frozen y-axis gutter + horizontally scrolling plot. The gutter (.h2h-chart-axis)
   sits outside the scroll area so the scale stays visible at any scroll offset. */
.h2h-chart-scroll {
    position: relative;
    display: flex; align-items: flex-start; overflow: hidden;
    padding: var(--space-3) 0 var(--space-4) var(--space-2);
}
/* Right-edge fade hints that the timeline scrolls (toggled from JS on scroll
   state). Only the right side — the left is the frozen axis, not a scroll edge. */
.h2h-chart-scroll::after {
    content: ''; position: absolute; top: 0; bottom: 0; right: 0; width: 24px;
    pointer-events: none; opacity: 0; transition: opacity var(--transition-fast); z-index: 1;
    background: linear-gradient(to right, transparent, var(--surface));
}
.h2h-chart-scroll.more-right::after { opacity: 1; }
.h2h-chart-axis { flex: 0 0 auto; }
.h2h-chart-plot {
    flex: 1 1 auto; min-width: 0;
    overflow-x: auto; overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    /* Let a horizontal drag scroll the plot reliably (default touch-action
       biases the page's vertical scroll and makes horizontal panning fiddly);
       contain stops the scroll chaining into back-nav / page bounce. */
    touch-action: pan-x;
    overscroll-behavior-x: contain;
    padding-right: var(--space-4);
}

/* ── Detail drawer — opens under the chart when a race is tapped ── */
.h2h-detail-host { min-width: 0; }
.h2h-detail-hint {
    text-align: center; color: var(--text-faint);
    font-size: var(--text-base); font-style: italic;
    padding: var(--space-6) var(--space-5);
    border: 1px dashed var(--border); border-radius: var(--radius-lg);
}
.h2h-detail-head {
    display: flex; align-items: flex-start; gap: var(--space-4); flex-wrap: wrap;
    padding: var(--space-5) var(--space-5) var(--space-4);
    border-bottom: 1px solid var(--border-soft);
}
.h2h-detail-title { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; flex: 1; }
.h2h-detail-title strong { font-size: var(--text-md); font-weight: var(--font-weight-bold); color: var(--text-2); }
/* Loadable race title doubles as the "load this race" link (no separate button). */
.h2h-title-link {
    align-self: flex-start; max-width: 100%;
    background: none; border: 0; padding: 0;
    font: inherit; font-size: var(--text-md); font-weight: var(--font-weight-bold);
    text-align: left; white-space: normal;
    color: var(--accent); cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: color var(--transition-fast);
}
.h2h-title-link .h2h-title-txt {
    text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px;
}
/* open-in glyph signals the title loads the whole race (not just a link) */
.h2h-title-open {
    width: 0.82em; height: 0.82em; flex: none;
    margin-left: 0.35em; vertical-align: -0.1em;
    fill: none; stroke: currentColor; stroke-width: 2;
    stroke-linecap: round; stroke-linejoin: round;
}
.h2h-title-link:hover, .h2h-title-link:focus-visible { color: var(--accent-hover); outline: none; }
.h2h-detail-date { font-size: var(--text-sm); font-weight: var(--font-weight-bold); color: var(--text-muted); }
.h2h-detail-prog { font-size: var(--text-sm); color: var(--text-muted); }
.h2h-chip-now {
    align-self: flex-start;
    font-size: var(--text-micro); font-weight: var(--font-weight-bold);
    text-transform: uppercase; letter-spacing: 0.05em;
    color: var(--accent); background: var(--surface-tint);
    padding: 1px var(--space-3); border-radius: var(--radius-pill);
}
/* Result sits on its own line below the title (order:3 + full-width basis) so a
   long race title keeps the whole width and doesn't wrap early.
   Two rows — places above, times below — around a centred "v": each side hugs
   the divider, so a place never reads as part of the neighbouring time. */
.h2h-detail-res {
    order: 3; flex: 1 0 100%;
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    grid-template-areas:
        "posA  vs  posB"
        "timeA gap timeB";
    align-items: baseline;
    column-gap: var(--space-8); row-gap: var(--space-2);
    margin-top: var(--space-2);
    font-variant-numeric: tabular-nums;
}
.h2h-res-pos { font-size: var(--text-md); font-weight: var(--font-weight-black); }
.h2h-res-time { font-size: var(--text-base); font-weight: var(--font-weight-semibold); }
.h2h-res-pos-a, .h2h-res-time-a { text-align: right; }
.h2h-res-pos-b, .h2h-res-time-b { text-align: left; }
.h2h-res-pos-a { grid-area: posA; }
.h2h-res-pos-b { grid-area: posB; }
.h2h-res-time-a { grid-area: timeA; }
.h2h-res-time-b { grid-area: timeB; }
.h2h-detail-vs {
    grid-area: vs; text-align: center;
    font-size: var(--text-base); font-weight: var(--font-weight-normal);
    color: var(--text-faint);
}
.h2h-detail-gap {
    grid-area: gap; text-align: center; white-space: nowrap;
    font-size: var(--text-sm); font-weight: var(--font-weight-bold);
    color: var(--text-muted);
}
.h2h-detail-actions { display: flex; align-items: center; gap: var(--space-3); margin-left: auto; }
.h2h-detail-close {
    --icon-btn-size: 28px; --icon-btn-radius: var(--radius-md);
    border-color: var(--border); background: var(--surface);
    color: var(--text-muted); font-size: var(--text-md);
}
.h2h-detail-modes { display: flex; gap: var(--space-2); flex-wrap: wrap; padding: var(--space-4) var(--space-5) 0; }
.h2h-detail-body { padding: var(--space-5); }
.h2h-pos-chart-host { min-width: 0; overflow-x: auto; }

/* ========================================
   NCAA → WT CROSSOVER (.xo-*)
   ======================================== */
.xo-banner {
    display: flex; align-items: center; gap: var(--space-4);
    padding: var(--space-5) var(--space-6); border-radius: var(--radius-lg);
    font-size: var(--text-base); margin-bottom: var(--space-5); line-height: 1.5;
}
.xo-suggested {
    background: var(--warning-bg);
    border: 1px solid var(--warning); color: var(--warning);
}
.xo-confirmed {
    background: var(--success-bg);
    border: 1px solid var(--success); color: var(--success);
}
.xo-icon { font-size: var(--text-xl); flex-shrink: 0; }
.xo-team { color: inherit; opacity: 0.7; }
.xo-yob { font-size: var(--text-sm); color: inherit; opacity: 0.6; }
.xo-banner .team-logo { height: 16px; vertical-align: text-bottom; }
@media (min-width: 769px) { .xo-banner { font-size: var(--text-md); padding: var(--space-6) var(--space-8); } }

.xo-stats { display: flex; gap: var(--space-7); margin-bottom: var(--space-4); flex-wrap: wrap; }
.xo-stat { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); color: var(--text-muted); }
.xo-stat-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.xo-stat-val { font-weight: var(--font-weight-bold); font-size: var(--text-base); }
.xo-stat-lbl { color: var(--text-faint); }
@media (min-width: 769px) {
    .xo-stat { font-size: var(--text-md); }
    .xo-stat-val { font-size: var(--text-lg); }
}

.xo-actions { display: flex; gap: var(--space-4); flex-wrap: wrap; }
.xo-btn {
    padding: var(--space-4) var(--space-8); border-radius: var(--radius-lg);
    border: 1px solid; font-size: var(--text-base); font-weight: var(--font-weight-semibold);
    cursor: pointer; min-height: 40px;
    -webkit-tap-highlight-color: transparent;
    transition: background var(--transition-fast), border-color var(--transition-fast);
}
.xo-confirm { background: var(--success); color: var(--on-accent); border-color: var(--success);
    /* folded up from the SaaS layer 2026-09-03 */
    background: var(--accent);
    border-color: var(--accent);
}
.xo-confirm:hover { filter: brightness(0.92); }
.xo-confirm:active { filter: brightness(0.84); }
.xo-dismiss { background: var(--surface); color: var(--text-muted); border-color: var(--border-strong); }
.xo-dismiss:hover { background: var(--surface-hover); border-color: var(--text-faint); }
.xo-unlink {
    background: var(--surface); color: var(--text-faint); border-color: var(--border);
    font-size: var(--text-sm); padding: var(--space-2) var(--space-6); min-height: 32px;
}
.xo-unlink:hover { color: var(--danger); border-color: var(--danger); background: var(--danger-bg); }
/* A dismissed suggestion, kept as one quiet line so the decision stays visible
   and reversible. It used to hide the whole section, which made "not this
   person" indistinguishable from "no match found" and undoable only from the
   console. Deliberately the quietest thing in the panel — it is a footnote,
   not an offer. */
.xo-dismissed {
    display: flex; align-items: center; gap: var(--space-5); flex-wrap: wrap;
    font-size: var(--text-sm); color: var(--text-faint);
}
.xo-dismissed strong { color: var(--text-muted); font-weight: var(--font-weight-semibold); }
.xo-restore {
    background: transparent; color: var(--text-muted); border-color: var(--border);
    font-size: var(--text-sm); padding: var(--space-2) var(--space-6); min-height: 32px;
    margin-left: auto;
}
.xo-restore:hover { color: var(--accent); border-color: var(--accent); }
@media (min-width: 769px) { .xo-btn { font-size: var(--text-md); padding: var(--space-5) var(--space-10); } }

.xo-legend {
    display: flex; gap: var(--space-7); align-items: center;
    padding: var(--space-3) var(--space-4); font-size: var(--text-xs);
    color: var(--text-muted); flex-wrap: wrap;
}
.xo-leg { display: inline-flex; align-items: center; gap: var(--space-2); font-weight: var(--font-weight-semibold); }
.xo-leg-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.xo-leg-note { margin-left: auto; font-weight: var(--font-weight-normal); font-style: italic; color: var(--text-faint); }
@media (min-width: 769px) { .xo-legend { font-size: var(--text-base); } }

/* ========================================
   REDUCED MOTION
   ======================================== */
@media (prefers-reduced-motion: reduce) {
    .athlete-path, .athlete-circle { transition: none !important; }
}

/* ========================================
   T100 STANDINGS (.t100-*)
   ----------------------------------------
   Purple/violet season-story theme. The page is visualization-first:
   the only table-shaped element is the calendar. Everything else is a
   d3 chart or a click-to-drill card.
   ======================================== */
/* Plain title block (no card) for the T100 season page. */
.t100-title { margin: 0 0 var(--space-5); }
.t100-title-sub {    /* T100: fold the purple sub-theme into the app accent */
    /* folded up from the SaaS layer 2026-09-03 */
    color: var(--accent-hover);
}
.t100-venue-link { margin-top: var(--space-3); }

/* ── Gender toggle — a shared `.switcher`; only the spacing is local. ── */
.t100-gender-toggle { margin-bottom: var(--space-5); }

/* Hero charts: bleed to the section edges and drop the inner card chrome so
   they read as full-width focal elements rather than boxed thumbnails. */
.t100-season-host, .t100-pc-host {
    margin-left: calc(-1 * var(--space-5));
    margin-right: calc(-1 * var(--space-5));
    padding: var(--space-2) 0 0;
    border-left: none; border-right: none; border-radius: 0;
    background: transparent;
}
@media (min-width: 769px) {
    .t100-season-host, .t100-pc-host {
        margin-left: calc(-1 * var(--space-8));
        margin-right: calc(-1 * var(--space-8));
    }
}

/* ── Season matrix: an app-styled table with a pinned name column (left) and a
   pinned total (right); the round columns scroll between them. ── */
.t100-mx-scroll {
    overflow-x: auto; overflow-y: hidden;
    margin: 0 calc(-1 * var(--space-5));
}
@media (min-width: 769px) { .t100-mx-scroll { margin: 0 calc(-1 * var(--space-8)); } }
.t100-mx {
    border-collapse: collapse; width: 100%;
    font-size: var(--text-sm); color: var(--text-2);
}
.t100-mx th, .t100-mx td {
    padding: var(--space-3) var(--space-2);
    text-align: center; vertical-align: middle;
}
.t100-mx thead th {
    background: var(--surface-sunken); color: var(--text-muted);
    font-size: var(--text-xs); font-weight: var(--font-weight-bold);
    border-bottom: 1px solid var(--border); line-height: 1.15;
    /* The angled venue labels sit on the header's baseline, so the two upright
       headers sit on it too rather than floating at the top of a tall row. */
    vertical-align: bottom;
}
.t100-mx tbody tr { border-bottom: 1px solid var(--border-soft); cursor: pointer; }
.t100-mx tbody tr:last-child { border-bottom: none; }
/* Column widths come from the <colgroup> (equal round columns); table-layout is
   fixed so long venue headers can't stretch their column. */
/* Pinned name (left) + total (right); opaque so scrolled cells pass behind. */
.t100-mx .mx-name {
    position: sticky; left: 0; z-index: 2;
    text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    background: var(--surface); box-shadow: 1px 0 0 var(--border);
}
/* Rank, then name with its flag after it — the summary podium's order. */
/* The NAME truncates; the flag does not. Truncating the line as a whole put an
   atomic inline box (the flag <img>) at the end of an ellipsing line, and the
   browser's answer to that is to drop the text and render a bare "…" — the same
   trap UI-CONVENTIONS records for a <button> in a truncating cell. So the flag
   is a flex sibling that never shrinks, and the ellipsis belongs to the name. */
/* The NAME truncates; the flag does not. Truncating the line as a whole put an
   atomic inline box (the flag <img>) at the end of an ellipsing line, and the
   browser's answer is to drop the text and render a bare "…" — the same trap
   UI-CONVENTIONS records for a <button> in a truncating cell. line-height leaves
   room for the .athlete-link underline, 3px below the baseline, which the
   truncating box would otherwise clip. */
.mx-who-name {
    display: inline-flex; align-items: center; gap: var(--space-2);
    line-height: 1.45; max-width: calc(100% - 2.2em); vertical-align: middle;
}
.mx-who-name .athlete-link, .mx-who-name > span:first-child {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.mx-who-name .flag-img, .mx-who-name .flag-code { flex: none; }
/* The total is the last column and scrolls with the table. NOT pinned: the rows
   are sorted by it, so it need not follow the reader across a season, and
   pinning it cost 62px of a 390px screen. */
.t100-mx .mx-total { white-space: nowrap; }
.t100-mx thead .mx-name, .t100-mx thead .mx-total { z-index: 3; background: var(--surface-sunken); }
.t100-mx tbody tr:hover .mx-name { background: var(--surface-hover); }
.t100-mx tbody tr.sel .mx-name { background: var(--surface-selected); }
/* The season matrix is sized to about twenty rows and scrolls past them. It used
   to render only the first twenty with nothing underneath, so an athlete ranked
   21st was absent rather than truncated. Header pins while the body scrolls. */
.t100-mx-host { max-height: min(62vh, 620px); overflow: auto; }
.t100-mx-host thead th { position: sticky; top: 0; z-index: 4; }
.t100-mx-host .t100-mx-scroll { margin: 0; overflow: visible; }
/* The NAME header is pinned LEFT, so it has to cover the angled venue labels
   that scroll under it. The total is not pinned to either side — it scrolls
   with the table — so it has no such claim, and holding it up here was what
   cut the LAST round's label in half ("Vancouver" as "Vanco"): that label
   leans up and to the right, into the air above "Pts". Left to the header's
   own z-index it sits under the labels' ladder (set per column by the
   renderer, see _drawMatrix) and the whole venue reads. */
.t100-mx-host .t100-mx thead .mx-name { z-index: 99; }
/* Finishers per round, upright under the angled venue. Small and quiet: it
   qualifies the placing in the cell below, it is not a heading of its own. */
.mx-nrow th {
    padding: 0 var(--space-1) var(--space-2);
    font-size: var(--text-micro); font-weight: var(--font-weight-normal);
    color: var(--text-faint); font-variant-numeric: tabular-nums;
}
.mx-nrow .mx-name { font-size: var(--text-micro); color: var(--text-faint); }
/* A race from a period whose points are deprecating (para only — a season table
   has no such thing). Same card, shaded ground, so a set reads as two halves
   without a written heading over each. */
/* ── The race card: one race on an athlete's card ────────────────────────────
   Built by raceCardHTML() and shared by the season standings AND the para
   class ranking, which is why it is NOT `t100-`: it was, and sitting one
   letter away from the T100 CALENDAR row (`t100-cal-*`, below) meant two
   different components answered the same grep. That is how they ended up
   spelling the same date two different ways. */
.racecard-old { background: var(--surface-tint); }
.racecard-old:hover { background: var(--surface-hover); }

.mx-rank {
    display: inline-block; min-width: 1.4em; margin-right: var(--space-2);
    color: var(--text-muted); font-weight: var(--font-weight-bold);
    font-variant-numeric: tabular-nums; vertical-align: middle;
}
.t100-mx .mx-round, .t100-mx .mx-cell { padding-left: var(--space-1); padding-right: var(--space-1); }
/* ── Heat tiers: the shared cell fills for EVERY matrix ──────────────────────
   The season standings, the para class ranking and race history's grids all
   shade a cell by how good the result in it was, so they all wear these.
   js/config/heat-scale.js decides which one a value gets; nothing picks a
   fill by hand and nothing writes a colour inline.

   Each class sets its own `color`. The ink has to flip partway up the ramp
   (dark on the pale tiers, white on the deep ones) and the ramp inverts
   between themes, so ink cannot be a separate token the cell inherits — it
   is paired with the fill or it goes light-on-light in one theme. */
.heat-gold   { background: var(--heat-gold-bg);   color: var(--heat-gold-ink); }
.heat-silver { background: var(--heat-silver-bg); color: var(--heat-silver-ink); }
.heat-bronze { background: var(--heat-bronze-bg); color: var(--heat-bronze-ink); }
.heat-1 { background: var(--heat-1-bg); color: var(--heat-1-ink); }
.heat-2 { background: var(--heat-2-bg); color: var(--heat-2-ink); }
.heat-3 { background: var(--heat-3-bg); color: var(--heat-3-ink); }
.heat-4 { background: var(--heat-4-bg); color: var(--heat-4-ink); }
.heat-5 { background: var(--heat-5-bg); color: var(--heat-5-ink); }
.heat-6 { background: var(--heat-6-bg); color: var(--heat-6-ink); }
.heat-gf-1 { background: var(--heat-gf-1-bg); color: var(--heat-gf-1-ink); }
.heat-gf-2 { background: var(--heat-gf-2-bg); color: var(--heat-gf-2-ink); }
.heat-gf-3 { background: var(--heat-gf-3-bg); color: var(--heat-gf-3-ink); }
.heat-gf-4 { background: var(--heat-gf-4-bg); color: var(--heat-gf-4-ink); }
.heat-gf-5 { background: var(--heat-gf-5-bg); color: var(--heat-gf-5-ink); }
.heat-gf-6 { background: var(--heat-gf-6-bg); color: var(--heat-gf-6-ink); }
.mx-cell {
    font-weight: var(--font-weight-bold); font-variant-numeric: tabular-nums;
    font-size: var(--text-xs);
}
.mx-dns { color: var(--text-faint); }
/* A result that scored but does not count toward the total (outside best-N).
   It keeps its number and loses its fill: the fill means "this is how the
   season was won", and a dropped race is not part of that answer. Overrides
   the tier it would otherwise have worn, so it comes after them. */
.mx-cell.mx-drop { background: transparent; color: var(--text-muted); }
/* A venue name is a word over a two-digit placing, so horizontally it set the
   column width and the season stopped fitting on a phone — every venue ended up
   ellipsed anyway. At 45 degrees the label uses the header's HEIGHT, which costs
   nothing, and the column narrows to the number it holds. The cell anchors the
   rotation: the label is taken out of flow, pinned to the cell's bottom, and
   turned about that corner. */
.mx-round {
    cursor: pointer; position: relative;
    /* Room for the longest venue label on the diagonal: a 12-character name is
       ~60px of text, which leans ~42px up. */
    height: min(calc(68 * var(--u)), 102px); vertical-align: bottom; padding: 0;   /* grows with the label it holds */
}
.mx-vn {
    position: absolute; bottom: var(--space-2); left: 50%;
    transform-origin: left bottom; transform: rotate(-45deg);
    white-space: nowrap; line-height: 1; text-align: left;
    font-size: var(--text-micro);
    /* The label leans out over the cells to its right, whose backgrounds would
       otherwise paint over it — a later sibling paints above an unstacked
       earlier one. Below the pinned name header (z-index 3), which must still
       cover the labels that scroll under it. */
    z-index: 1;
}
.mx-round.mx-gf { color: var(--warning); }
.mx-round.sel { background: var(--accent-subtle); }
.mx-final { margin-left: var(--space-1); font-size: var(--text-micro); color: var(--major-accent); font-weight: var(--font-weight-bold); }
/* A continental championship: scores in the ranking, not a round of the tour. */
.mx-final.mx-cont { color: var(--text-muted); }
/* "Antofagasta AMERICAS" is ~20 characters on the diagonal: a taller header,
   only on a table that carries one. */
.t100-mx.has-cont .mx-round { height: min(calc(96 * var(--u)), 144px); }
/* Inline expanded detail row (race-by-race points) under the selected athlete. */
.t100-mx .mx-detail-row td { padding: 0; background: var(--surface-sunken); border-bottom: 1px solid var(--border); }
/* The detail row spans a table that scrolls sideways, so its cards used to run
   off the right edge with the season. Stuck to the scrollport's left edge and
   sized to it, the whole set stays in view wherever the season is scrolled to. */
.t100-mx .mx-detail-row .t100-drill-races {
    position: sticky; left: 0;
    width: min(100%, calc(100vw - 2 * var(--space-5)));
    padding: var(--space-4) var(--space-5);
}
.mx-total b { color: var(--accent-hover); font-variant-numeric: tabular-nums; font-size: var(--text-xs); }
/* Split total bar: violet counting points + gold Grand Final points.
   Kept, but thin: the number and the bar together used to make a standings row
   half again as tall as the same row in race history's grids, which is most of
   why a phone showed so much less of the season. Nothing is dropped — the bar
   still reads as a share of the leader's total. */

/* Scoring chart legend (Grand Final / Regular), below the plot. */
.t100-pc-legend {
    display: flex; justify-content: center; flex-wrap: wrap;
    gap: var(--space-6); margin-top: var(--space-3);
    font-size: var(--text-sm); color: var(--text-2);
}
.t100-pc-leg-item { display: inline-flex; align-items: center; gap: var(--space-2); font-weight: var(--font-weight-semibold); }
.t100-pc-swatch { width: 14px; height: 3px; border-radius: var(--radius-pill); }
/* Fills come from CSS, not from a colour resolved in JS: an inline background
   is captured at render time and this legend is not rebuilt on a theme change. */
.t100-pc-swatch.t100-pc-gf  { background: var(--major-accent); }
.t100-pc-swatch.t100-pc-reg { background: var(--accent-hover); }

/* ── Athlete drill-down panel ── */
.t100-drill { margin-top: var(--space-5); }
.t100-drill-close {
    position: absolute; top: var(--space-3); right: var(--space-4);
    background: transparent; border: 0; font-size: var(--text-xl);
    color: var(--text-muted); cursor: pointer; line-height: 1;
    padding: var(--space-2);
}
.t100-drill-close:hover { color: var(--text-2); }

.t100-drill-races {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(136px, 1fr));
    gap: var(--space-2); padding: var(--space-4) var(--space-5);
}
.t100-drill-race {
    border-left: 3px solid var(--text-faint);
    padding: var(--space-2) var(--space-3);
    cursor: pointer;
    text-align: left;
    transition: background var(--transition-fast), border-color var(--transition-fast);
    -webkit-tap-highlight-color: transparent;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    font: inherit;
    color: inherit;
    text-decoration: none;   /* an <a> when the race has an address */
}
.t100-drill-race:hover { background: var(--surface-hover); }
.racecard-counting { border-left-color: var(--accent); }
.racecard-counting:hover { background: var(--accent-subtle); }
.racecard-gf       { border-left-color: var(--warning); background: var(--warning-bg); }
.racecard-gf:hover { background: var(--badge-amber-bg); }
.racecard-dropped  { border-left-color: var(--border-strong); opacity: 0.75; }
/* Left-justified, both lines: date then venue, place then points. The gap is
   wider than the app's usual inline spacing on purpose — each line is two
   unrelated facts sitting side by side, and at --space-2 they read as one
   run-on string ("15 OctWollongong", "2/13+648"). */
.racecard-line { display: flex; align-items: baseline; gap: var(--space-6); min-width: 0; }
.racecard-date {
    font-family: var(--font-family-mono);
    font-size: var(--text-xs); color: var(--text-muted);
}
.racecard-pos {
    font-family: var(--font-family-mono);
    font-size: var(--text-sm); font-weight: var(--font-weight-bold);
    color: var(--text-2);
}
/* The points ARE the label: bold indigo when they count toward the season
   total, grey when they were dropped. This replaced a written tag on its own
   line in every card. */
.racecard-pts {
    font-family: var(--font-family-mono);
    font-size: var(--text-sm); font-weight: var(--font-weight-bold);
    color: var(--accent-hover);
}
.racecard-dropped .racecard-pts { color: var(--text-muted); font-weight: var(--font-weight-semibold); }
.racecard-dropped .racecard-pos { color: var(--text-muted); }
/* A score that still counts but is being devalued (para's previous period, worth
   a third). Accent, so it reads as a counting figure; dimmed, so it reads as an
   old one. Grey is not available here — that is what a DROPPED score means, and
   these are the opposite: they are in the total. */
.racecard-old.racecard-counting .racecard-pts { color: var(--accent-ink-dim); }

.t100-drill-footer {
    display: flex; gap: var(--space-3); flex-wrap: wrap;
    align-items: center; padding: var(--space-4) var(--space-7);
    border-top: 1px solid var(--surface-tint); background: var(--surface-sunken);
    font-size: var(--text-xs);
}
.t100-drill-hint { margin-left: auto; color: var(--text-faint); font-style: italic; }

/* ── Calendar ── */
.t100-calendar { display: flex; flex-direction: column; gap: var(--space-2); }
/* Date, venue and status on one line; the winners under it. All left-aligned:
   the three-column grid pushed the status to the far right, so a row read as
   three separate things at three depths instead of one race. */
/* The CALENDAR row -- a race in the season's schedule, not a result on an
   athlete's card. Series-specific, so it keeps the t100- prefix; named for the
   calendar it belongs to (.t100-calendar) rather than for "race", which the
   race card above is equally entitled to. */
.t100-cal-row {
    display: flex; flex-direction: column; gap: var(--space-1);
    padding: var(--space-4) var(--space-5);
    transition: background var(--transition-fast);
}
.t100-cal-top {
    display: flex; align-items: baseline; gap: var(--space-3); flex-wrap: wrap;
}
.t100-cal-row.complete { cursor: pointer; }
.t100-cal-row.complete:hover { background: var(--surface-hover); border-color: var(--border-strong); }
.t100-cal-row.cancelled { opacity: 0.55; text-decoration: line-through; }
.t100-cal-row.grand-final { border-left: 4px solid var(--warning); background: var(--warning-bg); }
.t100-cal-date {
    font-family: var(--font-family-mono); font-size: var(--text-sm);
    color: var(--text-muted); white-space: nowrap;
}
.t100-cal-venue { font-weight: var(--font-weight-semibold); color: var(--text-2); font-size: var(--text-md); }
.t100-gf-tag {
    margin-left: var(--space-3); font-size: var(--text-micro); font-weight: var(--font-weight-bold);
    color: var(--warning); background: var(--badge-amber-bg);
    padding: 1px var(--space-3); border-radius: var(--radius-sm);
    letter-spacing: 0.5px;
}
.t100-cal-winners {
    font-size: var(--text-sm); color: var(--text-muted);
    display: flex; gap: var(--space-5); flex-wrap: wrap;
}
.t100-cal-status {
    font-size: var(--text-xs); font-weight: var(--font-weight-semibold);
    color: var(--text-muted); white-space: nowrap;
}
.t100-cal-row.complete  .t100-cal-status { color: var(--success); }
.t100-cal-row.pending   .t100-cal-status { color: var(--warning); }
.t100-cal-row.upcoming  .t100-cal-status { color: var(--text-faint); }
.t100-cal-row.cancelled .t100-cal-status { color: var(--danger); }

/* ── Points-curve viz ── */
.t100-pc-host { position: relative; }
.t100-pc-tip {
    position: absolute; display: none;
    background: var(--scrim-tooltip); color: var(--on-surface-inverse);
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius-sm); font-size: var(--text-xs);
    pointer-events: none; z-index: var(--z-tooltip);
    white-space: nowrap;
}

/* ── Tie-breakers ── */
.t100-tiebreak {
    margin: 0; padding-left: var(--space-10);
    color: var(--text-2); font-size: var(--text-base); line-height: 1.7;
}
.t100-tiebreak li { margin-bottom: var(--space-1); }

/* ── Race-band drill-down (tap a completed band on the chart) ── */
.t100-rdrill-head {
    display: flex; align-items: baseline; gap: var(--space-4);
    flex-wrap: wrap; padding: var(--space-5) var(--space-7);
    background: linear-gradient(135deg, var(--accent-subtle), var(--surface-tint));
    border-bottom: 1px solid var(--border);
    position: relative;
}
.t100-rdrill-venue { font-size: var(--text-lg); font-weight: var(--font-weight-bold); color: var(--text-2); }
.t100-rdrill-date {
    font-family: var(--font-family-mono); font-size: var(--text-sm); color: var(--text-muted);
}
.t100-rdrill-results { padding: var(--space-3) var(--space-6); }
.t100-rdrill-row {
    display: grid; grid-template-columns: auto auto 1fr auto;
    gap: var(--space-4); align-items: center;
    padding: var(--space-3) var(--space-2);
    border-bottom: 1px solid var(--surface-tint);
}
.t100-rdrill-row:last-child { border-bottom: 0; }
.t100-rdrill-pos {
    font-family: var(--font-family-mono); font-weight: var(--font-weight-bold);
    font-size: var(--text-md); color: var(--text-muted);
    width: 1.6em; text-align: right;
}
.t100-rdrill-pos.pod { color: var(--badge-violet-ink); }
.t100-rdrill-name { font-weight: var(--font-weight-semibold); color: var(--text-2); font-size: var(--text-md); min-width: 0; }
.t100-rdrill-pts {
    font-family: var(--font-family-mono); font-weight: var(--font-weight-bold);
    font-size: var(--text-md); color: var(--accent-hover);
}
.t100-rdrill-pts.gf { color: var(--warning); }
/* .t100-rdrill-load = .btn + a smaller load pill (hover from .btn:hover). */
.t100-rdrill-load {
    padding: var(--space-3) var(--space-6); font-size: var(--text-sm);
    -webkit-tap-highlight-color: transparent;
    transition: background var(--transition-fast);
}

/* ── The standings ROW: shared by every ranked-competitor page ────────────
   A ranked competitor with a headline number and an expandable breakdown — the
   NCAA power index and the para class ranking today, and whatever ranks next.

   These were spelled `.t100-sr-*` / `.t100-stand-row` / `.t100-det-*` until
   2026-09-20, which was a lie in both directions: the T100/WTCS page does NOT
   use them (it renders the `.mx-*` race grid into #t100Standings instead), and
   the pages that DO use them are NCAA and para. A shared family wearing one
   feature's prefix is how the drift starts — nobody building the next standings
   page thinks to reach for a class named after a tour it has nothing to do
   with, so they write a fourth copy. The name says "standings" now because that
   is what it is.

   Compose these; put only what is genuinely different about your page in your
   own prefix (see `.np-*` below, which sets grid columns and adds a school
   line, and nothing else). */
#t100Standings { display: flex; flex-direction: column; gap: var(--space-2); }
.stand-row {
    display: grid;
    grid-template-columns: auto auto 1fr auto;
    gap: var(--space-4);
    align-items: center;
    padding: var(--space-3) var(--space-5);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background var(--transition-fast), border-color var(--transition-fast);
}
.stand-row:hover { background: var(--surface-hover); border-color: var(--border-strong); }
.stand-row.podium { border-left: 3px solid var(--accent); }
.stand-rank {
    font-family: var(--font-family-mono); font-size: var(--text-md);
    font-weight: var(--font-weight-bold); color: var(--text-muted);
    width: 1.8em; text-align: center;
}
.stand-row.podium .stand-rank { color: var(--accent-hover); }
.stand-body { min-width: 0; }
.stand-head {
    display: flex; justify-content: space-between; align-items: baseline;
    gap: var(--space-4); margin-bottom: var(--space-2);
}
.stand-total {
    font-family: var(--font-family-mono); font-weight: var(--font-weight-bold);
    font-size: var(--text-lg); color: var(--badge-violet-ink);
}
.stand-chev { color: var(--text-faint); font-size: var(--text-sm); }
.stand-detail {
    display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4);
    margin-top: var(--space-3); padding-top: var(--space-3);
    border-top: 1px dashed var(--border);
}
/* Author `display:flex` outranks the UA [hidden]{display:none}, so collapse
   needs its own (higher-specificity) author rule to actually hide the row. */
.stand-detail[hidden] { display: none; }
.stand-det-row {
    display: flex; align-items: baseline; gap: var(--space-3);
    font-size: var(--text-xs); color: var(--text-muted);
}
.stand-det-row.dropped { opacity: 0.6; }
.stand-det-venue { color: var(--text-2); font-weight: var(--font-weight-semibold); }
.stand-det-pts { font-family: var(--font-family-mono); font-weight: var(--font-weight-bold); color: var(--text-muted); }
.stand-det-pts.counting { color: var(--badge-violet-ink); }
.stand-det-pts.gf { color: var(--warning); }

/* ── Scoring reference table (behind a <details>) ── */
.t100-scoring-details { margin-top: var(--space-4); }
.t100-scoring-summary {
    cursor: pointer; font-size: var(--text-sm); font-weight: var(--font-weight-semibold);
    color: var(--badge-violet-ink); padding: var(--space-2) 0;
}
.t100-rules-blurb { font-size: var(--text-sm); color: var(--text-2); line-height: 1.5; margin: var(--space-3) 0; }

/* ── NCAA power rankings ──
   Rides the .stand-row chrome above; these are only the extras that row
   doesn't already carry (a school/starts subline, a division tag, and the
   races-and-weights reference table). */
/* .stand-row's four columns assume a standings row's avatar. These rows
   are rank / body / chevron, so without an explicit template the body would be
   sized to its content and the chevron would absorb the 1fr — leaving every
   row a different width and the bars unreadable against each other. */
.np-row { grid-template-columns: auto 1fr auto; }
.np-stand-sub {
    display: flex; align-items: baseline; gap: var(--space-3);
    margin-top: var(--space-2); font-size: var(--text-xs); color: var(--text-muted);
    min-width: 0;
}
/* No ellipsis: the cell carries the short label now (the crest beside it is the
   identity, the full name is on hover), so there is nothing left to clip. */
.np-stand-school { min-width: 0; color: var(--text-muted); }
.np-starts { margin-left: auto; flex: none; font-variant-numeric: tabular-nums; }
.np-stand-sub:empty { display: none; }
.np-div {
    font-size: var(--text-micro); font-weight: var(--font-weight-bold);
    color: var(--text-faint); letter-spacing: 0.4px;
}
/* A race name inside a drill row opens that race, and sits in a line of running
   text, so it reads as a link, and is a real <a href> (UI-CONVENTIONS). */
.np-race-link {
    background: none; border: 0; padding: 0; font: inherit; cursor: pointer;
    color: var(--text-2); text-decoration: underline; text-underline-offset: 2px;
}
.np-race-link:hover { color: var(--accent-hover); }
/* One block for this table, not two. There were two `td, th` rules seventeen
   lines apart setting different horizontal padding (--space-4 then --space-3);
   the second silently won and the first had been dead since whenever they
   diverged. Keep the table's rules together. */
.np-racetable { width: 100%; }
.np-racetable td, .np-racetable th {
    text-align: left; padding: var(--space-2) var(--space-3); white-space: nowrap;
}
.np-racetable td:first-child { white-space: normal; }
.np-racetable tr.np-dropped td { color: var(--text-faint); }
.np-racetable .np-rt-w, .np-racetable .np-rt-n { font-variant-numeric: tabular-nums; text-align: right; }
.np-racetable .np-rt-tier, .np-racetable .np-rt-n { color: var(--text-muted); }
.np-controls, .pr-controls { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-bottom: var(--space-5); }
.np-share { margin-bottom: var(--space-4); }
.np-show-all { margin-top: var(--space-4); }
.np-find, .t100-find { margin-bottom: var(--space-4); }
.np-math { margin-left: auto; font-family: var(--font-family-mono); font-size: var(--text-xs); color: var(--text-faint); }
.np-math-summary { margin-bottom: var(--space-3); }
.np-race-date { flex: none; font-family: var(--font-family-mono); }
.np-race-row { grid-template-columns: 1fr auto; }
.t100-points-tbl {
    border-collapse: collapse; font-size: var(--text-sm); margin-top: var(--space-3);
}
.t100-points-tbl th, .t100-points-tbl td {
    padding: var(--space-1) var(--space-5); text-align: right; font-variant-numeric: tabular-nums;
}
.t100-points-tbl th { color: var(--text-muted); font-weight: var(--font-weight-bold); border-bottom: 1px solid var(--border); }
.t100-points-tbl td { color: var(--text-2); }
.t100-points-tbl .t100-gf-cell { color: var(--warning); font-weight: var(--font-weight-semibold); }
.t100-points-tbl .t100-overflow td { border-top: 1px solid var(--border); color: var(--text-muted); }

/* ── Para class ranking ──
   The season-standings matrix and its drill cards, pointed at a sport class (see
   js/ui/para-ranking.js). Everything structural — the pinned name column, the
   angled venue headers, the shaded cells, the row that expands into race cards —
   is the standings' own, including the cell colour scale. These are only the
   extras a class page needs that a season table has no use for. */
/* Taller than the 320px the secondary grids get: here the grid IS the page, and
   a class ladder cut to five rows would hide the reader's own row as often as
   not. Still capped, so a 69-deep class does not push everything else away. */
.pr-grid { max-height: min(62vh, 560px); }
/* Longer than a season round's venue: a class races Wollongong and Antofagasta,
   and a 45-degree label takes the header's HEIGHT, so the room has to be here. */
.pr-grid .mx-round { height: min(calc(84 * var(--u)), 126px); }
/* Above every column's own stacking order (set inline, left to right) so the
   pinned name header covers the angled labels that scroll under it. Written at
   this weight deliberately: `.rh-grid-host .t100-mx thead .mx-name` already sets
   z-index 5, which outranks a shorter selector and let the labels through.
   The NAME header only: the total header is not pinned sideways, and at 99 it
   painted over the last round's angled label ("Antofagasta" as "Antofa"). */
.rh-grid-host.pr-grid .t100-mx thead .mx-name { z-index: 99; }
/* Finishers, upright, under the angled venue row. Small and quiet: it qualifies
   the place in the cell below, it is not a heading of its own. */
.pr-nrow th {
    padding: 0 var(--space-1) var(--space-2);
    font-size: var(--text-micro); font-weight: var(--font-weight-normal);
    color: var(--text-faint); font-variant-numeric: tabular-nums;
}
.pr-nrow .mx-name { font-size: var(--text-micro); color: var(--text-faint); }
/* The period boundary, and the deprecating half behind it. Points from the
   previous period are worth a third and are rolling off, so those columns are
   faded — the single most useful thing to see coming on a ranking about to turn
   over. Opacity rather than a tint, because the cells carry fixed heatmap fills
   that a tint would sit under rather than over. */
.pr-grid .pr-periodline { border-left: 2px solid var(--border-strong); }
.pr-grid .pr-prev { opacity: 0.55; }
.pr-grid thead .pr-prev { opacity: 0.75; }
/* The race the page was opened from, marked in its own column head so a reader
   arriving from a race can find it among forty. */
.mx-round.pr-anchor { box-shadow: inset 0 2px 0 var(--accent); }
/* The total, with its make-up on hover: this period's points and what last
   period still adds, in the same two accents the cards use, so the split reads
   back to them without a legend. Hover only — the total is what the ranking is
   ordered by, and the breakdown is secondary to it. */
.pr-total {
    font-family: var(--font-family-mono); white-space: nowrap;
    position: relative; line-height: 1.25;
}
.pr-total b { font-size: var(--text-xs); color: var(--accent-hover); }
/* Borrowed from `.tooltip`: same ground, ink, radius and elevation, so a hover
   readout looks the same wherever it comes from. It is not the shared tooltip
   because that one escapes its text by design and this needs two colours. */
.pr-t-split {
    position: absolute; right: 0; bottom: calc(100% + var(--space-2));
    display: flex; gap: var(--space-4);
    padding: var(--space-3) var(--space-4);
    background: var(--scrim-tooltip); border-radius: var(--radius-md);
    font-size: var(--text-micro); white-space: nowrap;
    opacity: 0; pointer-events: none; z-index: var(--z-tooltip);
    transition: opacity var(--transition-fast);
}
.pr-t-now { color: var(--accent-ink); font-weight: var(--font-weight-bold); }
.pr-t-old { color: var(--accent-ink-dim); font-weight: var(--font-weight-bold); }
/* No hover on touch, and none needed: tapping the row opens the cards, which
   show every contribution. Same rule the shared tooltip follows. */
@media (hover: hover) and (pointer: fine) {
    .pr-total:hover .pr-t-split, .mx-row:focus-within .pr-t-split { opacity: 1; }
}
/* The one control on the page: whether the deprecating period is drawn. */
#prPrev { margin-bottom: var(--space-4); }
.pr-chart-switch { margin-bottom: var(--space-4); }

/* ── Athlete headshots + flags ──
   Circular photo with a monogram underneath; the <img> removes itself on a
   404 (see _avatar) so the monogram shows through. */
.t100-avatar {
    position: relative; display: inline-flex; flex: none;
    width: 36px; height: 36px; border-radius: 50%; overflow: hidden;
    background: linear-gradient(135deg, var(--accent-subtle), var(--surface-tint));
    align-items: center; justify-content: center;
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06);
}
.t100-avatar img {
    position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover; object-position: top center;
}
.t100-avatar-mono {
    font-size: var(--text-md); font-weight: var(--font-weight-bold);
    color: var(--accent-hover); letter-spacing: 0.5px; user-select: none;
}
.t100-rdrill-avatar { width: 28px; height: 28px; }
.t100-rdrill-avatar .t100-avatar-mono { font-size: var(--text-sm); }

/* Ability development tracks (career strength-of-field rating per discipline) */
.abt-bar { display:flex; gap: var(--space-5); align-items:center; flex-wrap:wrap; margin-bottom: var(--space-5); }
.abt-tog { font-size:var(--text-base); color:var(--text-2); display:inline-flex; align-items:center; gap: var(--space-2); cursor:pointer; }
.abt-reset { font:inherit; font-size:var(--text-base); padding: var(--space-2) var(--space-5); border:1px solid var(--border); border-radius: 8px; background:var(--surface); color:var(--text-2); cursor:pointer; }
.abt-reset:hover { border-color:var(--border-strong); }
.abt-hint { font-size:var(--text-sm); color:var(--text-faint); }
/* Hover readout on the ability tracks. Deliberately INVERSE — a dark ground
   with light ink in both themes, unlike the surface-coloured .tooltip — so it
   reads over the lane fills without inheriting their tint. It was built from
   eleven inline .style() calls in ability-tracks.js, which put its background
   and shadow outside every search of css/. */
.abt-tip {
    position: absolute; z-index: var(--z-tooltip); pointer-events: none;
    max-width: 190px; padding: var(--space-2) var(--space-4);
    background: var(--scrim-tooltip); color: var(--on-surface-inverse);
    border-radius: var(--radius-md); box-shadow: var(--shadow-2);
    font-size: var(--text-sm); line-height: 1.35;
}
/* No horizontal padding so the chart SVG spans the full card width and its year
   ticks line up with the career arc; the header keeps its own inset instead. */
.abt-lane { background:var(--surface); border:1px solid var(--border); border-radius: 12px; padding: var(--space-4) 0 var(--space-2); margin-bottom: var(--space-5); }
.abt-lane-head { display:flex; align-items:baseline; justify-content:space-between; gap: var(--space-4); padding: 0 var(--space-5); }
.abt-lane-name { font-weight:800; font-size:var(--text-md); letter-spacing:.02em; }
/* The race count is read against the axis under it, so it is the axis's size
   (.axis text in chart-theme.css), not a heading's. */
.abt-lane-read { font-size:var(--chart-text-micro); color:var(--text-muted); text-align:right; }
@media (min-width: 769px) { .abt-lane-read { font-size:var(--chart-text-sm); } }
.abt-lane-read b { color:var(--text); font-variant-numeric:tabular-nums; }
.abt-lanes svg { display:block; width:100%; height:auto; }
/* Dual short/long athletes: two side-by-side columns on wide screens. */
.abt-lanes.abt-split { display:flex; gap: var(--space-8); align-items:flex-start; }
.abt-lanes.abt-split .abt-col { flex:1 1 0; min-width:0; }
.abt-col-head { font-weight:800; font-size:var(--text-base); color:var(--text-muted); letter-spacing:.04em; text-transform:uppercase; margin: 0 var(--space-1) var(--space-3); }
/* Phones: stack the Short / Long sections instead of side-by-side (too narrow). */
@media (max-width: 640px) { .abt-lanes.abt-split { flex-direction:column; gap: var(--space-2); } .abt-lanes.abt-split .abt-col { width:100%; } }
.abt-brush .selection { fill:var(--accent); fill-opacity:.08; stroke:var(--seg-swim); stroke-opacity:.5; }
.abt-legend { color:var(--text-muted); font-size:var(--text-sm); margin-top: var(--space-4); line-height:1.9; display:flex; flex-wrap:wrap; }
.abt-legend .abt-leg-item { display:inline-flex; align-items:center; white-space:nowrap; margin-left: var(--space-6); }
.abt-legend .abt-leg-item--end { margin-left:auto; margin-right: var(--space-6); }
.abt-legend .abt-shade { display:inline-block; width:14px; height:11px; background:rgba(37,99,235,.18); margin-right: var(--space-2); }
.abt-legend .abt-xk { display:inline-block; margin-right: var(--space-2); color:var(--text-muted); font-weight:800; }
.abt-legend .abt-dnfk { display:inline-block; margin-right: var(--space-2); color:var(--text-muted); font-weight:600; }

/* ============================================================
   SaaS chrome layer (folded in from the former saas-theme.css).
   Loads after the base rules above, so it keeps overriding them —
   same cascade as when it was a separate file. Dedupe incrementally.
   ============================================================ */
.header-sep { display: none; }
.header-btn {
    background: var(--surface);
    color: var(--text-2);
    border: 1px solid var(--border);
    font-weight: 500;
    box-shadow: var(--shadow-1);
}
/* Action labels: full text by default, compact on phones (no hamburger —
   the buttons stay one tap away, just shorter). */
.lbl-short { display: none; }
@media (max-width: 640px) {
    .header-btn .lbl-full { display: none; }
    .header-btn .lbl-short { display: inline; }
}
.header-btn:hover:not(:disabled) {
    background: var(--surface-hover);
    border-color: var(--border-strong);
    color: var(--text);
}
/* Modal-footer "Load race" stays the one primary action */
.src-modal-foot .header-btn {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--on-accent);
}
.src-modal-foot .header-btn:hover:not(:disabled) { background: var(--accent-hover); }
.header-file-btn.has-file { background: var(--badge-green-bg); color: var(--badge-green-ink); border-color: var(--badge-green-ink); }

/* Tabs are defined once, up in TAB NAVIGATION — the underline style used to be
   patched in here as a second layer, which meant a tab's real appearance was
   two rules 2700 lines apart. It's one rule now; change it there. */

/* ── Surfaces: flatten, border-define ──────────────────── */
/* Kill the candy gradients — neutral panels with an accent rule */
.ap-header, .rh-header,
.t100-rdrill-head {
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
}
/* ── Chips & filters: outline-quiet, filled-active ─────── */
.filter-options-btn, .ctrl-pill {
    border-color: var(--border);
    color: var(--text-muted);
    font-weight: 500;
}
.filter-options-btn.active, .ctrl-pill.on {
    background: var(--text);            /* dark-fill active = neutral, lets data colors pop */
    border-color: var(--text);
    color: var(--surface);               /* inverse of --text so it flips in dark */
    box-shadow: none;
}
.xo-confirm:hover { background: var(--accent-hover); }
.src-modal-head, .src-modal-foot { background: var(--surface); }

/* ── Podium: number badges, not gold bars ──────────────── */
.podium-1, .team-row.podium { background: var(--surface); }
.podium-card { border-left-width: 2px; }
.podium-1 { border-color: var(--gold); }   /* keep medal hue only as a thin rule */
.stand-total, .t100-rdrill-pos.pod,
.stand-det-pts.counting, .t100-scoring-summary { color: var(--accent-hover); }

/* ── Country flags: real images, not emoji ─────────────── */
.flag-img {
    width: 16px; height: 12px;
    border-radius: 2px;
    object-fit: cover;
    vertical-align: -2px;
    flex: none;
}
/* Every flag image — rectangular (.flag-img) or the circular ones from the WT /
   T100 APIs — carries a hairline rim so a flag whose edge matches the surface
   (JP/CA white on white, DE/EE black on dark) still reads as a flag.
   NOTE: this must be an OUTER shadow. An inset shadow paints under an <img>'s
   replaced content, so the old inset rim here was invisible on exactly the
   flags that needed it. */
.flag-img, .country-chip .flag-img, .ap-flag, .lf-flag {
    box-shadow: 0 0 0 1px var(--flag-ring);
}
/* Flags fill their box so the rim hugs the flag (team logos in the same chip
   stay `contain` — see the .country-chip img rule above). */
.country-chip .flag-img { object-fit: cover; }
/* Fallback chip for codes with no national flag (clubs, mixed relay…) */
.flag-code {
    display: inline-block;
    padding: 1px var(--space-2);
    border-radius: var(--radius-sm);
    background: var(--surface-tint);
    color: var(--text-muted);
    font-size: 0.72em;
    font-weight: 700;
    letter-spacing: 0.02em;
    vertical-align: 1px;
    font-variant-numeric: tabular-nums;
}
/* Initials crest for an NCAA team with no logo (replaces 🎓) */
.team-crest {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px; height: 18px;
    border-radius: var(--radius-sm);
    background: var(--text);
    color: var(--surface);
    font-size: var(--text-micro);
    font-weight: 700;
    letter-spacing: 0.02em;
    vertical-align: -4px;
    flex: none;
}

/* ── Rank badges: numeric, not medals ──────────────────── */
.rank-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.5em;
    height: 1.5em;
    padding: 0 0.42em;
    border-radius: var(--radius-sm);
    background: var(--surface-tint);
    color: var(--text-2);
    border: 1px solid var(--border);
    font-weight: 700;
    font-size: 0.82em;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}
/* Medal hues kept only as thin chips so the podium still reads at a glance */
.rank-badge-1 { background: var(--badge-amber-bg); color: var(--badge-amber-ink); border-color: var(--badge-amber-ink); }
.rank-badge-2 { background: var(--badge-neutral-bg); color: var(--badge-neutral-ink); border-color: var(--badge-neutral-ink); }
.rank-badge-3 { background: var(--badge-red-bg); color: var(--badge-red-ink); border-color: var(--badge-red-ink); }

/* Right-size the container that used to hold an emoji (badges bring own size).
   .podium-medal moved up to the podium block; the .h2h-empty-icon rule that sat
   here was byte-identical to the one in the h2h section and is gone. */
.xo-icon { display: inline-flex; align-items: center; }
/* ── Landing / welcome (no race loaded) ────────────────── */
.landing { padding: clamp(var(--space-8), 8vh, var(--space-16)) var(--space-4); }
.landing-hero { max-width: 660px; margin: 0 auto; text-align: center; }
.landing-title {
    display: block;                    /* override global `h1 { display:none }` */
    font-size: var(--text-display);
    font-weight: 800;
    letter-spacing: -0.025em;
    color: var(--text);
    margin: 0 0 var(--space-3);
}
.landing-tagline {
    font-size: var(--text-lg);
    line-height: 1.5;
    color: var(--text-muted);
    margin: 0 auto var(--space-8);
    max-width: 34em;
}
.landing-cta { margin-bottom: var(--space-10); }
.landing-primary {
    display: inline-block;
    padding: var(--space-4) var(--space-8);
    background: var(--accent);
    color: var(--on-accent);
    border: none;
    border-radius: var(--radius-md);
    font-size: var(--text-lg);
    font-weight: 700;
    cursor: pointer;
    transition: background var(--transition-fast), transform .05s;
}
.landing-primary:hover { background: var(--accent-hover); }
.landing-primary:active { transform: translateY(1px); }


.landing-foot { border-top: 1px solid var(--border); margin-top: var(--space-16); padding-top: var(--space-8); text-align: center; }
/* The one door to every upcoming forecast: the landing's own round dropdown
   (.lf-race-slot + .lf-race-round, so it carries their 44px phone tap area),
   above Privacy and on the hero button's centre line. Outside a tile's flex row
   the slot has no width of its own, so it is given one. */
.landing-forecasts { margin-bottom: var(--space-12); }   /* clears Privacy's own 44px tap area */
.landing-forecasts[hidden] { display: none; }
.landing-forecasts > .lf-race-slot { flex: none; width: min(11rem, 100%); margin-inline: auto; }
.landing-forecasts > .lf-race-slot:hover { background: var(--surface); color: var(--accent); }
.privacy-note { display: inline-block; text-align: left; max-width: 34em; }
.privacy-note > summary { cursor: pointer; list-style: none; color: var(--text-faint); font-size: var(--text-eyebrow); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 700; }
.privacy-note > summary::-webkit-details-marker { display: none; }
.privacy-note > summary:hover { color: var(--text-muted); }
.privacy-note p { margin-top: var(--space-4); color: var(--text-muted); font-size: var(--text-sm); line-height: 1.5; }

.landing-featured { border-top: 1px solid var(--border); padding-top: var(--space-6); text-align: left; }
.lf-label {
    font-size: var(--text-eyebrow);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-faint);
    font-weight: 700;
    margin-bottom: var(--space-3);
}
/* Sub-heading inside the races list (e.g. "Juniors & U23") — like .lf-label but
   spaced above so it separates one band of tiles from the one before it. */
.lf-sublabel {
    font-size: var(--text-eyebrow);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-faint);
    font-weight: 700;
    margin: var(--space-5) 0 var(--space-3);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
}
/* The first band has nothing above it to separate from: it sits straight under
   the section title, so it drops the rule and the leading space. */
.lf-sublabel:first-child {
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
}
/* Reserve the loaded height so async content fills it instead of pushing the
   page down on slow loads (CLS). Sized to the rendered 5-row lists. */
.lf-list { display: flex; flex-direction: column; gap: var(--space-2); min-height: 220px; }
.lf-item {
    display: flex;
    align-items: center;
    min-height: min(2.5rem, 60px);  /* follows text size, capped like the type scale */
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    cursor: pointer;
    text-align: left;
    transition: border-color var(--transition-fast), background var(--transition-fast);
}
.lf-item:hover { border-color: var(--accent); background: var(--accent-subtle); }
/* Tiles that open a page with an address are real <a href>s (UI-CONVENTIONS,
   "Navigation that has a URL"), so they must not pick up the browser's link
   underline or visited colour. */
a.lf-item, a.lf-race-gender, a.ap-race-main { text-decoration: none; }
a.lf-item { color: inherit; }
/* They were <button>s, and a button does not inherit the page font: it takes the
   platform's control font (Arial at 13.33px on Chromium Linux, the system face
   on iOS). Keep that look rather than restyling the landing. The system keyword
   is the same one the browser's own button rule uses; each engine ignores the
   one it does not know. :where() keeps this at zero specificity, so the classes'
   own font-size and font-weight still win and only family + line-height come
   from here The size is a token (27 Sep 2026): the
   control font's own is fixed and would not follow the reader's text size. .ap-race-main already set `font: inherit`, so it needs nothing. */
:where(a.lf-item, a.lf-race-gender) { font: -webkit-small-control; font: -moz-button; font-size: var(--text-md); }
.lf-name { font-weight: 600; color: var(--text-2); }
/* Date + host city. Shown at every width: on a phone it's the only thing that
   says where a race was, since the tile is one line of chrome either way. */
.lf-meta { font-size: var(--text-sm); color: var(--text-faint); }
.lf-item::after { content: '→'; margin-left: auto; align-self: center; color: var(--accent); font-weight: 700; }
/* Latest races: the race name on its own line (full width, no truncation), with
   the Women + Men buttons stacked underneath it. */
.lf-race {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
}
/* A world title: its tiles and its band heading in the accent. Never red: red
   is LIVE. (Gold, the medal colour, was tried 24 Sep 2026 and did not look right.) */
.lf-race--champs { border-left-color: var(--accent); }
.lf-sublabel--champs { color: var(--accent); }
.lf-race-info { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
.lf-race-genders { display: flex; flex-wrap: wrap; gap: var(--space-2); }
/* The pill is 32 tall at every width; a phone's 44px tap area is invisible (the
   sweep block at the foot of the file). 44 visible read as clunky (27 Sep 2026).
   A round dropdown's pill is its .lf-race-slot, with the select laid over it. */
.lf-race-gender, .lf-race-slot {
    flex: 1 1 calc(50% - var(--space-2)); min-height: 32px;
    display: flex; align-items: center; justify-content: center;
    font-size: var(--text-sm); font-weight: 600; color: var(--accent);
    padding: 0 var(--space-6); border: 1px solid var(--accent); border-radius: var(--radius-md);
    background: var(--surface); cursor: pointer; white-space: nowrap;
    transition: background var(--transition-fast), color var(--transition-fast);
}
.lf-race-gender:hover, .lf-race-slot:hover { background: var(--accent); color: var(--on-accent); }
.lf-race-gender.lf-loading, .lf-race-slot:has(.lf-loading) { opacity: .5; pointer-events: none; }
/* Mixed relay: a team race, not a gender split → full-width row of its own. */
.lf-race-relay { flex-basis: 100%; }
/* Ghost: the gender is scheduled but not yet scored. Muted + dashed so it reads
   as a placeholder holding its half-width slot, not a clickable button. */
.lf-race-gender.lf-ghost {
    color: var(--text-faint); border: 1px dashed var(--border);
    background: transparent; cursor: default;
}
.lf-race-gender.lf-ghost:hover { background: transparent; color: var(--text-faint); }
/* Live: the gender's race is being pushed to the feed right now. Red border +
   text and a pulsing dot; opens live mode. Reverts to a plain button once the
   result is final (the live overlay stops marking it). */
.lf-race-gender.lf-race-live {
    color: var(--danger); border-color: var(--danger);
    display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
}
.lf-race-gender.lf-race-live:hover { background: var(--danger); color: var(--on-accent); }
.lf-race-gender.lf-race-live:hover .lf-live-dot { background: var(--on-accent); box-shadow: none; animation: none; }
.lf-live-dot {
    flex: none; width: 8px; height: 8px; border-radius: 50%;
    background: var(--danger); animation: lf-live-pulse 1.8s ease-out infinite;
}
@keyframes lf-live-pulse {
    0%   { box-shadow: 0 0 0 0 var(--danger-bg); }
    70%  { box-shadow: 0 0 0 7px transparent; }
    100% { box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: reduce) { .lf-live-dot { animation: none; } }
/* Pre-race: counting down to the gun. The dot holds still until the race moves,
   and the digits are tabular so the button does not jitter as they tick. */
.lf-race-gender.lf-race-pre { font-variant-numeric: tabular-nums; }
.lf-race-pre .lf-live-dot { animation: none; }
/* Dropdown variant (multi-round / para-class genders): a caret hints it opens,
   and text-align-last centres the closed-state gender label like the buttons. */
.lf-race-slot { position: relative; padding: 0; }
/* The select paints nothing but its label and caret, and is the pill's full
   width and its slot's height (44 on a phone, over the 32 pill: its tap area). */
.lf-race-round {
    position: absolute; left: 0; right: 0; top: 50%; height: 100%; transform: translateY(-50%);
    -webkit-appearance: none; appearance: none; text-align-last: center;
    font-size: inherit; font-weight: inherit; color: inherit; border: 0; border-radius: var(--radius-md); cursor: pointer;
    background-color: transparent;
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: calc(100% - 14px) 50%, calc(100% - 9px) 50%;
    background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
    padding: 0 var(--space-12) 0 var(--space-6);
}
.lf-race-round:focus-visible { outline: none; }
.lf-race-slot:has(.lf-race-round:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.lf-race-round option { color: var(--text); background: var(--surface); }

/* ── Landing dynamic sections: rankings grid + tiles ───── */
.lf-empty { padding: var(--space-2) var(--space-1); color: var(--text-faint); font-size: var(--text-sm); }
.lf-loading { opacity: 0.55; pointer-events: none; }
/* Shared-rank rows: rank · man · woman — rank on the left, both names left-aligned. */
.lf-rankings { display: flex; flex-direction: column; gap: var(--space-1); min-height: 240px; }
.lf-rank-headrow, .lf-rank-row {
    display: grid; grid-template-columns: 1.5em 1fr 1fr; align-items: center; gap: var(--space-2);
}
.lf-rank-headrow { margin-bottom: var(--space-1); padding: 0 var(--space-1); }
.lf-rank-headrow span {
    font-size: var(--text-eyebrow); text-transform: uppercase; letter-spacing: 0.06em;
    font-weight: 700; color: var(--text-faint);
}
.lf-rank { align-items: center; gap: var(--space-2); min-width: 0; justify-content: flex-start; text-align: left; }
.lf-rank::after { content: none; }
.lf-rk {
    font-variant-numeric: tabular-nums; font-weight: 700;
    color: var(--text-muted); text-align: left; min-width: 1.2em;
}
.lf-flag {
    width: 18px; height: 18px; border-radius: 50%; object-fit: cover; flex: none;
}
/* Wrap, never truncate: "Cassandre Beaugrand" read "Cassandre Beau..." at 390px,
   the world champion's name clipped on the landing. The row stretches both of
   its buttons to the taller one, so a wrapped name never leaves its pair short. */
.lf-rank .lf-name { min-width: 0; overflow-wrap: anywhere; line-height: var(--line-tight); }
.lf-rank-row { align-items: stretch; }
.lf-rank-row .lf-rk { align-self: center; }

/* ============================================================
   PREDICTION VIEW (prediction-view.js) — .pvx-*
   ============================================================ */
.pvx { margin:0 auto; padding: 0 var(--space-6) 40px; font-family:var(--font-family); position:relative; color:var(--text); }
.pvx-title { font-size:var(--text-xl); font-weight:var(--font-weight-black); line-height:var(--line-tight); color:var(--text); }
.pvx-meta { font-size:var(--text-base); color:var(--text-muted); margin: var(--space-2) 0; }
.pvx-asof { display:flex; align-items:center; gap:var(--space-3); font-size:var(--text-sm); color:var(--text-faint); margin: var(--space-1) 0 var(--space-3); }
.pvx-asof b { color:var(--text-muted); font-weight:var(--font-weight-semibold); }
.pvx-row { display:flex; gap:var(--space-5); flex-wrap:wrap; align-items:center; margin-bottom:var(--space-3); }
.pvx select { padding:var(--space-3) var(--space-4); border:1px solid var(--border); border-radius:var(--radius-md); background:var(--surface); font-size:var(--text-base); color:var(--text-2); }
/* The prediction view's tabs ARE `.tab-navigation` — it carries both classes,
   so the bar geometry has one source and only the margin is local. */
.pvx-tabs { margin:var(--space-3) 0 var(--space-2); }
.pvx-cap { font-size:var(--text-sm); color:var(--text-muted); margin:var(--space-2) 0 0; }
.pvx-adj { display:none; }
.pvx-adj.show { display:block; }
.pvx-legend { display:flex; flex-wrap:wrap; gap: var(--space-3) var(--space-7); margin-top:var(--space-3); font-size:var(--text-sm); color:var(--text-muted); }
.pvx-legend .lg { display:inline-flex; align-items:center; gap: var(--space-2); }
.pvx-legend .lg-dot { width:8px; height:8px; border-radius:50%; display:inline-block; flex:none; background:var(--text-muted); }
.pvx-legend .lg-bar { width:16px; height:5px; border-radius: var(--radius-sm); display:inline-block; flex:none; background:var(--text-muted); }
.pvx-legend .lg-whisk { width:16px; height:2px; border-radius:2px; display:inline-block; flex:none; background:var(--chart-ref); }
/* Result marker in the forecast-review legend: the same diamond the chart draws,
   so the legend swatch and the glyph are one shape. Colour is set inline from
   the accuracy tier, which is the only thing that varies between the three. */
.pvx-legend .lg-dia { width:9px; height:9px; display:inline-block; flex:none; transform:rotate(45deg); border-radius:1px; }
/* A second legend row sits under the first; without this they weld together. */
.pvx-legend + .pvx-legend { margin-top:var(--space-2); }
.pvx-stage { min-height:320px; position:relative; }
/* Long charts scroll inside a fixed box so the frozen axis above them stays put. */
.pvx-scroll { max-height:500px; overflow-y:auto; -webkit-overflow-scrolling:touch; border-radius:var(--radius-lg); }
.pvx-scroll--short { max-height:440px; }
.pvx-note { display:flex; gap:var(--space-7); flex-wrap:wrap; font-size:var(--text-sm); color:var(--text-muted); margin-top:var(--space-3); }
.pvx-note i { display:inline-block; width:14px; height:3px; border-radius:2px; vertical-align:middle; margin-right:var(--space-2); }
.pvx-alts { margin-top:var(--space-5); font-size:var(--text-base); }
.pvx-alts-title { font-weight:var(--font-weight-bold); color:var(--text-2); margin-bottom: var(--space-2); }
.pvx-alt { display:flex; justify-content:space-between; max-width:520px; padding:4px var(--space-3); border-bottom:1px solid var(--border-soft); }
/* Next 10: the WHOLE row is the name's tap area, at the row's own compact
   height. A 44px row was tried on 26 Sep and read as a big visual change for a
   list you scan, so the look stays and the area widens instead (nothing else in
   the row is interactive to collide with). Scoped to .pvx-alts: the forecast
   review reuses .pvx-alt as a scoreboard with nothing to tap. */
.pvx-alts .pvx-alt { position:relative; }
.pvx-alts .pvx-alt .athlete-link::after { content:''; position:absolute; inset:0 0 -1px; }
.pvx-alt.foc { background:var(--accent-subtle); font-weight:var(--font-weight-bold); border-radius:var(--radius-sm); }
/* CHILD combinator, deliberately. As a descendant selector this also caught the
   .athlete-link that sits last inside a name cell, so every athlete name in a
   pvx-alt row rendered in JetBrains Mono -- the prediction view's Next 10 list
   as well as the forecast review's table. The rule is for the trailing NUMBER. */
.pvx-alt > span:last-child { font-family:var(--font-family-mono); }
/* Three-column variant of the scoreboard row: a label, then two comparable
   numbers in aligned columns (what happened, and what we forecast against it).
   The flex/space-between default cannot align a number column down a list, and
   two numbers per row is the whole point of this particular scoreboard. */
.pvx-alt-rank { flex:none; min-width:2.1em; text-align:right; font-variant-numeric:tabular-nums; color:var(--text-muted); margin-right:var(--space-2); }
.pvx-alt .pvx-alt-bib { font-family:var(--font-family-mono); font-variant-numeric:tabular-nums; color:var(--text-faint); min-width:2.2em; text-align:right; margin-left:auto; margin-right:var(--space-4); }
/* Names in this view use the shared .athlete-link cue; .pvx-name only marks the
   SVG labels so they keep their chart sizing alongside it. */
.pvx details { font-size:var(--text-base); margin-top:var(--space-4); }
.pvx details > summary { cursor:pointer; color:var(--accent); font-weight:var(--font-weight-bold); padding:var(--space-2) 0; }
.pvx-tip { position:absolute; pointer-events:none; background:var(--surface); color:var(--text-2); border:1px solid var(--border); box-shadow:0 4px 16px rgba(15,23,42,.08), 0 1px 3px rgba(15,23,42,.04); font-size:var(--text-base); padding: var(--space-3) var(--space-5); border-radius:var(--radius-lg); white-space:nowrap; opacity:0; transition:opacity var(--transition-fast); z-index:20; }
.pvx svg text { font-family:var(--font-family); }
.pvx svg text.pvx-name:hover { fill:var(--accent); }
.pvx-field-note { font-size:var(--text-sm); color:var(--text-faint); margin-top:var(--space-4); text-align:center; font-style:italic; }
@media (max-width: 480px) {
  .pvx { padding:0 var(--space-5) var(--space-16); }
  .pvx-title { font-size:var(--text-lg); }
}

/* ============================================================
   DEVELOPMENT-CHART WHAT-IF PANEL (development-chart.js) — .whatif-*
   ============================================================ */
.whatif-wrap { margin: var(--space-2) 0 var(--space-1); }
/* Pacing-impact toggle uses the shared `.ctrl-check` — no bespoke style. */
/* The shared adjustment sliders (js/ui/adjust-panel.js): the prediction form
   guide and the development chart's pacing what-if both render this. */
.adjust { display:flex; flex-wrap:wrap; gap: var(--space-5) var(--space-7); align-items:center; justify-content:center; padding: var(--space-4) var(--space-1); margin: var(--space-1) 0; }
.adjust[hidden] { display:none; }
/* Packs as many sliders per row as the width honestly holds, rather than a fixed
   two. At a 40% basis a five-leg panel always cost three rows, including on a
   desktop wide enough for one. The min-width is what stops it going too far. */
.adjust-item { display:flex; flex-direction:column; gap: var(--space-2); flex:1 1 126px; max-width:220px; min-width:126px; }
.adjust-top { display:flex; align-items:baseline; justify-content:space-between; gap: var(--space-3); line-height:1; }
.adjust-lbl { font-size:var(--text-micro); font-weight:var(--font-weight-bold); letter-spacing:.06em; color:var(--text-faint); }
.adjust-val { font-family:var(--font-family-mono); font-size:var(--text-base); font-weight:var(--font-weight-bold); color:var(--text-muted); white-space:nowrap; }
.adjust-val.changed { color:var(--accent); }
.adjust-val.faster { color:var(--success); }
.adjust-val.slower { color:var(--danger); }
.adjust-range { width:100%; margin: 0; cursor:pointer; accent-color:var(--accent); }
.adjust-reset { flex:none; }
.whatif-pack { flex:1 1 100%; display:flex; align-items:baseline; justify-content:space-between; gap: var(--space-5); font-size:var(--text-sm); color:var(--text-muted); padding-top: var(--space-1); border-top:1px solid var(--border); margin-top: var(--space-1); }
.whatif-pack-l { text-align:left; }
.whatif-pack-r { text-align:right; margin-left:auto; }
.whatif-pack b { font-weight:700; color:var(--text-muted); }

/* ========================================
   SHARE SHEET (.sc-*)
   ----------------------------------------
   The preview modal for share cards. Always dark, whatever the app theme, so a
   card is judged against the dark ground it will be posted onto. Colours come
   from the --sheet-* tokens, which are deliberately not re-defined in dark.

   This lived as a string in share-card.js and was injected into <head>. It is
   here so it can be found, diffed and audited like every other rule.
   ======================================== */
.sc-modal {
    position: fixed; inset: 0; z-index: var(--z-modal);
    background: var(--sheet-scrim);
    display: none; align-items: center; justify-content: center;
    padding: var(--space-8); backdrop-filter: blur(3px);
}
.sc-modal.open { display: flex; }
.sc-sheet {
    background: var(--sheet-bg); border: 1px solid var(--sheet-line);
    border-radius: 18px; width: min(440px, 100%); padding: var(--space-8);
    box-shadow: 0 24px 60px rgba(0, 0, 0, .6);
    position: relative; color: var(--sheet-ink);
}
.sc-close {
    position: absolute; top: 12px; right: 14px;
    /* the share sheet is always-dark, so its dismiss takes the sheet's ink */
    color: var(--sheet-ink-dim); font-size: var(--text-2xl);
}
.sc-title { font-weight: var(--font-weight-black); font-size: var(--text-lg); margin: 0 0 var(--space-6); }
.sc-preview {
    display: flex; justify-content: center; background: var(--sheet-well);
    border-radius: 12px; padding: var(--space-7); margin-bottom: var(--space-6);
}
.sc-preview canvas {
    border-radius: 10px; box-shadow: 0 8px 24px rgba(0, 0, 0, .5);
    max-width: 100%; max-height: min(60vh, 540px);
    width: auto; height: auto; object-fit: contain;
}
/* A card exported with transparency previews on a checkerboard, so "no
   background" is visibly different from "white background". */
.sc-preview canvas.sc-alpha {
    background-color: var(--sheet-alpha-base);
    background-image:
        linear-gradient(45deg, var(--sheet-alpha-check) 25%, transparent 25%, transparent 75%, var(--sheet-alpha-check) 75%),
        linear-gradient(45deg, var(--sheet-alpha-check) 25%, transparent 25%, transparent 75%, var(--sheet-alpha-check) 75%);
    background-size: 22px 22px; background-position: 0 0, 11px 11px;
}
.sc-row { display: flex; gap: var(--space-4); align-items: center; margin-top: var(--space-5); }
.sc-modes button {
    flex: 1; background: var(--sheet-raised); border: 1px solid var(--sheet-line);
    color: var(--sheet-ink-dim); border-radius: 9px; padding: var(--space-4);
    font-size: var(--text-md); font-weight: var(--font-weight-semibold); cursor: pointer;
}
.sc-modes button.on { border-color: var(--sheet-accent); color: var(--sheet-ink); background: var(--sheet-raised-on); }
.sc-styles { display: none; align-items: center; justify-content: center; gap: var(--space-6); margin-top: var(--space-3); }
.sc-styles .sc-ghost { padding: var(--space-2) 11px; font-size: var(--text-lg); line-height: 1; }
.sc-style-dots { display: flex; gap: var(--space-3); }
.sc-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--sheet-line); }
.sc-dot.on { background: var(--sheet-accent); }
.sc-hint { color: var(--sheet-ink-dim); font-size: var(--text-base); margin-top: var(--space-4); min-height: 30px; }
.sc-spacer { flex: 1; }
.sc-ghost {
    background: var(--sheet-raised); border: 1px solid var(--sheet-line);
    color: var(--sheet-ink); border-radius: 9px; padding: var(--space-4) var(--space-6);
    font-size: var(--text-md); font-weight: var(--font-weight-semibold); cursor: pointer;
}
.sc-primary {
    background: var(--sheet-accent); border: 0; color: var(--on-accent); border-radius: 9px;
    padding: var(--space-4) var(--space-8); font-size: var(--text-md); font-weight: var(--font-weight-bold); cursor: pointer;
}
/* The app-wide transient message (js/ui/toast.js). Composes `.card card--sm` for
   the surface and says only what is different: it floats, so it takes --shadow-2
   directly per the note on the card traits above. `.toast--anchored` is the share
   sheet's in-modal placement, which is the only reason this is a modifier and not
   two components. */
.toast {
    position: fixed; left: 50%; bottom: 24px; z-index: var(--z-tooltip);
    transform: translateX(-50%) translateY(8px);
    /* `left: 50%` leaves shrink-to-fit only the right half of the viewport to
       work with, which wraps the text at ~195px. max-content restores the
       natural width and max-width does the capping. */
    width: max-content; max-width: min(340px, calc(100vw - var(--space-8) * 2));
    color: var(--text); box-shadow: var(--shadow-3); text-align: center; line-height: 1.4;
    font-size: var(--text-base); padding: var(--space-4) var(--space-6);
    opacity: 0; transition: opacity var(--transition-fast), transform var(--transition-fast); pointer-events: none;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast-sub { display: block; margin-top: var(--space-2); color: var(--text-muted); }
/* Pinned under the control that fired it; js/ui/toast.js computes left/top from
   the anchor's rect, the same way main.js places the app info tip. The bottom and
   translate defaults have to go or they fight the computed coordinates. */
.toast--pinned { bottom: auto; transform: translateY(8px); }
.toast--pinned.show { transform: translateY(0); }
/* The share sheet's in-modal placement, which is the only reason this is a
   modifier and not two components. */
.toast--anchored { position: absolute; bottom: 14px; z-index: auto; }
/* The share sheet has its own palette and predates the shared primitive; it keeps
   the pill it always had. Placement still comes from --anchored above. */
.sc-modal .toast {
    max-width: none; background: var(--sheet-line); color: var(--sheet-ink);
    border: 0; box-shadow: none;
    padding: var(--space-3) var(--space-6); border-radius: var(--radius-pill);
}
.sc-busy .sc-preview { position: relative; }
.sc-busy .sc-preview::after {
    content: "Generating…"; position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    color: var(--sheet-ink-dim); font-size: var(--text-md);
    background: rgba(6, 10, 18, .6); border-radius: 12px;
}

/* The class name IS the door to that class's ranking: same thin underline the
   app uses everywhere for "this name goes somewhere" (see athlete-link.js).
   The page it opens is the standings matrix pointed at a sport class — see the
   "Para class ranking" block above, which is only the extras it needs. */
.para-class-open { cursor: pointer; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; }
.para-class-open:hover, .para-class-open:focus-visible { color: var(--accent); }

/* Phone Replay: the pinned header tucks up while scrolling down and returns on
   the slightest scroll up (main.js _setupStickyHeader). Transform only, so the
   page never reflows under a thumb. */
.app-header { transition: transform .22s ease; }
.hdr-autohide .app-header.is-tucked { transform: translateY(calc(-100% - 8px)); }
/* ============================================================
   CHART THEME OVERRIDES
   ============================================================ */

/* ── Axes: token-driven so they adapt to dark and aren't silently overriding
   the charts' own .attr() calls. Domain lines shown (the axis rule), tokenized. */
.axis path.domain { stroke: var(--chart-axis-line); }
.axis line { stroke: var(--chart-axis-line); }
/* One size for every large chart's tick labels (rank, development, …). Charts
   must NOT re-set font-size inline on axis text — let this own it so they stay
   standardized across charts and adapt at the mobile breakpoint. */
.axis text {
    font-family: var(--font-family);
    fill: var(--chart-axis);
    font-size: var(--chart-text-micro);
    font-weight: 400;
    font-variant-numeric: tabular-nums;
}
@media (min-width: 769px) {
    .axis text { font-size: var(--chart-text-sm); }
}
.axis-label {
    font-family: var(--font-family);
    fill: var(--chart-label);
    font-size: var(--chart-text-base);
    font-weight: 500;
}
/* Development chart end-of-line name labels. Sizes live here (not inline JS) so
   watched names track the rank chart's name size; fill stays inline (watched =
   the athlete's own colour, field = muted). */
.dev-end-label { font-size: var(--chart-text-xs); font-weight: 400; }
.dev-end-pick { cursor: pointer; }

/* ── Power index: the field, and the five that count ──────────────────────
   Both take their hue from a tintClass() group (.tint-di and friends),
   so a theme flip re-resolves it without JS. Nothing here names a colour. */
/* SVG text is painted by `fill`; `color` does nothing to it. The lane label
   borrowed .tint-ink, which sets color — so it fell back to the UA's black and
   sat black-on-near-black in dark mode. The tint has to reach it as a fill. */
.np-lane-name { font-size: var(--chart-text-base); font-weight: 800; fill: var(--tint-c); }
.np-lane-n { font-size: var(--chart-text-xs); font-weight: 600; fill: var(--text-faint); }
.np-dot { opacity: 0.42; cursor: pointer; }
.np-dot-best { stroke: var(--chart-halo); stroke-width: 1.6; cursor: pointer; }
/* Standings search: matches ringed, the rest of the field pushed back. */
.np-dim { opacity: 0.12; }
.np-depth.np-dim { opacity: 0.3; }  /* a team keeps a readable name */
.np-dot.np-hit, .np-dot-best.np-hit { opacity: 1; stroke: var(--text); stroke-width: 1.6; }
.np-depth { cursor: pointer; }
.np-depth-name { font-size: var(--chart-text-sm); font-weight: 600; fill: var(--text-2); }
.np-depth-dot { stroke: var(--chart-halo); stroke-width: 1.2; cursor: pointer; }
/* The span between a team's best and fifth-best counting score — the row's
   actual content. Thin enough to read as a connector, not as a bar. */
.np-depth-span { stroke-width: 2; opacity: 0.35; }
.np-depth-total {
    font-size: var(--chart-text-sm); font-weight: 700; fill: var(--text-muted);
    font-variant-numeric: tabular-nums;
}
.np-depth-short { font-size: var(--chart-text-xs); font-weight: 600; fill: var(--text-faint); }

/* ── Para class ranking: the same two shapes, pointed at a sport class ────
   The ledger's dots are one athlete's published scores; a previous-period one
   is smaller and faded, as the grid fades that period's columns. In the class
   lanes, the class being read is full strength and its neighbours sit back. */
.pr-dot { fill: var(--accent); }
.pr-dot-prev { fill: var(--accent); opacity: 0.4; }
.pr-span { stroke: var(--accent); }
.pr-lane-name { fill: var(--text-muted); }
.pr-lane-dot { fill: var(--text-muted); }
.pr-lane { opacity: 0.55; }
.pr-lane-mine { opacity: 1; }
.pr-lane-mine .pr-lane-name { fill: var(--accent); }
.pr-lane-mine .pr-lane-dot { fill: var(--accent); }
/* A searched athlete, named in whichever lane they are ranked in. */
.pr-lane-hit { font-size: var(--chart-text-sm); font-weight: 700; fill: var(--text); }
.dev-end-label.spot { font-size: var(--chart-text-base); font-weight: 700; }
/* The two letters inside a school crest when no logo is on disk. Sized here
   with the other end-of-line labels; the fill is inline, because it is the
   squad's family hue. */
.dev-team-initials { font-size: var(--chart-text-micro); font-weight: 800; letter-spacing: 0.02em; }
@media (min-width: 769px) {
    .dev-end-label { font-size: var(--chart-text-xs); }
    .dev-end-label.spot { font-size: var(--chart-text-base); }
    .dev-team-initials { font-size: var(--chart-text-micro); }
}

/* Prediction view SVG text. Sizes and weights live here rather than in
   .attr('font-size', …) so they follow the breakpoint like every other chart's
   labels. Dynamic fills (an athlete's own colour) stay inline, as with
   .dev-end-label above. The 520px switch is prediction-view.js's own `mob`
   layout breakpoint, so text and row geometry change together. */
.pvx-band-label { font-size: var(--chart-text-xs); font-weight: 600; letter-spacing: .04em; }
.pvx-flag { border-radius: 1px; }
/* Race numbers off the start list. Mono + tabular so a 2-digit number does not
   shove the column it sits in. */
.pvx-row-bib, .pvx-podium-bib {
    font-family: var(--font-family-mono); font-variant-numeric: tabular-nums;
    fill: var(--chart-axis); font-size: var(--chart-text-micro);
}
.pvx-podium-bib { font-size: var(--chart-text-xs); }
/* Leading finishing place on a review row. Same mono/tabular column as a race
   number, a step darker and larger: after the race the place is the fact the
   row is about, not an identifier hung off the side of it. */
.pvx-row-pos {
    font-family: var(--font-family-mono); font-variant-numeric: tabular-nums;
    fill: var(--text-2); font-size: var(--chart-text-xs);
}
.pvx-axis-title { font-size: var(--chart-text-xs); fill: var(--chart-axis); }
/* Rotated y-axis title, drawn by drawYAxisTitle() in js/config/chart-theme.js.
   Both charts that show one had their own inline size and colour; the type lives
   here now so they cannot drift apart again. */
.y-axis-title { font-size: var(--chart-text-md); font-weight: 600; fill: var(--chart-label); }
/* The SMALL charts' rotated y caption. Same job as .y-axis-title, a size down:
   these charts have a measured gutter (axisLeftMargin in js/config/chart-theme.js)
   with only SMALL_Y_CAPTION_W to spare, so a 14px title would sit on the ticks.
   The two charts that had one each set 9px inline, which is what this replaces. */
.axis-caption { font-size: var(--chart-text-micro); fill: var(--chart-axis); }
@media (min-width: 769px) { .axis-caption { font-size: var(--chart-text-xs); } }
.pvx-medal { font-size: var(--chart-text-xl); }
.pvx-podium-name { font-size: var(--chart-text-md); font-weight: 700; }
.pvx-podium-name.is-focus { font-weight: 900; }
.pvx-podium-mark { font-size: var(--chart-text-sm); }
.pvx-podium-pct { font-size: var(--chart-text-md); font-weight: 800; font-family: var(--font-family-mono); }
.pvx-podium-cap { font-size: var(--chart-text-xs); }
.pvx-podium-win { font-size: var(--chart-text-md); font-weight: 700; font-family: var(--font-family-mono); fill: var(--text-2); }
/* Row labels carried a SIZE here and took their colour from an inline `fill`
   attribute in prediction-view.js. Any other chart reusing the class (the
   Forecast tab's range chart) therefore got the SVG default — pure black, which
   is invisible on the dark surface. The default belongs here with every other
   static fill in this file; a focus colour is a dynamic fill and stays inline,
   set as a `style` so it still wins over these rules. */
.pvx-row-name { font-size: var(--chart-text-sm); font-weight: 500; fill: var(--text-2); }
.pvx-row-name.is-focus { font-weight: 800; }
.pvx-row-val { font-size: var(--chart-text-micro); font-family: var(--font-family-mono); fill: var(--chart-axis); }
.pvx-lane-name { font-size: var(--chart-text-sm); font-weight: 400; }
.pvx-lane-name.is-focus { font-size: var(--chart-text-base); font-weight: 800; }
.pvx-lane-val { font-size: var(--chart-text-xs); font-family: var(--font-family-mono); }
.pvx-leg-label { font-size: var(--chart-text-nano); font-weight: 400; }
.pvx-leg-label.is-pick { font-size: var(--chart-text-micro); font-weight: 700; }
.pvx-leg-label.is-focus { font-size: var(--chart-text-xs); font-weight: 800; }
@media (min-width: 520px) {
    .pvx-band-label { font-size: var(--chart-text-base); }
    .pvx-axis-title { font-size: var(--chart-text-sm); }
    .pvx-medal { font-size: var(--chart-text-xl); }
    .pvx-podium-name { font-size: var(--chart-text-lg); }
    .pvx-podium-mark { font-size: var(--chart-text-base); }
    .pvx-podium-pct { font-size: var(--chart-text-lg); }
    .pvx-podium-cap { font-size: var(--chart-text-sm); }
    .pvx-podium-win { font-size: var(--chart-text-lg); }
    .pvx-row-name { font-size: var(--chart-text-base); }
    .pvx-row-val { font-size: var(--chart-text-micro); }
    .pvx-lane-name { font-size: var(--chart-text-sm); }
    .pvx-lane-name.is-focus { font-size: var(--chart-text-base); }
    .pvx-lane-val { font-size: var(--chart-text-xs); }
    .pvx-leg-label { font-size: var(--chart-text-nano); }
    .pvx-leg-label.is-pick { font-size: var(--chart-text-xs); }
    .pvx-leg-label.is-focus { font-size: var(--chart-text-sm); }
    .pvx-row-bib { font-size: var(--chart-text-micro); }
    .pvx-row-pos { font-size: var(--chart-text-sm); }
    .pvx-podium-bib { font-size: var(--chart-text-sm); }
}

/* ── Grid lines: whisper-quiet (the faintest layer) ── */
.grid line {
    stroke: var(--chart-grid);
    stroke-opacity: 1;
    stroke-width: 0.5;
    shape-rendering: crispEdges;
}
.grid path.domain { display: none; }

/* ── Athlete paths ── */
/* Base opacity comes from --data-line-op (CSS wins over the SVG opacity
   *attribute*, so this is the real control). The token is boosted in dark
   (tokens.css) so the data doesn't go faint against the dark surface. */
.athlete-path {
    fill: none;
    stroke-width: 1.5;
    opacity: var(--data-line-op);
    stroke-linecap: round;
    stroke-linejoin: round;
    pointer-events: none;
    transition: opacity 0.2s, stroke-width 0.2s;
}
.athlete-path.hidden { display: none; }
.athlete-path-hitarea { pointer-events: stroke; stroke: transparent; stroke-width: 12; fill: none; }
.athlete-path-hitarea.hidden { display: none; pointer-events: none; }
.athlete-path.spotlighted {
    opacity: 1 !important;
    stroke-width: 2.8 !important;
}
.athlete-path.spotlight-dimmed { opacity: 0.06 !important; }
/* On the dark surface 0.06 is invisible — keep a faint trace of the field. */
:root[data-theme="dark"] .athlete-path.spotlight-dimmed { opacity: 0.14 !important; }

.athlete-circle {
    opacity: 0.65;
    stroke: var(--chart-halo);
    stroke-width: 1.2;
    transition: all 0.2s;
}
.athlete-circle.hidden { display: none; }
.athlete-circle.spotlighted { opacity: 1 !important; }
.athlete-circle.spotlight-dimmed { opacity: 0.04 !important; }

/* ── Radar / Spider ── */
.radar-area {
    fill-opacity: 0.08;
    stroke-width: 1.8;
    transition: all 0.2s;
}
.radar-area:hover {
    fill-opacity: 0.18;
    stroke-width: 2.2;
}
.radar-area.hidden { display: none; }
/* The outline of a polygon that carries an estimated axis, drawn edge by edge so
   the estimated ones can be dotted. Same weight as the path stroke it replaces;
   no fill of its own (the polygon underneath still carries it). */
.radar-edge { fill: none; stroke-width: 1.8; }
.radar-edge.hidden { display: none; }
.radar-dots { fill-opacity: 0.8; }
.radar-dots.hidden { display: none; }
/* NCAA Teams mode. A squad's polygon is an average over several athletes, so it
   carries a heavier stroke than an individual's line. */
.radar-area-team { fill-opacity: 0.13; stroke-width: 2.6; }
.radar-area-team:hover { fill-opacity: 0.22; }

/* ── Tooltip card (theme-aware) ── */
.tooltip {
    background: var(--surface-raised);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 8px;
    box-shadow: var(--shadow-2);
    padding: var(--space-5) var(--space-6);
    font-size: var(--text-base);
    line-height: 1.5;
}
@media (min-width: 769px) {
    .tooltip { padding: var(--space-6) var(--space-7); font-size: var(--text-base); }
}

/* ── Chart containers: no tinted background ── */
.chart-container {
    background: var(--surface);
    border: 1px solid var(--border-soft);
    border-radius: var(--radius-lg);
}
.chart-container.active { border-color: var(--border); }

.chart-title {
    background: transparent;
    border-bottom: 1px solid var(--border-soft);
    font-weight: 600;
    color: var(--text-2);
}

/* ── Segment header bands (rank + dev charts) ── */
.x-section-rect { rx: 2; ry: 2; transition: fill var(--transition-fast); -webkit-tap-highlight-color: transparent; }
.x-section-rect:hover { filter: brightness(0.94); }
.x-section-rect.active {
    stroke: var(--text-2);
    stroke-width: 1.5;
}

/* ── Inline filter bar ── */
.inline-filters {
    background: var(--surface);
    border: 1px solid var(--border-soft);
    border-radius: var(--radius-md);
}

/* ── Dev chart scroll container ── */
.scroll-container {
    border-color: var(--border-soft) !important;
    background: var(--surface) !important;
}

/* Spotlight labels */
.spotlight-name {
    pointer-events: none;
    text-shadow: 0 0 4px var(--chart-halo), 0 0 8px var(--chart-halo), 0 0 12px var(--chart-halo);
    font-weight: 600;
}

/* Replay labels that MOVE every frame (watched names, exit notes) take their
   halo as a stroke painted under the fill, never a CSS text-shadow. iOS Safari
   repaints SVG text within the glyph bounds, and a text-shadow's blur reaches
   past them, so a moving label left strips of its old shadow behind: "shadow
   names", clipped on one side, parked where the riders had been (the bottom of
   the Pontevedra bike course, 25 Sep 2026). A stroke is part of the text's own
   geometry, so the repaint that moves the text also erases it. */
.wlabel, .exit-note {
    pointer-events: none;
    font-weight: 600;
    paint-order: stroke;
    stroke: var(--chart-halo);
    stroke-width: 3px;
    stroke-linejoin: round;
}

.rbadge {
    font-size: var(--text-xs);
    font-weight: 600;
    padding: 1px var(--space-3);
    border-radius: var(--radius-sm);
}
.rbadge.top { background: var(--badge-green-bg); color: var(--badge-green-ink); }
.rbadge.mid { background: var(--badge-amber-bg); color: var(--badge-amber-ink); }
.rbadge.low { background: var(--badge-red-bg); color: var(--badge-red-ink); }

/* ── Replay controls: a media bar ──
   Three rows, each a 44px tap height: the scrubber with the time either side,
   the transport, then the settings you still change mid-race (Course/Timeline,
   Focus, Options). Plain glyphs in the text colour at one weight: the filled
   accent disc Play used to be read as childish (26 Sep 2026). The live board
   (live-view.js _transport) uses the same bar. */
.replay-controls {
    display: flex;
    flex-direction: column;
    gap: 0;
    padding: 0 var(--space-5) var(--space-2);
    margin-bottom: var(--space-2);
}
.rc-scrub {
    display: flex; align-items: center; gap: var(--space-4);
    min-height: 44px;
    font-variant-numeric: tabular-nums; white-space: nowrap;
}
.rc-scrub .rc-cur { font-size: var(--text-md); font-weight: 700; color: var(--text); min-width: 3.6em; }
.rc-scrub .rc-max { font-size: var(--text-base); font-weight: 600; color: var(--text-muted); min-width: 3.6em; text-align: right; }
/* The track you see is the shared 6px slider track (styles.css, "Unified
   range slider"); the input itself is the whole 44px row, so a thumb that lands
   a little high or low still scrubs. The played part is painted ON the track:
   `input.` is needed to outrank the shared input[type="range"] rules, which is
   why the elapsed fill had silently stopped showing. */
input.rc-timeline { flex: 1 1 auto; width: auto; min-width: 80px; height: 44px; }
input.rc-timeline::-webkit-slider-runnable-track {
    background: linear-gradient(to right, var(--accent) 0 var(--rc-pct, 0%), var(--border-strong) var(--rc-pct, 0%) 100%);
}
input.rc-timeline::-moz-range-track {
    background: linear-gradient(to right, var(--accent) 0 var(--rc-pct, 0%), var(--border-strong) var(--rc-pct, 0%) 100%);
}

.rc-transport { display: flex; align-items: center; justify-content: center; gap: var(--space-4); min-height: 44px; }
/* Every transport glyph: a 44px square, no fill, no frame until pressed. */
.rc-btn {
    width: 44px; height: 44px; flex: none; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    border: none; border-radius: var(--radius-md);
    background: transparent; color: var(--text-2);
    font-size: var(--text-lg); font-weight: 700; font-variant-numeric: tabular-nums;
    cursor: pointer; -webkit-tap-highlight-color: transparent;
    transition: background var(--transition-fast), color var(--transition-fast);
}
.rc-btn:hover { background: var(--surface-tint); color: var(--text); }
.rc-btn:active { background: var(--surface-hover); }
.rc-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Play leads by weight and a hairline ring, not by colour. */
.rc-play-btn, .rc-pause-btn { color: var(--text); box-shadow: inset 0 0 0 1.5px var(--border-strong); border-radius: 50%; }
.rc-speed { display: inline-flex; align-items: center; flex: none; margin-left: var(--space-5); }
.rc-speed-value {
    font-size: var(--text-md); font-weight: 800; color: var(--accent-ink);
    min-width: 34px; text-align: center; font-variant-numeric: tabular-nums;
}
/* Row 3: the settings row. Options (the filter) rides at its end; when its
   panel opens it takes the row's full width below. */
.rc-toolbar { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; min-height: 44px; }
.rc-focus-btn { font-size: var(--text-base); }
.rc-toolbar .replay-filter { margin-left: auto; }
.rc-toolbar .replay-filter .inline-filters { margin: 0; padding: 0; background: none; border: 0; box-shadow: none; }
.rc-toolbar .replay-filter:has(.filter-options-panel.open) { flex-basis: 100%; margin-left: 0; }

/* Replay map labels (leg names, START/FINISH, leg-leaders) sit over the course
   route, so they were unreadable where they crossed a leg line. A white halo
   behind the text keeps them legible without moving them; non-scaling-stroke so
   the halo stays a crisp constant thickness as the map zooms. */
.map-annot {
    paint-order: stroke;
    stroke: var(--chart-halo);
    stroke-width: 3.5px;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
}
@media (min-width: 769px) {
    .rc-focus-btn { font-size: var(--text-base); }
    .rc-scrub .rc-cur { font-size: var(--text-md); }
}
/* Phone: the same bar at a lighter weight. On a desktop it sits in a wide card
   and reads as light; on a phone the same 22px glyphs, bold 15px "−30" and the
   ringed Play filled the row and read as clunky, childish (26 Sep 2026). The
   tap areas stay 44px: only the ink shrinks. Play leads by being the darkest
   mark, not by a ring. */
@media (max-width: 768px) {
    .rc-transport { gap: var(--space-2); }
    .rc-btn { font-size: var(--text-md); font-weight: 600; color: var(--text-muted); }
    .rc-btn svg { width: 15px; height: 15px; }
    .rc-play-btn, .rc-pause-btn { box-shadow: none; border-radius: var(--radius-md); color: var(--text); }
    .rc-play-btn svg, .rc-pause-btn svg { width: 20px; height: 20px; }
    .rc-speed-dn, .rc-speed-up { font-size: var(--text-lg); font-weight: 500; }
    .rc-speed-value { font-size: var(--text-base); font-weight: 700; }
    .rc-scrub .rc-cur { font-size: var(--text-md); }
    .rc-scrub .rc-max { font-size: var(--text-sm); }
    /* The track sits 8px low in its 44px box, closer to the transport glyphs
       it belongs with: padding moves the drawn line, not the box, so the tap
       height and the bar's height are unchanged. The times follow it. */
    input.rc-timeline { box-sizing: border-box; padding-top: 16px; }
    .rc-scrub .rc-cur, .rc-scrub .rc-max { padding-top: 16px; }
    /* The Reel button ends the scrub row on a phone; its glyph sits on the
       times' line, its 44px box unchanged. */
    .rc-scrub .rc-reel { flex: none; padding-top: 16px; margin-left: calc(-1 * var(--space-2)); }
}

/* ── Replay ticker ── */
.replay-ticker {
    background: var(--surface) !important;
    border-color: var(--border-soft) !important;
    font-size: var(--text-base);
}
.replay-ticker-header {
    font-size: var(--text-sm) !important;
    color: var(--text-muted) !important;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    border-bottom-color: var(--border-soft) !important;
}

/* ── Live board ──────────────────────────────────────────────────────────
   A live race arrives in beats, so the board is built around the wait: a
   clock and a filling bar to the next split, then the state of the race as
   packs, then the chase arithmetic for whoever is being watched. Everything
   is tokenized so it follows the theme. */
.lv-board { display: flex; flex-direction: column; gap: var(--space-4); }

/* Following a real feed there is no transport, because there is nothing to
   scrub to. This strip takes its place: live, how old the data is, and a way
   into the poll log. The AGE is the load-bearing part — a board that has
   silently stopped updating is indistinguishable from a quiet race. */
.lv-livebar {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    font-size: var(--text-xs);
}
.lv-live-dot {
    width: 8px; height: 8px; border-radius: 50%;
    background: var(--danger); flex: none;
    animation: lv-pulse 2s ease-in-out infinite;
}
@keyframes lv-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@media (prefers-reduced-motion: reduce) { .lv-live-dot { animation: none; } }
.lv-live-tag { font-weight: 700; letter-spacing: 0.04em; color: var(--text-2); }
.lv-live-sim {
    font-weight: 700; color: var(--surface); background: var(--text-muted);
    border-radius: var(--radius-sm); padding: 1px 4px; margin-left: 4px;
}
.lv-live-age { color: var(--text-muted); }
.lv-live-err { color: var(--danger); margin-left: auto; }
.lv-livebar .lv-diag-btn { margin-left: auto; }
.lv-live-err:not(:empty) + .lv-diag-btn { margin-left: var(--space-3); }
/* The feed has gone quiet: the dot stops pretending everything is fine. */
.lv-livebar.stale .lv-live-dot { background: var(--text-muted); animation: none; }

.lv-waiting {
    text-align: center; padding: var(--space-6) var(--space-4);
    color: var(--text-muted); font-size: var(--text-sm);
}
.lv-waiting-title { font-size: var(--text-lg); font-weight: 700; color: var(--text); }
/* -sub is the start-list line alone; the time and progress lines under it are
   -note, so a selector for the start list never matches three elements. */
.lv-waiting-sub, .lv-waiting-note { margin-top: var(--space-2); }
.lv-waiting-start { font-variant-numeric: tabular-nums; }
.lv-waiting-note:empty { display: none; }

/* Poll log. Deliberately plain: read once a year, under pressure. */
.lv-diag-panel {
    margin-top: var(--space-3);
    padding: var(--space-3);
    font-family: var(--font-family-mono);
    font-size: var(--text-xs);
    overflow-x: auto;
}
.lv-diag-head {
    display: flex; flex-wrap: wrap; gap: var(--space-3);
    color: var(--text-muted); padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--border); margin-bottom: var(--space-2);
}
.lv-diag-log { width: 100%; border-collapse: collapse; }
.lv-diag-log td { padding: 2px var(--space-2); color: var(--text-2); white-space: nowrap; }
.lv-diag-log tr.bad td { color: var(--danger); }

.lv-beat {
    padding: var(--space-4) var(--space-5);
}
.lv-beat-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-4); }
/* Following a race: time since the start sits dead centre between the mat clock
   and the mat name. Space-between only centres it when the two sides are the same
   width, which "31:49" and "Bike Link" are not, so the row becomes 1fr/auto/1fr. */
.lv-beat-row:has(> .lv-race-clock) { display: grid; grid-template-columns: 1fr auto 1fr; }
.lv-beat-row:has(> .lv-race-clock) .lv-next { justify-self: end; }
.lv-race-clock {
    font-family: var(--font-family-mono); font-variant-numeric: tabular-nums;
    font-size: var(--text-md); font-weight: 700; color: var(--danger);
}
.lv-clock {
    font-family: var(--font-family-mono); font-variant-numeric: tabular-nums;
    font-size: var(--text-xl); font-weight: 800; color: var(--text);
}
.lv-next { text-align: right; display: flex; flex-direction: column; }
.lv-next-label { font-size: var(--text-sm); font-weight: 700; color: var(--text-2); }
.lv-next-eta { font-family: var(--font-family-mono); font-size: var(--text-xs); color: var(--text-muted); }
.lv-fill { height: 4px; border-radius: 2px; background: var(--surface-tint); margin: var(--space-3) 0 var(--space-2); overflow: hidden; }
.lv-fill span { display: block; height: 100%; background: var(--accent); transition: width .3s linear; }
.lv-through { font-size: var(--text-xs); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.lv-beat-done .lv-beat-label { margin-left: var(--space-4); font-size: var(--text-sm); color: var(--text-muted); }
.lv-road { display: flex; flex-direction: column; }
.lv-road-head { font-size: var(--text-xs); font-weight: 700; color: var(--text-muted); letter-spacing: .04em; margin-bottom: var(--space-2); }
.lv-group { display: flex; flex-direction: column; }
/* A pack is one thing on the road, so it is one row carrying one number: how
   far behind the pack in front. Opening it lists who is in it, in the order
   they crossed the mat, and each name is a watch toggle. */
.lv-pack {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    width: 100%;
    padding: var(--space-3) var(--space-4);
    cursor: pointer;
    text-align: left;
    font: inherit;
    color: inherit;
}
.lv-pack:hover { background: var(--surface-tint); }
.lv-caret { color: var(--text-muted); font-size: var(--text-xs); }
.lv-members {
    display: flex; flex-direction: column;
    margin: 2px 0 var(--space-2) var(--space-5);
    border-left: 2px solid var(--border); padding-left: var(--space-3);
}
.lv-member {
    display: flex; align-items: baseline; gap: var(--space-3);
    background: none; border: 0; padding: var(--space-2) var(--space-3);
    cursor: pointer; text-align: left; font: inherit; color: var(--text-2);
    border-radius: var(--radius-sm);
}
.lv-member:hover { background: var(--surface-tint); }
.lv-member.on { color: var(--accent); font-weight: 700; }
/* A row with nothing to open: positions shown before the board can build. */
.lv-member--static { cursor: default; }
.lv-member--static:hover { background: none; }
.lv-waiting-list { padding: 0 var(--space-3) var(--space-4); }
/* The finish order: a flat list, not a road, so no indent rail. */
.lv-finish { margin-left: 0; border-left: 0; padding-left: 0; }
.lv-finish-sub { margin-top: var(--space-4); padding-left: var(--space-3); }
.lv-mk { font-family: var(--font-family-mono); color: var(--text-muted); font-size: var(--text-xs); min-width: 2ch; }
.lv-mn { font-size: var(--text-sm); }
/* The current place after a name on the road board: quiet, never bold. */
.lv-place { color: var(--text-muted); font-weight: var(--font-weight-normal); font-variant-numeric: tabular-nums; }
/* In a member row it is its own flex item (the name stays the name), pulled
   back so it sits beside the name rather than a column's gap away. */
.lv-member .lv-place { margin-left: calc(var(--space-2) * -1); font-size: var(--text-sm); }
.lv-mc { font-size: var(--text-xs); color: var(--text-muted); }
.lv-mg { margin-left: auto; font-family: var(--font-family-mono); font-variant-numeric: tabular-nums; font-size: var(--text-xs); color: var(--text-2); }
/* A pack carrying a watched athlete keeps the watch-list accent, so it never
   disappears into the field however far down the road it is. */
.lv-pack-watched { border-color: var(--accent); box-shadow: inset 3px 0 0 var(--accent); }
.lv-size {
    font-family: var(--font-family-mono); font-weight: 800; color: var(--text-2);
    min-width: 2.5ch; text-align: right; font-variant-numeric: tabular-nums;
}
.lv-names { color: var(--text-2); font-size: var(--text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A row naming several watched athletes wraps rather than cutting the list
   off: the names ARE the answer to "where are the people I follow". */
.lv-pack-watched .lv-names { white-space: normal; overflow: visible; }
.lv-pgap { margin-left: auto; font-family: var(--font-family-mono); font-variant-numeric: tabular-nums; font-weight: 700; color: var(--text); }
/* The separation between two packs, drawn where it is on the road. The strip
   grows with the gap, so the shape of the race reads before any number does. */
.lv-between {
    display: flex; align-items: center; gap: var(--space-3);
    min-height: var(--gap-h, 10px);
    padding-left: var(--space-5); font-size: var(--text-xs);
    color: var(--text-muted); font-variant-numeric: tabular-nums;
    border-left: 2px dotted var(--border); margin-left: var(--space-5);
}
.lv-drop::before { content: '↓ '; }
.lv-findwrap { position: relative; }
.lv-hit {
    display: flex; align-items: baseline; gap: var(--space-3); width: 100%;
    background: none; border: 0; padding: var(--space-3) var(--space-4);
    cursor: pointer; text-align: left; font: inherit; color: var(--text-2);
}
.lv-hit:hover { background: var(--surface-tint); }
.lv-hit.on { color: var(--accent); font-weight: 700; }
.lv-empty { font-size: var(--text-sm); color: var(--text-muted); padding: var(--space-3) 0; }
.lv-tail { font-size: var(--text-xs); color: var(--text-muted); padding: var(--space-3) 0 0 var(--space-5); }

/* Watched athletes ride on the pack row rather than in a panel of their own. */
.lv-watched-name { color: var(--accent); font-weight: 700; }
.lv-lens { display: flex; justify-content: flex-end; }

/* Race notes: what the timer has said about athletes. Amber throughout, since
   red on this site means LIVE; the tag's word, not its colour, says out. */
/* A two-column grid so every name starts at the same x, whatever the tag. */
.lv-notes {
    display: grid; grid-template-columns: max-content 1fr;
    column-gap: var(--space-3); row-gap: var(--space-2); align-items: baseline;
    border-color: var(--warning);
    box-shadow: inset 3px 0 0 var(--warning);
    padding-left: calc(var(--space-4) + 3px);
}
.lv-note { display: contents; font-size: var(--text-sm); }
.lv-note > * { font-size: var(--text-sm); }
.lv-note-tag {
    justify-self: start;
    font-size: var(--text-xs); font-weight: 700; letter-spacing: .02em;
    color: var(--warning); background: var(--warning-bg);
    border-radius: var(--radius-sm); padding: 1px var(--space-2);
}
.lv-note-body { display: flex; flex-wrap: wrap; column-gap: var(--space-2); min-width: 0; }
.lv-note-who { font-weight: 700; color: var(--text); }
.lv-note.on .lv-note-who { color: var(--accent); }
.lv-note-text { color: var(--text-2); }
.lv-note-body .lv-note-where { color: var(--text-muted); font-size: var(--text-xs); }
.lv-note-tag.lv-note-tag { font-size: var(--text-xs); }
.lv-note--new > * { animation: lv-note-in 1.6s ease-out; }
@keyframes lv-note-in { 0% { background: var(--warning-bg); } 100% { background: transparent; } }
@media (prefers-reduced-motion: reduce) { .lv-note--new > * { animation: none; } }
.lv-notes-more {
    grid-column: 2; justify-self: start;
    min-height: 44px; margin-block: calc(var(--space-2) * -1);
    padding: 0; background: none; border: 0; cursor: pointer;
    font-size: var(--text-xs); color: var(--text-muted);
}
.lv-note-tag--row { font-weight: 600; }
.lv-lens .switcher-btn { font-size: var(--text-xs); }

/* A watched athlete opened out to the same segment table the charts show. */
.lv-segments {
    margin: 2px 0 var(--space-3) var(--space-6);
    padding: var(--space-3) var(--space-4);
    background: var(--surface-tint); border-radius: var(--radius-md);
}
.lv-segments table { width: 100%; }


/* ── Relay: the leg layer on the live board (main.js _relayLegBar) ─────────── */
.rl-legbar { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); }
.rl-where { font-size: var(--text-sm); color: var(--text-muted); text-align: center; }

/* ── Field map (js/charts/field-map.js): the athlete page's two-leg scatter ──
   No words on the chart: axis names, tick numbers and a number on each picked
   mark are all the text there is. Pick colours are data and are set as SVG
   paint by the chart; everything here is chrome. */
/* Rows of controls 44px apart centre to centre, so one row's tap area never
   cuts the next one's short (UI-CONVENTIONS, tap areas, rule 3). */
.fm-controls { display: flex; flex-wrap: wrap; gap: var(--space-5) var(--space-3); margin-bottom: var(--space-5); }
.fm-search { flex: 1 1 9rem; min-width: 0; }
.fm-picks { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.fm-picks:empty { display: none; }
/* The page's own athlete: a chip with no ×, so it needs the right-hand room the × zone gives the others. */
.fm-me { padding-right: var(--space-3); }
/* The chart takes every gesture: pinch and drag zoom it (field-map.js). */
.fm-svg { display: block; touch-action: none; }
.fm-reset[hidden] { display: none; }
.fm-axis-name { font-size: var(--chart-text-sm); font-weight: var(--font-weight-semibold); fill: var(--text-muted); }
.fm-axis-unit { font-weight: var(--font-weight-normal); }
.fm-zero { stroke: var(--border-soft); }
.fm-dot { fill: var(--text-faint); fill-opacity: 0.4; }
.fm-dot--in { fill: var(--accent); fill-opacity: 0.7; }
.fm-dot--out { fill-opacity: 0.15; }
.fm-guide { stroke-opacity: 0.4; stroke-dasharray: 2 3; }
.fm-unsure { fill-opacity: 0.16; stroke-width: 1.5; }
.fm-num {
    font-size: var(--chart-text-sm); font-weight: var(--font-weight-bold);
    paint-order: stroke; stroke: var(--surface); stroke-width: 3px; stroke-linejoin: round;
}
.fm-hit { fill: transparent; cursor: pointer; }
/* ============================================================================
   SplitLab Design System — "Lab Slate"  (see design/SYSTEM.md)
   Single source of truth for design tokens, light AND dark.
   Loaded LAST so it consolidates the drifted per-file tokens: the new semantic
   roles are defined here, and every legacy token name is re-pointed at them as
   a live `var()` alias — so the existing CSS (3.9k lines) adapts to the system
   and to dark mode without rewriting each rule. Only hardcoded hex values (and
   d3-inlined chart chrome) still need hand-migration.
   Type scale, spacing, weights, chip/discipline/medal tokens are left to their
   original files (unchanged by the redesign).
   ============================================================================ */

:root {
    color-scheme: light;

    /* ── Semantic surfaces ───────────────────────────────────────────────── */
    --bg-app: #f8fafc;
    --surface: #ffffff;
    --surface-raised: #ffffff;   /* tooltips, dropdowns, popovers, sticky bars */
    --surface-sunken: #fafbfc;   /* recessed panels inside cards */
    --surface-hover: #f1f5f9;
    --surface-tint: #eef2f6;     /* zebra, muted chips, rank-badge bg */
    --surface-selected: #f1f0fd; /* OPAQUE accent tint for a selected row — safe under sticky columns (accent-subtle is translucent and leaks when scrolled) */
    /* Always-dark chrome (hero toolbars, dark tooltips): NOT re-defined in dark,
       so it stays dark in both themes. Pair with white/near-white text. */
    --surface-inverse: #1e293b;
    --on-surface-inverse: #f8fafc;
    /* Share sheet: an always-dark preview modal, so a share card is judged on
       the dark ground it will be posted onto rather than on the app's theme.
       NOT re-defined in dark, same rule as --surface-inverse above. These were
       27 literals inside a stylesheet built from a JS string in share-card.js,
       which put them outside every search of css/. */
    --sheet-scrim: rgba(2, 6, 15, .74);
    --sheet-bg: #0b1220;
    --sheet-well: #060a12;      /* recessed frame the card previews inside */
    --sheet-raised: #0f172a;    /* buttons on the sheet */
    --sheet-line: #1e293b;
    --sheet-ink: #e2e8f0;
    --sheet-ink-dim: #94a3b8;
    --sheet-raised-on: #171c34;     /* a selected button on the sheet */
    --sheet-accent: #6366f1;
    --sheet-alpha-base: #ffffff;    /* the 'white background' half of the preview */
    --sheet-alpha-check: #d1d5db;   /* transparency checkerboard */

    /* Always-white tile behind NCAA team logos: most logos are authored on a
       solid white background, so they need a white backing in BOTH themes to sit
       cleanly on coloured rows / selection tints / dark mode. NOT re-defined in
       dark. The ring is a translucent hairline so it reads on light backgrounds
       and disappears against dark ones (where the white tile already contrasts). */
    --logo-tile: #ffffff;
    --logo-tile-ring: rgba(15, 23, 42, .12);

    /* ── Borders ─────────────────────────────────────────────────────────── */
    --border: #e2e8f0;
    --border-soft: #eef2f6;
    --border-strong: #cbd5e1;

    /* ── Text ────────────────────────────────────────────────────────────── */
    --text: #0f172a;
    --text-2: #334155;
    --text-muted: #64748b;
    --text-faint: #94a3b8;

    /* ── Accent (one brand: indigo) ──────────────────────────────────────── */
    --accent: #4f46e5;
    --accent-hover: #4338ca;
    --accent-ink: #4f46e5;       /* accent-colored TEXT/links on a surface */
    --accent-ink-dim: #a5a2ee;   /* accent TEXT at reduced weight — a figure that still counts but is being devalued (para's deprecating period) */
    --accent-subtle: rgba(79, 70, 229, .08);
    --on-accent: #ffffff;

    /* ── State ───────────────────────────────────────────────────────────── */
    --success: #047857;
    --success-bg: #ecfdf5;
    --danger: #dc2626;
    --danger-bg: #fee2e2;
    --warning: #b45309;
    --warning-bg: #fffbeb;
    --focus-ring: rgba(79, 70, 229, .30);

    /* ── Soft status badges (one canonical tint per hue; adapts to dark) ──────
       Replaces a scatter of one-off pastel hexes on rank/position/ability/T100
       pills so they share a look and don't glare on dark. */
    --badge-green-bg:   rgba(16, 185, 129, .12);  --badge-green-ink:  #047857;
    --badge-amber-bg:   rgba(245, 158, 11, .15);  --badge-amber-ink:  #b45309;
    --badge-red-bg:     rgba(239, 68, 68, .12);   --badge-red-ink:    #b91c1c;
    --badge-blue-bg:    rgba(59, 130, 246, .12);  --badge-blue-ink:   #1d4ed8;
    --badge-violet-bg:  rgba(139, 92, 246, .12);  --badge-violet-ink: #6d28d9;
    --badge-cyan-bg:    rgba(6, 182, 212, .12);   --badge-cyan-ink:   #0e7490;
    --badge-neutral-bg: rgba(100, 116, 139, .14); --badge-neutral-ink: #475569;

    /* ── Discipline triad (swim / bike / run) ────────────────────────────────
       One source for the three colours the whole app reads a race by. They were
       duplicated as literal hex in chart-theme.js and in a SEG_COLOR map in
       several views, which is why nothing could reason about them: the NCAA
       division palette was allowed to collide with them, and the values had
       never been checked for contrast.

       Two roles, because one value cannot serve both: --seg-* is the MARK (a
       chart line, a swatch, a fill) where the hue matters and the contrast bar
       is 3:1 non-text; --seg-*-ink is TEXT (a caption, a label) and clears
       4.5:1 on its own ground. The mark values are the app's established hues
       and are unchanged; only the ink is adjusted, and only where it had to be. */
    --seg-swim: #3b82f6;   --seg-swim-ink: #196cf4;
    --seg-bike: #16a34a;   --seg-bike-ink: #12863d;
    --seg-run:  #dc2626;   --seg-run-ink:  #dc2626;

    /* ── Medals ──────────────────────────────────────────────────────────────
       IDENTITY colour, the same class of thing as a flag: gold is gold on both
       grounds, so there is deliberately no dark override below. Moved here from
       styles.css on 3 Sep 2026 — they resolved correctly there, which is exactly
       why nobody noticed the one value family that did not live in the single
       source. Anything reading these through cssVar() is checked by
       tests/style-surface-budget.spec.js, which reads all three stylesheets. */
    --gold:   #FFD700;
    --silver: #C0C0C0;
    --bronze: #CD7F32;

    /* ── Ordered-category ramps (race history) ───────────────────────────────
       NCAA divisions and WT race distances are ORDERED sets, not categorical
       ones, so each is a single-hue ramp rather than a colour triad. A ramp
       reads as "one family, N levels", which is what they are — and it can
       never be mistaken for the categorical discipline hues (swim blue / bike
       green / run red) the way the old per-division triad was: it gave DIII
       bike's exact green, DII a near-swim blue and DI a near-run red.

       Higher step = higher rank (DI, longer race) = more contrast against the
       ground, which is why dark mode is not a flip of these values. Divisions
       are drawn as small bold label text so every step clears 4.5:1; distances
       are only dots and legend swatches, so they clear 3:1 non-text. Purple and
       teal both sit well clear of the discipline hues and of --accent. */
    --rh-div-1: #b046d3;   /* DIII */
    --rh-div-2: #81259f;   /* DII  */
    --rh-div-3: #49155b;   /* DI   */
    --rh-dist-1: #0fa3b7;  /* super-sprint */
    --rh-dist-2: #0c899a;  /* sprint       */
    --rh-dist-3: #0a6f7d;  /* standard     */
    --rh-dist-4: #08555f;  /* middle       */
    --rh-dist-5: #053b42;  /* long         */

    /* ── Prediction tiers and data-source identity ───────────────────────────
       The tier bands on the prediction view and the NCAA / World Triathlon dots
       on the profile. Both were inline literals on legend swatches, so neither
       could follow the theme. */
    --tier-rest: #64748b;
    --src-ncaa: #f97316;
    --src-wt: #3b82f6;

    /* Podium card grounds and the prediction row tints. These were a DARK()
       branch inside prediction-view.js holding both themes' hexes in JS, so
       they sat outside the token system and only followed a theme switch if
       something happened to redraw. */
    --medal-1-bg: #fef3c7;
    --medal-2-bg: #f1f5f9;
    --medal-3-bg: #fff1f2;
    --medal-1-line: #f59e0b;
    --medal-2-line: #94a3b8;
    --medal-3-line: #f97316;
    --row-focus-bg: #eff6ff;
    --row-rival-bg: #f0f9ff;

    /* ── Replay course map ───────────────────────────────────────────────────
       The course is drawn straight onto --surface-sunken, with no map tile of
       its own, so every annotation has to clear contrast on BOTH grounds. They
       were hardcoded in replay-chart.js and never did: FINISH sat at 2.25:1 and
       START at 2.15:1 in dark mode, and START was under AA on light too.

       These are read through cssVar() at draw time and the replay redraws on a
       theme change, so a token here is enough. A literal in the chart is not:
       redrawing re-runs the same hardcoded value. */
    --map-start: #196cf4;          /* START label */
    --map-finish: #991b1b;         /* FINISH label */
    --map-transition: #d97706;     /* transition-zone ring */
    --map-transition-fill: rgba(251, 191, 36, .12);
    --map-transition-ink: #b45309; /* the "T" inside the zone */
    --map-handover-ink: #92400e;   /* relay HANDOVER label */
    --map-marker: #eb6c05;
    --map-flag-edge: #bbbbbb;      /* checkered finish flag hairline */

    /* ── Heat cells (matrix grids; the number sits ON the fill) ─────────────
       ONE ladder for every matrix in the app: the season standings (T100 /
       WTCS), the para class ranking, and race history's career, team and
       repeat-starter grids. They are the same table showing the same kind of
       quantity — how far up a field a result sits — so they get one scale.
       js/config/heat-scale.js is the only thing that maps a value to a tier.

       Tier 1 is faintest, tier 6 deepest; a HIGHER tier is a BETTER result.
       Every fill carries its own ink because the ink has to flip partway up
       the ramp (dark on the pale tiers, white on the deep ones) and a text
       token cannot know which tier it landed on. Every pair clears 4.5:1 —
       the cell text is small and bold — and consecutive tiers are ≥1.14
       apart so the steps stay tellable.

       The GF ladder is the same ramp in the Grand Final's amber, which is
       --major-accent's family: a Grand Final is a different kind of round and
       the app already marks it warm (rings, FINAL tags, win cells).

       Dark values are NOT flips. They run toward the LIGHT end, because on a
       dark ground prominence is lightness; the tints are selected over
       --surface and the ink flips at the same place in the ramp. */
    /* Muted, not the metals. #FFD700 / #C0C0C0 / #CD7F32 were fine when the
       podium was the only fill in the app, but the medals now land on every
       matrix -- standings and the NCAA grids gained them -- and at full
       saturation that many of them read as alarms beside the purple ramp
       rather than as a rank. Same three identities, a step off the spectral
       edge. */
    --heat-gold-bg: #f7dd7a;      --heat-gold-ink: #1e293b;
    --heat-silver-bg: #d8dee6;    --heat-silver-ink: #1e293b;
    --heat-bronze-bg: #e0ab7d;    --heat-bronze-ink: #1e293b;
    --heat-1-bg: #f2eff7;         --heat-1-ink: #334155;
    --heat-2-bg: #ddd8ec;         --heat-2-ink: #1e293b;
    --heat-3-bg: #c3bce0;         --heat-3-ink: #1e293b;
    --heat-4-bg: #a79ed2;         --heat-4-ink: #1e293b;
    --heat-5-bg: #7358ab;         --heat-5-ink: #ffffff;
    --heat-6-bg: #4c1d95;         --heat-6-ink: #ffffff;
    --heat-gf-1-bg: #fff4e3;      --heat-gf-1-ink: #334155;
    --heat-gf-2-bg: #fee3bd;      --heat-gf-2-ink: #1e293b;
    --heat-gf-3-bg: #fdcd92;      --heat-gf-3-ink: #1e293b;
    --heat-gf-4-bg: #f9ad5d;      --heat-gf-4-ink: #1e293b;
    --heat-gf-5-bg: #a85504;      --heat-gf-5-ink: #ffffff;
    --heat-gf-6-bg: #7c2d12;      --heat-gf-6-ink: #ffffff;

    /* ── Chart chrome (frames only; series/discipline colors untouched) ───── */
    --major-accent: #f59e0b;     /* major-championship / Grand-Final marker (rings, win cells, FINAL tags) */
    --chart-axis: #94a3b8;       /* tick text */
    --chart-axis-line: #e2e8f0;  /* tick marks / axis rules */
    --chart-grid: #f1f5f9;       /* gridlines */
    --chart-label: #64748b;      /* axis titles, in-SVG captions */
    --chart-halo: #ffffff;       /* label halos, dot outlines, "white" strokes */
    --chart-ref: #cbd5e1;        /* neutral reference lines (VS midline, medians) */
    --chart-band: #3b82f6;       /* statistical range band, drawn at low opacity */
    --data-line-op: 0.5;         /* unselected athlete-line opacity (boosted in dark) */

    /* ── Race-tier identity ──────────────────────────────────────────────
       What KIND of race a result was: a WTCS round, a World Cup, an NCAA
       Regional. Categorical identity, closer to a flag or a medal than to
       chart chrome. They lived as literal hex in three JS tables that had
       drifted apart — the same WTCS race drew teal on a venue history and red
       on a profile. js/config/tier-colors.js reads these and is the one source.

       THREE ROLES, because the same tier lands on three different grounds and
       no single value can clear all of them:

         --tier-*          the HUE. The identity itself. Used as a tint source
                           (a chip is `color-mix` of this with the surface) and
                           as a swatch. Never used as text.
         --tier-*-ink      TEXT and DOTS on the app surface, AND the text of a
                           tier chip, whose ground is a 14% tint of the hue. The
                           tint is the harder of the two — it pulls the ground
                           toward the ink's own hue and costs about 0.7 — so the
                           value is tuned against the tint and clears the plain
                           surface comfortably.
         --tier-*-on-dark  TEXT on always-dark chrome — the career-arc tooltip
                           is `--scrim-tooltip` in BOTH themes, so its tier name
                           needs a light value even in light mode. Defined once
                           and never re-declared; dark's ink simply aliases it.

       RAMP SPACING follows --rh-div above, which runs 4.50 / 7.78 / 13.62 with
       ~1.74 between steps. The two ordered families here (the WT red family and
       the NCAA levels) are staged at 4.5 / 6.9 / 10.6, ~1.53 apart. Raising a
       ramp to a shared 4.5 floor instead would have collapsed it: all three
       NCAA oranges landed within 1.00 of each other, one indistinguishable
       colour where there had been three. A ramp needs separated levels, not a
       common minimum. */
    --tier-olympics:        #f59e0b;
    --tier-olympics-ink:        #976106;
    --tier-olympics-on-dark:        #d38809;
    --tier-world-champs:    #dc2626;
    --tier-world-champs-ink:    #951818;
    --tier-world-champs-on-dark:    #ee9494;
    --tier-wtcs-final:      #b91c1c;
    --tier-wtcs-final-ink:      #5f0e0e;
    --tier-wtcs-final-on-dark:      #f6c6c6;
    --tier-wtcs:            #ef4444;
    --tier-wtcs-ink:            #d01212;
    --tier-wtcs-on-dark:            #f15f5f;
    --tier-t100:            #7c3aed;
    --tier-t100-ink:            #7b39ed;
    --tier-t100-on-dark:            #a374f2;
    --tier-world-cup:       #2563eb;
    --tier-world-cup-ink:       #1a5bea;
    --tier-world-cup-on-dark:       #5f8df0;
    --tier-olympic-qual:    #3b82f6;
    --tier-olympic-qual-ink:    #0b60eb;
    --tier-olympic-qual-on-dark:    #5492f7;
    --tier-conti-champs:    #0891b2;
    --tier-conti-champs-ink:    #06738e;
    --tier-conti-champs-on-dark:    #099fc3;
    --tier-conti-cup:       #059669;
    --tier-conti-cup-ink:       #047854;
    --tier-conti-cup-on-dark:       #06a674;
    --tier-multisport:      #7c2d12;
    --tier-multisport-ink:      #af4019;
    --tier-multisport-on-dark:      #e26034;
    --tier-regional-champs: #9333ea;
    --tier-regional-champs-ink: #8e2ae9;
    --tier-regional-champs-on-dark: #b26df0;
    --tier-games:           #0e7490;
    --tier-games-ink:           #0e708b;
    --tier-games-on-dark:           #139bc0;
    --tier-junior-u23:      #0d9488;
    --tier-junior-u23-ink:      #0a766d;
    --tier-junior-u23-on-dark:      #0ea496;
    --tier-para-series:     #0284c7;
    --tier-para-series-ink:     #026ea6;
    --tier-para-series-on-dark:     #0299e6;
    --tier-para-cup:        #0d9488;
    --tier-para-cup-ink:        #0a766d;
    --tier-para-cup-on-dark:        #0ea496;
    --tier-para-conti-cup:  #059669;
    --tier-para-conti-cup-ink:  #047854;
    --tier-para-conti-cup-on-dark:  #06a674;
    --tier-ncaa-nationals:  #ea580c;
    --tier-ncaa-nationals-ink:  #5a2205;
    --tier-ncaa-nationals-on-dark:  #fcd2bc;
    --tier-ncaa-regionals:  #f97316;
    --tier-ncaa-regionals-ink:  #863903;
    --tier-ncaa-regionals-on-dark:  #fba061;
    --tier-ncaa-cup:        #fb923c;
    --tier-ncaa-cup-ink:        #b05104;
    --tier-ncaa-cup-on-dark:        #f47105;
    --tier-ncaa-invite:     #fdba74;
    --tier-ncaa-invite-ink:     #9e5012;
    --tier-ncaa-invite-on-dark:     #ea9234;
    --tier-other:           #94a3b8;
    --tier-other-ink:           #5b6e89;
    --tier-other-on-dark:           #8899b0;

    /* ── Scrims ──────────────────────────────────────────────────────────
       The wash behind a modal, and the ground under a dark tooltip. These were
       four hardcoded near-blacks (rgba(0,0,0,.7), rgba(0,0,0,.9),
       rgba(15,23,42,.55), rgba(15,23,42,.96)) picked one modal at a time. A
       fixed near-black over an already-dark app reads as a dead panel, so the
       dark values lean on opacity rather than on more black. */
    --scrim:         rgba(15, 23, 42, .55);
    --scrim-strong:  rgba(15, 23, 42, .72);
    --scrim-tooltip: rgba(15, 23, 42, .96);

    /* ── Stacking order ──────────────────────────────────────────────────
       Everything that floats above the page reads from here. There were 9
       improvised values (900 / 1000 / 1100 / 1300 / 2000 / 9999 / 10000) with
       no scale, which is how .app-info-tip ended up at 1100 — under both modal
       scrims — while its two sibling tooltips sat at 10000. The order below is
       the order these things must stack; a raw z-index above 20 is a bug now.
       Values 1-20 elsewhere in this file are LOCAL: sticky table headers and
       frozen columns layering inside their own stacking context, which is a
       different problem and correctly stays local. */
    --z-header:   100;   /* pinned app header, under every floating layer */
    --z-dropdown: 200;   /* anchored panel: inline search results */
    /* 400 was --z-drawer, retired with the legacy off-canvas sidebar. The gap is
       left deliberately: the remaining steps keep the numbers they shipped with. */
    --z-menu:     500;   /* app menu, over every anchored panel */
    --z-modal:    600;   /* modal scrims + the share sheet */
    --z-tooltip:  700;   /* always on top, including over a modal */

    /* ── Radii (consolidated) ────────────────────────────────────────────── */
    --radius-sm: 4px;
    --radius-md: 6px;
    --radius-lg: 10px;           /* cards, sections, chart frames */
    --radius-xl: 14px;           /* modals, page shell */
    --radius-pill: 999px;

    /* ── Elevation (borders first; shadows mean "floats") ────────────────── */
    --shadow-1: 0 1px 2px rgba(2, 6, 23, .06);
    --shadow-2: 0 4px 12px rgba(2, 6, 23, .10);
    --shadow-3: 0 12px 32px rgba(2, 6, 23, .16);
}

/* ── Dark theme ──────────────────────────────────────────────────────────
   data-theme is resolved to "light" | "dark" by the FOUC-free head script
   (auto → the OS preference, kept live via a matchMedia listener), so only
   the semantic roles need dark values here; every alias above follows. ──── */
:root[data-theme="dark"] {
    color-scheme: dark;

    --bg-app: #0b1220;
    --surface: #131c2e;
    --surface-raised: #1b2740;
    --surface-sunken: #0e1626;
    --surface-hover: #1a2539;
    --surface-tint: #162135;
    --surface-selected: #20284d;

    --border: #243349;
    --border-soft: #1c2940;
    --border-strong: #33445f;

    --text: #e8edf6;
    --text-2: #c3cee0;
    --text-muted: #8ea0b8;
    --text-faint: #5d7089;

    --accent: #6366f1;
    --accent-hover: #818cf8;
    --accent-ink: #a5b4fc;
    --accent-ink-dim: #635fa8;   /* dimmed accent TEXT — see the light block */
    --accent-subtle: rgba(99, 102, 241, .16);
    --on-accent: #ffffff;

    --success: #34d399;
    --success-bg: rgba(52, 211, 153, .12);
    --danger: #f87171;
    --danger-bg: rgba(248, 113, 113, .12);
    --warning: #fbbf24;
    --warning-bg: rgba(251, 191, 36, .12);
    --focus-ring: rgba(129, 140, 248, .40);

    /* Discipline triad: same hues, ink lifted to clear 4.5:1 on the dark ground. */
    --seg-swim: #3b82f6;   --seg-swim-ink: #3b82f6;
    --seg-bike: #16a34a;   --seg-bike-ink: #16a34a;
    --seg-run:  #dc2626;   --seg-run-ink:  #e45656;

    /* Ordered-category ramps — selected for the dark ground, not flipped:
       higher step still means higher rank, so it runs toward the light end. */
    --rh-div-1: #be55e1;   /* DIII */
    --rh-div-2: #d38beb;   /* DII  */
    --rh-div-3: #e8c2f4;   /* DI   */
    --rh-dist-1: #1d717c;  /* super-sprint */
    --rh-dist-2: #2ba7b8;  /* sprint       */
    --rh-dist-3: #55c8d7;  /* standard     */
    --rh-dist-4: #91dce5;  /* middle       */
    --rh-dist-5: #cdeff3;  /* long         */

    --badge-green-bg:   rgba(16, 185, 129, .18);  --badge-green-ink:  #6ee7b7;
    --badge-amber-bg:   rgba(245, 158, 11, .18);  --badge-amber-ink:  #fcd34d;
    --badge-red-bg:     rgba(239, 68, 68, .20);   --badge-red-ink:    #fca5a5;
    --badge-blue-bg:    rgba(59, 130, 246, .20);  --badge-blue-ink:   #93c5fd;
    --badge-violet-bg:  rgba(139, 92, 246, .20);  --badge-violet-ink: #c4b5fd;
    --badge-cyan-bg:    rgba(34, 211, 238, .16);  --badge-cyan-ink:   #67e8f9;
    --badge-neutral-bg: rgba(148, 163, 184, .18); --badge-neutral-ink: #cbd5e1;

    /* Tiers and source identity: lifted for the dark ground. */
    --tier-rest: #94a3b8;
    --src-ncaa: #fb923c;
    --src-wt: #60a5fa;

    /* Podium grounds and row tints: translucent so the dark surface shows through. */
    --medal-1-bg: rgba(250, 204, 21, .15);
    --medal-2-bg: rgba(148, 163, 184, .15);
    --medal-3-bg: rgba(251, 146, 60, .15);
    --medal-1-line: #fbbf24;
    --medal-2-line: #cbd5e1;
    --medal-3-line: #fb923c;
    --row-focus-bg: rgba(99, 102, 241, .20);
    --row-rival-bg: rgba(99, 102, 241, .11);

    /* Replay course map: same hues, lifted to clear the dark map ground. */
    --map-start: #3b82f6;
    --map-finish: #df4c4c;
    --map-transition: #d97706;
    --map-transition-fill: rgba(251, 191, 36, .14);
    --map-transition-ink: #f4791b;
    --map-handover-ink: #d15c14;
    --map-marker: #fb923c;
    --map-flag-edge: #bbbbbb;

    /* Heat cells — the same ladder selected for the dark surface. It runs
       toward the LIGHT end (on a dark ground prominence is lightness), so
       these are not flips of the light values; the ink flips at the same
       point in the ramp. Checked over --surface: every pair ≥ 4.5:1, every
       consecutive step ≥ 1.24 apart. */
    /* SOLID here, unlike the ramp's faint tiers. They used to be translucent
       tints, which was right against the old blue ladder but not this one: a
       25% gold over the dark surface lands at luminance 0.06, below a
       mid-ramp cell, so a win read fainter than a sixth place. A medal is a
       categorical fact, not a step on the ramp, and it has to look like a
       marked cell rather than a washed-out one. */
    --heat-gold-bg: #e8c65c;                    --heat-gold-ink: #1e293b;
    --heat-silver-bg: #b9c4d2;                  --heat-silver-ink: #1e293b;
    --heat-bronze-bg: #c9955f;                  --heat-bronze-ink: #1e293b;
    --heat-1-bg: rgba(167, 139, 250, .10);      --heat-1-ink: #c4b5fd;
    --heat-2-bg: rgba(167, 139, 250, .22);      --heat-2-ink: #ddd6fe;
    --heat-3-bg: rgba(167, 139, 250, .38);      --heat-3-ink: #f0ebff;
    --heat-4-bg: #7c3aed;                       --heat-4-ink: #ffffff;
    --heat-5-bg: #a78bfa;                       --heat-5-ink: #1e293b;
    --heat-6-bg: #cdbcff;                       --heat-6-ink: #1e293b;
    --heat-gf-1-bg: rgba(251, 146, 60, .10);    --heat-gf-1-ink: #fdba74;
    --heat-gf-2-bg: rgba(251, 146, 60, .22);    --heat-gf-2-ink: #fed7aa;
    --heat-gf-3-bg: rgba(251, 146, 60, .38);    --heat-gf-3-ink: #ffeeda;
    --heat-gf-4-bg: #c2410c;                    --heat-gf-4-ink: #ffffff;
    --heat-gf-5-bg: #f97316;                    --heat-gf-5-ink: #1e293b;
    --heat-gf-6-bg: #fdba74;                    --heat-gf-6-ink: #1e293b;

    --chart-axis: #7c90ac;       /* tick text — clearly readable */
    --chart-axis-line: #3a5279;  /* axis rules / transitions — a clear step above grid */
    --chart-grid: #182339;       /* gridlines — the faintest layer */
    --chart-label: #93a5be;
    --chart-halo: #131c2e;
    --chart-ref: #33445f;
    --chart-band: #60a5fa;
    --data-line-op: 0.8;

    /* Race-tier ink on the dark ground IS the always-dark value — the tooltip
       and the dark app surface are close enough that a second set of numbers
       would be two things to keep in step for no gain. The hue and the on-dark
       value are not re-declared. */
    --tier-olympics-ink: var(--tier-olympics-on-dark);
    --tier-world-champs-ink: var(--tier-world-champs-on-dark);
    --tier-wtcs-final-ink: var(--tier-wtcs-final-on-dark);
    --tier-wtcs-ink: var(--tier-wtcs-on-dark);
    --tier-t100-ink: var(--tier-t100-on-dark);
    --tier-world-cup-ink: var(--tier-world-cup-on-dark);
    --tier-olympic-qual-ink: var(--tier-olympic-qual-on-dark);
    --tier-conti-champs-ink: var(--tier-conti-champs-on-dark);
    --tier-conti-cup-ink: var(--tier-conti-cup-on-dark);
    --tier-multisport-ink: var(--tier-multisport-on-dark);
    --tier-regional-champs-ink: var(--tier-regional-champs-on-dark);
    --tier-games-ink: var(--tier-games-on-dark);
    --tier-junior-u23-ink: var(--tier-junior-u23-on-dark);
    --tier-para-series-ink: var(--tier-para-series-on-dark);
    --tier-para-cup-ink: var(--tier-para-cup-on-dark);
    --tier-para-conti-cup-ink: var(--tier-para-conti-cup-on-dark);
    --tier-ncaa-nationals-ink: var(--tier-ncaa-nationals-on-dark);
    --tier-ncaa-regionals-ink: var(--tier-ncaa-regionals-on-dark);
    --tier-ncaa-cup-ink: var(--tier-ncaa-cup-on-dark);
    --tier-ncaa-invite-ink: var(--tier-ncaa-invite-on-dark);
    --tier-other-ink: var(--tier-other-on-dark);

    --scrim:         rgba(2, 6, 15, .68);
    --scrim-strong:  rgba(2, 6, 15, .80);
    --scrim-tooltip: rgba(2, 6, 15, .97);

    --shadow-1: 0 1px 2px rgba(0, 0, 0, .35);
    --shadow-2: 0 4px 14px rgba(0, 0, 0, .5);
    --shadow-3: 0 12px 36px rgba(0, 0, 0, .6);
}
