/**
 * Overlay — ONLY what `vendor/styles.css` does not already define. #90.
 *
 * ⚠ THIS FILE MUST STAY SMALL, AND THAT IS THE WHOLE DISCIPLINE OF THE CARRY-ACROSS.
 *
 * The look came across as one file (see `vendor/PROVENANCE.md`). Every rule added here is a place
 * where this client's appearance can drift from the product's, silently, with nothing reporting it.
 * So: if a thing looks wrong, first check whether the vendor sheet already has a class for it —
 * it defined 40 of the 43 classes the new views use — and reach for the class rather than write CSS.
 *
 * Everything below is built from the vendor sheet's own custom properties (`--accent`, `--text-mid`,
 * `--border`, `--bg-card`, `--fs-*`, `--mono`), so a palette change there moves these too instead
 * of leaving an island in last season's colours.
 */

/* ── The three kinds of nothing ──────────────────────────────────────────────────────────────────
 *
 * ⚠ These three must never be one careless palette edit away from identical. `denied` is the
 * upgrade prompt and borrows the accent the vendor sheet already uses for `.sb-upgrade`; `unknown`
 * is muted and italic; a value is just text. They also carry different WORDS ("upgrade" vs
 * "not known"), so colour alone is never load-bearing — which also keeps them distinguishable to
 * anyone who cannot rely on it.
 */
.denied {
  color: var(--accent-light, var(--accent));
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 38%, var(--border));
  border-radius: 3px; padding: 0 .28rem;
  font-size: var(--fs-2xs); font-weight: 700; font-family: var(--mono);
  cursor: help; white-space: nowrap;
}
.unknown { color: var(--text-muted); font-style: italic; }

/* ⚠ AND WHEN THE UPGRADE HAS SOMEWHERE TO GO IT STOPS BEING A `cursor: help` DEAD END. `.denied`
 * alone renders a chip that reads "upgrade" and cannot be clicked — #238's finding, and the reason
 * that chip existed with `cursor: help` for so long is that there was genuinely nowhere to send
 * anyone. The payload now carries `upgradeUrl`, so the chip that HAS one is a link and says so with
 * the cursor; the one that does not keeps the old treatment rather than faking a destination. */
.denied-link { cursor: pointer; text-decoration: none; }
.denied-link:hover, .denied-link:focus-visible {
  background: color-mix(in srgb, var(--accent) 26%, transparent);
  border-color: var(--accent);
}
.value { color: inherit; }

/* ── Badge variants the vendor sheet has no equivalent for ──────────────────────────────────────
 *
 * It defines sb-abn, sb-abn-partial, sb-delayed, sb-pred, sb-result, sb-started and sb-upgrade.
 * A trial, a nominations-only field and an emergency runner are states the legacy client showed
 * differently (or not at all on this surface), so they are the only three added — each inheriting
 * `.status-badge`'s size, weight and shape from the vendor sheet.
 */
.status-badge.sb-trial { color: var(--warn, #b45309); background: color-mix(in srgb, var(--warn, #b45309) 12%, transparent); border: 1px solid color-mix(in srgb, var(--warn, #b45309) 35%, var(--border)); }
.status-badge.sb-noms  { color: var(--text-mid); background: var(--bg-hover); border: 1px solid var(--border); }
.status-badge.sb-emg   { color: var(--text-muted); background: transparent; border: 1px dashed var(--border); }

/* ⚠⚠ CARD #849 — `raceFlags()`'s four badges (ABANDONED/TRIAL/JUMP-OUT/NOMS/RES) are now real
 * `<md-assist-chip disabled>`, same technique `.bbx-q`/`.bbx-flow` already use (card #800):
 * `disabled` states a fact rather than unavailability, so every `disabled-*-opacity` token is
 * cancelled back to 1 and the READABLE colour comes from the `disabled-*-color`/`disabled-outline
 * -color` tokens, since `disabled` is what routes the component's internal template to them.
 *
 * ⚠ SCOPED TO THE `md-assist-chip` TAG, NOT JUST THE CLASS — `sb-abn`/`sb-trial` are also plain
 * `<span>` class names elsewhere (raceMatrix()'s `.rmc-flag`, renderMeet/renderTrack's inline
 * badges, predictionPanel()'s CHALLENGER label) that this card does not touch; the vendor sheet's
 * own plain `color`/`background` rules above keep styling those exactly as before — a custom
 * element's shadow DOM does not inherit non-inherited properties like `background`/`padding`
 * from a host-element rule anyway, so leaving those rules in place is inert for THIS selector,
 * not a conflict.
 *
 * ⚠ SIZE: MWC's own default chip height is 32px — visibly bulkier than `.status-badge`'s original
 * compact footprint (padding: 0.08rem 0.35rem, font-size: --fs-2xs) it sits beside inline in a
 * hero's badge row. `--md-assist-chip-container-height`/`-label-text-size` bring it back down to
 * match its siblings (`.hero-state`, `.intl-badge`) rather than towering over them.
 *
 * Colour mapping, preserved from the pre-#849 plain-span rules: sb-abn's `--danger` IS
 * `--md-error`'s own seed colour (style.css's own token-derivation comment says so directly) —
 * error role. sb-result's `--green` is LIGHT-THEME INDIGO, not a real green (vendor/styles.css's
 * own comment: "success (WON/PLACED) = indigo, never green") — mapped to `--md-primary`, not
 * `--md-success` (a genuine new role reserved for Best Bets only, per docs/design-system.md).
 * sb-trial has no dedicated M3 role in this palette; `--warn` is referenced directly, unchanged.
 * sb-noms stays neutral, mapped to the on-surface-variant/outline-variant pairing. */
md-assist-chip.status-badge {
  --md-assist-chip-container-height: 22px;
  --md-assist-chip-label-text-size: var(--fs-2xs);
  --md-assist-chip-label-text-line-height: 1.2;
  --md-assist-chip-leading-space: 8px;
  --md-assist-chip-trailing-space: 8px;
  --md-assist-chip-container-shape: var(--md-shape-xs);
  --md-assist-chip-disabled-label-text-opacity: 1;
  --md-assist-chip-elevated-disabled-container-opacity: 1;
  --md-assist-chip-disabled-outline-opacity: 1;
}
md-assist-chip.status-badge.sb-abn {
  --md-assist-chip-elevated-disabled-container-color: var(--md-error-container);
  --md-assist-chip-disabled-label-text-color: var(--md-on-error-container);
}
md-assist-chip.status-badge.sb-trial {
  --md-assist-chip-disabled-label-text-color: var(--warn, #b45309);
  --md-assist-chip-disabled-outline-color: var(--warn, #b45309);
}
md-assist-chip.status-badge.sb-noms {
  --md-assist-chip-disabled-label-text-color: var(--md-on-surface-variant);
  --md-assist-chip-disabled-outline-color: var(--md-outline-variant);
}
md-assist-chip.status-badge.sb-result {
  --md-assist-chip-elevated-disabled-container-color: var(--md-primary-container);
  --md-assist-chip-disabled-label-text-color: var(--md-on-primary-container);
}
/* ⚠ CARD #851 — sb-nontab is new to the md-assist-chip.status-badge set (raceMatrix()'s venue
 * cell is the only caller); same --warn colour family as sb-trial, matching the pre-#851 plain
 * -span rule's own choice (style.css's .status-badge.sb-nontab, ~line 465). */
md-assist-chip.status-badge.sb-nontab {
  --md-assist-chip-disabled-label-text-color: var(--warn, #b45309);
  --md-assist-chip-disabled-outline-color: var(--warn, #b45309);
}

/* ⚠⚠ CARD #851 — RACE MATRIX TOKEN PASS, SCOPED TO EXACT MATCHES ONLY. `.race-matrix`'s own
 * structural CSS (`vendor/styles.css:1596-1727`) is hash-pinned (CMP-06) — cannot be edited
 * directly, only overridden from here, cascading after the vendor <link> (same technique #789's
 * touch-target override already uses on a different vendor control).
 *
 * Two literal values already exactly equal a real --md-* token, so they move onto it:
 * `border-radius: 8px` = `--md-shape-sm` exactly; `gap: 4px` = `--md-space-1` exactly. Every
 * OTHER dimension in the grid (cell/venue/colhead padding, the 64px column width) is a bespoke
 * value tuned for this specific fixed-width layout, not a generic spacing choice — forcing those
 * onto the nearest 4px-grid step would be a real, uncontrolled layout shift in a grid where every
 * pixel decides whether "01:20 pm" wraps. Left as literals, matching the "no MWC component fits a
 * dense, bespoke grid" finding this card's own badge-scope comments already document — the same
 * reasoning applies to the grid's fine-tuned spacing, not just to its lack of a chip/list swap.
 * Colours (`--bg-card`, `--text`, `--border`) are untouched: they're this app's own base theme
 * variables, already shared with every other surface, not a second palette to retone. */
.race-matrix { border-radius: var(--md-shape-sm); gap: var(--md-space-1); }

/* ⚠⚠ CARD #859 — RACE HERO + MEET NAV, TOKEN PASS, SAME DISCIPLINE AS #851 ABOVE, EVEN NARROWER
 * IN PRACTICE. `.race-hero`/`.hero-*`/`.race-info-grid`/`.ri-*` and `.meet-nav-row`/`.meet-nav`/
 * `.meet-nav-pill`/`.quaddie-btn`/`.mnp-*` (`vendor/styles.css:3480-3670`, hash-pinned, CMP-06)
 * were both read in full against the `--md-shape-*`/`--md-space-*` scale — only a value that is a
 * WHOLE, EXACT match gets moved, exactly #851's own rule ("forcing those onto the nearest 4px-grid
 * step would be a real, uncontrolled layout shift"), extended here to also mean: a multi-value
 * shorthand only qualifies when EVERY part matches (`.meet-nav`'s mobile `padding: 0.4rem 0.5rem`
 * and `.race-hero`'s own `padding: 1rem 0.75rem 0.4rem` each have a mix of matching and
 * non-matching parts and are left as literals rather than half-converted).
 *
 * ⚠ NEITHER COMPONENT'S OUTER SHAPE IS TOUCHED. `.race-hero`'s own `border-radius: var(--radius)`
 * already reads a variable (10px, not a literal, and not equal to any `--md-shape-*` step) and
 * `.meet-nav-pill`'s `border-radius: 6px` / `.quaddie-btn`'s `6px` are both one step off the
 * nearest token (4 or 8) — real values, not roundable without changing what renders. Neither
 * component gets wrapped in a real MWC component either: `raceHero()`/`meetStrip()` both called
 * byte-pinned `vendor/ui.js` functions (`UI.hero()`/`UI.meetNavPill()`, guarded by CMP-05 and by
 * `meetStrip()`'s own `data-at`-injection assertion) for their actual markup — #1126 deleted
 * `vendor/ui.js` and both of these callers along with it — and `.race-hero`
 * carries its own scroll-linked sticky/condensed behaviour (`#raceHero { position: sticky … }`,
 * JS-toggled `.condensed` class) that no other Phase B/D page had — the operator's own call, given
 * that tradeoff, was token-retone only for this first pass, not a structural rewrap.
 *
 * ⓘ `.hero-status`/`.hero-type`/`.hero-chip`/`.hero-meta` are real rules in the vendor sheet but
 * render NOTHING on this page today — `raceHero()`'s own header comments confirm it deliberately
 * never passes `status`, `type` or `watch` to `UI.hero()` — so retoning them would change zero
 * visible pixels; skipped for that reason, not overlooked. */
.race-info-grid { gap: var(--md-space-2) var(--md-space-6); }
.meet-nav-row { gap: var(--md-space-2); }
.quaddie-btn { margin-right: var(--md-space-3); }
.meet-nav { padding: var(--md-space-2) var(--md-space-3); gap: var(--md-space-1); }

/* ── Layout the new views need and the old pages had elsewhere ───────────────────────────────── */

/* The race page's meta line. The legacy race header is built by `race.js` into a shell this client
 * does not have, so this is the one piece of that page's chrome written rather than inherited. */
.race-meta-line { color: var(--text-mid); font-size: var(--fs-sm); margin: -.35rem 0 1rem; }
.race-meta-line a { color: var(--accent); }

.runner-list { display: flex; flex-direction: column; gap: .5rem; }

/* ⚠ A missing silk is a HOLE THE SIZE OF A SILK, not a collapsed cell. The runner number column is
 * fixed-width in the vendor sheet; letting the image be absent reflows every row that lacks one and
 * makes the field look broken rather than incomplete. */
.silk-img.silk-none { display: inline-block; width: 32px; height: 32px; border: 1px dashed var(--border); border-radius: 3px; }

.pricenote { color: var(--text-muted); font-size: var(--fs-xs); margin-top: 1rem; }

/* The tier control's label. `.sh-user` and `.tn-link` are the vendor sheet's; only this is new. */
.tn-tierpick { color: var(--text-muted); font-size: var(--fs-xs); display: inline-flex; gap: .35rem; align-items: center; }

/* ⚠ Absence and failure are different, and must not share a style. `.empty-state-sm` is the vendor
 * sheet's and is used for "we hold nothing here"; this is for "we could not reach the service",
 * which is our problem and not an empty result. */
.err {
  color: var(--danger, #b91c1c);
  background: color-mix(in srgb, var(--danger, #b91c1c) 7%, transparent);
  border: 1px solid color-mix(in srgb, var(--danger, #b91c1c) 30%, var(--border));
  border-radius: var(--radius, 6px); padding: .7rem .8rem;
}
/* A gap in the PRODUCT, stated on the page — distinct again from both of the above. */
.gap { color: var(--text-muted); background: var(--bg-card); border: 1px dashed var(--border); border-radius: var(--radius, 6px); padding: .55rem .7rem; font-size: var(--fs-xs); margin-top: 1rem; }

/* ── The chrome the vendor sheet has no class for (#97) ─────────────────────────────────────────
 *
 * ⓘ #90 overrode `--header-h` to one row because there was no second row. There is now — the
 * next-to-jump strip — so the override is DELETED rather than re-tuned, exactly as its own comment
 * asked. The vendor's 89px is correct again.
 *
 * ⚠ `.tn-soon` IS NOT `.tn-locked`, AND MERGING THEM WOULD BE A REAL DEFECT.
 *
 * The vendor sheet's `.tn-locked` means "your tier does not include this": greyed, padlocked,
 * linking to pricing. This means "we have not built this". A customer cannot fix the second by
 * paying, and a padlock tells them they can — the absent/denied confusion this whole project is
 * named after, arriving in the navigation where it costs a refund conversation rather than a
 * rendering nit. So: no padlock, no grey-out, and the milestone is shown as text.
 */
.tn-soon { display: flex; align-items: baseline; gap: .4rem; flex-wrap: wrap; }
.tn-soon .tn-soon-at {
  font-family: var(--mono); font-size: var(--fs-2xs); color: var(--text-muted);
  border: 1px dashed var(--border); border-radius: 3px; padding: 0 .3rem; white-space: nowrap;
}
.bn-tab.tn-soon .tn-soon-at { display: none; }   /* no room on a phone tab; the panel still says it */

/*
 * ⚠⚠ #725 — THE FOLLOW BUTTON'S OWN CLASS, NOT THE VENDOR'S. render.js's `followControl()` used to
 * emit `.follow-control` deliberately, to get this exact styling for free from vendor/styles.css
 * without a second declaration — but `vendor/ui.js` bound its OWN delegated click listener to that
 * same class at module load (a dead v2 handler posting through localStorage bearer auth), and
 * because that script loaded before toggle-collection.js in every page, the vendor's handler always
 * fired FIRST on every click — before toggle-collection.js's real one ever ran. Every follow click
 * quietly did two things: the real toggle AND a doomed request to a route that answers 404 with
 * credentials that do not exist here. Copied here rather than shared, so this button could stop
 * matching that selector at all — the fix had to be "don't collide", not "run second and stop the
 * other one", because the vendor's listener ran first no matter what order anything else ran in.
 * ⚠ #1126 deleted both `render.js` and `vendor/ui.js` outright, so this class collision is now
 * moot — kept as history for whoever next reads `.vo-follow` and wonders why it exists apart.
 */
.vo-follow { display: inline-flex; align-items: center; gap: 4px; background: transparent; border: 1px solid var(--border);
  color: var(--text-muted); border-radius: 6px; padding: 3px 8px; font-size: var(--fs-xs); cursor: pointer; margin-left: 6px; }
.vo-follow:hover { border-color: var(--accent); color: var(--text); }
.vo-follow .bs-star { fill: currentColor; }
.vo-follow.fc-on { color: #fbbf24; border-color: #fbbf24; }

/* The hamburger, which the vendor sheet styles only as `.tn-toggle` inside the drawer it hides. */
/* ⓘ `.sh-menu-btn` and its svg rule are GONE with the button they painted. The drawer is pinned
   open at >=1100px and opened from the bottom bar below that, so the header toggle was a third
   affordance for a two-affordance control. Leaving the rules behind would be two more of the
   573 classes PROVENANCE.md already records as styling nothing. */

/* The header search box (card #43) — the search box nav.js's own comment above named as missing.
 * `.sh-search` reserves the slot in shell.js; src/search.js mounts the input and dropdown into it.
 *
 * ⚠⚠ #712 — SIZING MATCHES `vendor/styles.css`'s `.sh-search` ON PURPOSE, NOT COINCIDENCE. That
 * sheet already carries v2's own header-search layout (`flex: 1 1 auto; max-width: 440px`,
 * comment: "Search box in the header top row") — it never took effect because this rule, loading
 * after it, set a competing `flex: 0 1 240px` that always wins the cascade. Restated here instead
 * of deleted, so this file stays the one place the NEW input's own layout (`.sb-input`/
 * `.sb-results`, not the vendored `.sb-field`) is declared. */
.sh-search { position: relative; display: flex; align-items: center; min-width: 0; flex: 1 1 auto; max-width: 440px; margin: 0 .5rem; }
.sb-input { width: 100%; height: 34px; padding: .35rem .6rem; border-radius: 6px;
  /* ⚠ `.sh-top` is a black bar (vendor/styles.css). `border: 1px solid var(--border)` and
     `background: var(--surface)` are this file's usual card-surface treatment, meant for a light
     background — invisible-bordered and wrong-toned against black. Matches the overlay tint
     `vendor/styles.css`'s own `.sh-search .sb-field` already uses for this exact bar. */
  border: 1px solid rgba(var(--overlay-rgb), .14); background: rgba(var(--overlay-rgb), .06);
  color: var(--text); font-size: var(--fs-xs); }
.sb-results { position: absolute; top: calc(100% + .25rem); left: 0; right: 0; z-index: 40;
  /* ⚠⚠ #712 — WAS `var(--surface)` WITH NO FALLBACK. `--surface` is never defined anywhere as a
     real custom property (every other use in vendor/styles.css supplies one, e.g.
     `var(--surface, var(--bg))`) — an undefined custom property with no fallback makes the
     declaration invalid at compute time, so `background` resolved to its initial value,
     transparent, and the dropdown rendered see-through over whatever was behind it. */
  background: var(--surface, var(--bg-card)); border: 1px solid var(--border); border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0,0,0,.15); max-height: 60vh; overflow-y: auto; }
.sb-item { display: flex; align-items: baseline; gap: .5rem; padding: .45rem .7rem; text-decoration: none;
  color: var(--text); border-bottom: 1px solid var(--border); }
.sb-item:last-child { border-bottom: none; }
.sb-item:hover, .sb-item.sb-active { background: rgba(var(--overlay-rgb), .06); }
.sb-kind { font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); flex: 0 0 auto; }
.sb-label { flex: 1 1 auto; font-size: var(--fs-xs); }
.sb-sub { font-size: var(--fs-2xs); color: var(--text-muted); flex: 0 0 auto; }

/* The horse page's two comment sources (card #43) — a plain wrapping block below each po-row,
 * never nested inside it: the vendored `.po-row` is `display:flex` with no wrap (styles.css), so a
 * block child there would squeeze onto the flex line instead of dropping to its own. */
.horse-run-wrap { display: flex; flex-direction: column; }
.po-comment { font-size: var(--fs-2xs); color: var(--text-muted); padding: 0 .5rem .35rem; }
/* ⚠ THE PREFIX NAMES THE KIND OF COMMENT, NOT WHERE IT CAME FROM (#1099). It read "TAB preview: "
 * on every past run of every horse page — a data-source disclosure, which releases.js's own policy
 * already forbids in customer copy. The field behind it is `previewComment`, a pre-race preview, so
 * the word that survives is the one describing what the reader is actually looking at.
 *
 * ⚠⚠ THE TWO CLASS NAMES ARE CROSSED, SO DO NOT RELABEL BY CLASS NAME. Verified, before #1126
 * deleted render.js, at its then line 3467-8:
 *   .po-comment-preview  holds  r.runnerComment   (the pre-race runner comment)
 *   .po-comment-tab      holds  r.previewComment  (the race preview)  <- THIS RULE
 * Anyone reading the selectors and trusting them will prefix the wrong line. Only the second is
 * prefixed at all, which is why the label has to name the preview and not the comment.
 *
 * ⓘ The CLASS keeps its name. It is a stylesheet hook, not something a customer reads, and this card
 *   was scoped to displayed text — but renaming it to `.po-comment-race-preview` would have needed
 *   render.js changed in the same commit, and would have removed the trap above rather than
 *   documented it. Moot now that #1126 deleted render.js, but left as the historical reason this
 *   class was never renamed to match. */
.po-comment-tab::before { content: "Race preview: "; font-style: italic; }

/* The second header row. `.jh-races` and `.jh-race` are the vendor's; the row that holds them was
 * built by nav.js alongside filters and a search box this client has no services for. */
.sh-jump { display: flex; align-items: center; gap: .6rem; height: var(--header-top-h); padding: 0 .9rem; min-width: 0; }
.jh-label { font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted); white-space: nowrap; }
.jh-r-track { font-size: var(--fs-xs); }
.jh-r-time { font-family: var(--mono); font-size: var(--fs-2xs); color: var(--accent); }
.jh-r-time.jh-off { color: var(--text-muted); }

/* An account menu entry that is a statement, not a control — see the missing Logout button. */
.sh-acct-note { color: var(--text-muted); font-size: var(--fs-xs); font-style: italic; cursor: default; }

/* ⚠ THE STRIP SCROLLS; IT DOES NOT DIVIDE.
 *
 * The vendor's `.jh-race` is `flex: 1 1 0`, so ten chips split the row and every track name
 * ellipsises to "M…", "H…", "W…" — a strip that renders and tells you nothing, which is this
 * project's whole complaint in miniature. `.jh-races` is already `overflow-x: auto`, so scrolling
 * is what it was built for; `flex: 1 1 0` prevents it.
 *
 * ⓘ The vendor sheet ALREADY has the right rule, at `max-width: 640px`: `flex: 0 0 auto`. The
 * desktop case assumed a shorter strip. Applying its own mobile answer at every width, rather than
 * inventing one.
 */
.jh-race { flex: 0 0 auto; }

/* ── A bottom-bar tab that is a BUTTON (card #189) ───────────────────────────────────────────────
 *
 * ⚠ THREE PROPERTIES, AND NOT ONE OF THEM IS A LOOK THIS FILE IS CHOOSING.
 *
 * `.bn-tab` is the vendor sheet's and was written for an `<a>`: it sets no background and no border,
 * because an anchor has neither. The tabs that navigate by script are `<button>`s now — a control
 * that never navigates must not be an anchor pointing at `#` — and a button arrives with the user
 * agent's own chrome. Measured: `background: rgb(239, 239, 239)` against `rgba(0,0,0,0)` on the two
 * anchors beside them, so two of four tabs rendered on a grey slab.
 *
 * ⓘ The sheet already does exactly this for the same reason — `button.watch-link { cursor: pointer;
 * font: inherit; }` — so this follows its precedent rather than inventing one.
 *
 * ⚠ It deliberately sets NOTHING ELSE. Font comes from the sheet's own `button, input, select…`
 * rule at line 40; colour, size, weight, spacing and the `:active` press state all stay `.bn-tab`'s.
 * Adding a colour here would be this file quietly owning the bar's appearance.
 */
button.bn-tab { background: none; border: 0; cursor: pointer; }

/* ── The admin feature matrix (#225) ────────────────────────────────────────────────────────────
 *
 * ⚠⚠ `.sr-only` IS FIRST BECAUSE WITHOUT IT THE TABLE IS WRONG, AND THE UNIT TEST STILL PASSES.
 * ADM-11 asserts every cell carries a screen-reader label saying which of the three states it is —
 * "·" cannot distinguish "not included" from "nobody decided". That test reads the MARKUP, and the
 * markup was right; the class it named was defined in no stylesheet in this repo, so every label
 * rendered as visible text and the table read "✓included · not included" in a column of glyphs.
 * A test that asserts a class name passes whether or not the class does anything — the same shape
 * as SEO-06 asserting an og:image that pointed at a file nobody had uploaded. ADM-12 now asserts
 * the rule EXISTS, which is the half the markup test cannot see.
 *
 * ⓘ The vendor sheet hides one label this way inline (`.bk-badge .bk-label`, line 5216) but names
 * no reusable class, so this is an addition rather than a duplicate — the overlay's own rule.
 */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ⚠ The table scrolls INSIDE its own box. Five tier columns plus a name and a state overflow a
 * phone, and a page that scrolls sideways as a whole loses the row heading you were reading. */
.admin-tablewrap { overflow-x: auto; margin: .75rem 0; }
.admin-matrix { border-collapse: collapse; width: 100%; font-size: var(--fs-sm); }
.admin-matrix th, .admin-matrix td {
  padding: .4rem .55rem; border-bottom: 1px solid var(--border); text-align: left;
}
.admin-matrix thead th { color: var(--text-dim); font-weight: 600; font-size: var(--fs-2xs);
  text-transform: uppercase; letter-spacing: .04em; white-space: nowrap; }
.admin-matrix tr.admin-cat th {
  background: var(--bg-hover); color: var(--text-mid); font-size: var(--fs-2xs);
  text-transform: uppercase; letter-spacing: .05em; padding-top: .5rem;
}
.admin-key { font-family: var(--mono); font-size: var(--fs-2xs); color: var(--text-dim); }
.admin-label { color: var(--text); }
.admin-tier-col, .admin-cell { text-align: center; }
.admin-cell { font-family: var(--mono); color: var(--text-dim); }
/* ⚠ The tick is not colour ALONE — it is a different glyph as well, so the three states survive a
 * monochrome screen and anyone who cannot rely on hue. Same rule as `.denied`/`.unknown` above. */
.admin-cell-on { color: var(--brand-vo-green); font-weight: 700; }

/* ⚠ FOUR STATES, FOUR APPEARANCES, AND THE POINT OF THE WHOLE SCREEN IS THAT TWO OF THEM ARE NOT
 * THE SAME. `untriaged` and `none` both hold zero tiers — one because nobody looked, one because
 * somebody decided. Rendering them alike turns untouched work into a wall of deliberate zeroes. */
.admin-state { font-size: var(--fs-2xs); font-weight: 700; white-space: nowrap;
  border-radius: 3px; padding: .08rem .34rem; border: 1px solid transparent; }
.admin-state-on { color: var(--brand-vo-green); }
.admin-state-untriaged {
  color: var(--accent-light, var(--accent)); cursor: help;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border-color: color-mix(in srgb, var(--accent) 38%, var(--border));
}
.admin-state-none { color: var(--text-dim); font-style: italic; font-weight: 600; cursor: help; }
.admin-state-off { color: var(--exch-lay); cursor: help;
  background: color-mix(in srgb, var(--exch-lay) 12%, transparent);
  border-color: color-mix(in srgb, var(--exch-lay) 34%, var(--border)); }

.admin-notice, .admin-note {
  border: 1px solid var(--border); border-radius: 4px; padding: .6rem .7rem;
  background: var(--bg-card); color: var(--text-mid); font-size: var(--fs-sm); margin: .75rem 0;
}
.admin-notice { border-color: color-mix(in srgb, var(--accent) 38%, var(--border)); }
.admin-note pre { overflow-x: auto; margin: .5rem 0; }
.admin-note code { font-family: var(--mono); font-size: var(--fs-2xs); color: var(--text); }

/* ⚠ AND AN UNDECIDED CELL IS NOT A DECIDED NO. ADM-12's class sweep caught this one: `matrixHtml`
 * emitted `admin-cell-untriaged` and no rule defined it, so an untriaged row rendered as a row of
 * plain dots — indistinguishable from "somebody decided nobody gets this", which is the single
 * distinction the whole screen exists to draw. Faint and dashed, so a pending row reads as pending
 * across its whole width rather than only in the state column. */
.admin-cell-untriaged { color: var(--text-dim); opacity: .55; border-bottom-style: dashed; }

/* ── The matrix writes now (#261 + this card) ───────────────────────────────────────────────────
 *
 * ⚠⚠ THESE THREE RULES ARE LOAD-BEARING, AND ADM-12 IS WHY THEY EXIST AT ALL. It swept the classes
 * `admin.js` emits and found all three defined nowhere — the same catch it was written for when
 * `.admin-cell-untriaged` styled nothing. A checkbox with no rule still renders as a checkbox, so
 * the screen would have looked finished and behaved wrongly in the two states below.
 *
 * ⓘ The tick keeps its cell's colour rather than the browser's, so the three states ADM-11 names
 * stay distinguishable to a sighted operator once a control sits in each of them. */
.admin-tick { display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
  padding: .2rem; margin: -.2rem; }
.admin-tick input { accent-color: var(--brand-vo-green); cursor: pointer; margin: 0; }
.admin-tick input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ⚠ A KILLED FEATURE'S CELLS MUST LOOK REFUSED, NOT MERELY BE REFUSED. 044: `enabled = false` is
 * off for every human including admins, so those boxes carry `disabled` — but `disabled` alone is a
 * faint grey most operators read as ordinary. An operator clicking a box that silently does nothing
 * concludes the SCREEN is broken and reaches for psql, which is the audit trail this card exists to
 * keep. `not-allowed` and the dimming say refused-on-purpose. */
.admin-cell-dead { opacity: .4; }
.admin-cell-dead .admin-tick, .admin-cell-dead .admin-tick input { cursor: not-allowed; }

/* ⚠ And the kill switch is not a nav pill by accident: it borrows `.race-nav-pill` so it cannot
 * drift from the product's own button, and adds only the state colour.
 *
 * ⚠⚠ KEYED ON `data-to`, WHICH IS WHAT THE MARKUP CARRIES — I first wrote these two rules against
 * `[aria-pressed]` and the button emits no such attribute, so both styled nothing and the page
 * looked exactly the same. ADM-12 could not see it: it sweeps CLASS names, and a class that exists
 * with a rule that never matches passes it. ADM-13 below now covers the attribute selectors.
 *
 * ⓘ `data-to` is the ACTION, not the state — `data-to="off"` is a live feature whose button says
 * "Turn off". So the danger colour goes on `off`, the destructive direction. */
.admin-switch[data-to="off"] { color: var(--exch-lay);
  border-color: color-mix(in srgb, var(--exch-lay) 45%, var(--border)); }
.admin-switch[data-to="on"] { color: var(--text-dim); }
.admin-switch[disabled] { opacity: .5; cursor: not-allowed; }
/* ── The matrix cell's corner flag (#290) ───────────────────────────────────────────────────────
 *
 * ⓘ ONE RULE, AND ONLY GEOMETRY. Colour, weight and shape come from the product's own
 * `.status-badge` + `.sb-trial` / `.sb-abn`, which every other surface already uses to say these
 * two things — this positions them in the cell and nothing more.
 *
 * ⚠ The vendor sheet has no corner flag for a trial or an abandoned race because v2's matrix did
 * not mark either per CELL: it is a horse-only product there, and abandonment was carried by
 * `.rm-abn`'s opacity alone. Vector's CLI-06 requires a trial to be visible AT BOTH LEVELS — "it is
 * not form and must never be presented as a result" — and CLI-02's rule is that opacity is not a
 * label. So the mark is an addition on purpose, not a gap in the carry-across.
 *
 * ⓘ Geometry copied from `.rmc-ai` (top-right, z-index 1) so it lands where the product already
 * puts a cell marker, and cannot collide with `.rmc-special`'s bottom-left.
 */
.rmc-flag {
  /* ⚠ BOTTOM-LEFT since #291, not top-right. `.rmc-ai` owns the top-right corner and now carries the
     prediction marker; two absolutely-positioned marks in one 64px cell must not share a corner.
     ⓘ The same separation v2 drew between `.rmc-ai` and `.rmc-special`. */
  position: absolute; bottom: 2px; left: 2px; z-index: 1;
  font-size: 8px; font-weight: 800; letter-spacing: 0.04em; line-height: 1;
  padding: 2px 3px; border-radius: 3px;
}

/* ── THE RACEDAY STRIP (#77) ───────────────────────────────────────────────────────────────────
   Built on the vendor segmented control (`.segmented` / `.seg-btn` / `.seg-on`) rather than a new
   component, so day selection looks like every other tab row in the product and inherits the
   horizontal-scroll behaviour a row of unknown length needs on a phone. Only the INTERNALS of a
   day button are new — the control itself is not. */
.day-strip { margin: 0.5rem 0 1rem; }

/* Each day is three stacked facts: weekday, date, and how much racing is on. The count is what
   makes the strip worth having — a 21-meeting Saturday and a 6-meeting Monday are different
   propositions and the reader should see which before spending a click. */
.day-strip .ds-day { flex-direction: column; gap: 1px; line-height: 1.15; padding: 0.35rem 0.75rem; }
.day-strip .ds-dow { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.75; }
.day-strip .ds-date { font-size: var(--fs-sm); font-weight: 700; white-space: nowrap; }

/* The step arrows are navigation, not a date — kept narrow so they read as chrome beside the days. */
.day-strip .ds-step { flex: 0 0 auto; min-width: 2rem; padding: 0.35rem 0.5rem; font-size: 1.1rem; font-weight: 700; }

/* ⚠ PRESENT AND INERT AT THE EDGE OF THE DATA, never removed. A control that disappears when you
   reach the first raceday we hold reads as a bug; one that is visibly disabled says "this is the
   end of what exists", which is the honest answer and the one the payload actually gives (`null`). */
.day-strip .ds-end { opacity: 0.3; cursor: not-allowed; pointer-events: none; }

/* ⚠⚠ THE SELECTED DAY. Until #326 nothing styled it: the markup said `seg-on` and this control's
   state class is `.active`, so the current day looked exactly like every other day and the only
   clue was the heading above the strip. The vendor `.seg-btn.active` rule (a 20% tint and an inset
   ring) applies now, and this strengthens it — on a phone a tint alone is easy to miss while
   scrolling a row of near-identical cells, which is what the operator reported.
   ⓘ `--accent-btn` / `--on-accent` are the same pair the OTHER segmented control uses for its
   selection, so "selected" looks like one thing across both. */
.day-strip .seg-btn.active {
  background: var(--accent-btn);
  color: var(--on-accent);
  box-shadow: inset 0 0 0 1.5px var(--accent-btn);
}
/* ⚠ The sub-labels are dimmed by default (0.75 and 0.7) so they sit behind the date. On a solid
   accent field that reads as washed-out rather than secondary, so the selected cell restores them
   and inherits its colour rather than keeping the muted one. */
/* ⓘ `.ds-count` was in this selector until #350 removed the count from the cell's face. A rule for
   a class nothing emits is the same class/stylesheet disagreement #343 was raised for, pointing the
   other way — so it goes when its markup does. */
.day-strip .seg-btn.active .ds-dow { opacity: 0.92; color: inherit; }

/* ── WHAT THE BOARD IS NOT SHOWING (#77 follow-on) ─────────────────────────────────────────────
   Quiet by default — it is a footnote to the matrix, not a second heading. It has to be READ to be
   trusted, though: a board that hides four meetings without saying so is indistinguishable from a
   day we only hold six for, which is the failure this line exists to prevent. */
.rm-hidden {
  margin: 0.6rem 0 0; font-size: var(--fs-sm); color: var(--text-muted);
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
}

/* A real <button>, styled as a link — it changes what you are looking at, it does not navigate. */
.rm-hidden-toggle {
  border: 1px solid var(--border); background: var(--bg-card); color: var(--text);
  font: inherit; font-size: var(--fs-sm); font-weight: 600;
  padding: 0.25rem 0.7rem; border-radius: 999px; cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.rm-hidden-toggle:hover { background: var(--bg-hover); }

/* ── NON-TAB AND MEETING GRADE (#068) ──────────────────────────────────────────────────────────
   `sb-nontab` joins the vendor `.status-badge` family (sb-trial, sb-abn) rather than inventing a
   parallel one — a reader already knows what a chip in that slot means. Amber, not red: a non-TAB
   meeting is real racing with a real result, not a fault. */
.status-badge.sb-nontab {
  background: var(--warn-bg, rgba(200, 130, 20, 0.14));
  color: var(--warn-text, #b06a10);
  border: 1px solid var(--warn-border, rgba(200, 130, 20, 0.35));
}

/* The grade sits beside the state tag and reads quieter than it — a venue's state is part of its
   identity, its grade is context. Uppercased here rather than at ingest so the stored value stays
   the plain word the API documents. */
.rm-venue-grade {
  text-transform: uppercase; letter-spacing: 0.04em;
  opacity: 0.6; font-size: 0.6rem;
}

/* ── EACH-WAY: THE MODEL LOOKED AND FOUND NOTHING (#304) ───────────────────────────────────────
   Rendered only when `eachWayAbstained === true`. A null (we never asked) stays silent, because a
   reader must be able to tell a finding from a gap. Quieter than a pick — it is a reason not to
   bet, not a selection. */
.tp-card-ew.tp-ew-none { opacity: 0.72; }
.tp-ew-abstain { font-size: var(--fs-sm); color: var(--text-muted); padding: 0.15rem 0 0.1rem; }

/* ── "NOT LOADED YET" — A WAIT, NOT A GAP (#310) ───────────────────────────────────────────────
   The fourth state beside value / not known / upgrade. It must not read like `unknown`: those two
   being one careless palette change from identical is the exact failure `cell()`'s header warns
   about, so this carries different TEXT and a different weight, not just a different grey. Lighter
   than `unknown` because it is the most temporary thing on the page — it resolves within the hour
   once capture resumes. */
.pending {
  color: var(--text-muted); opacity: 0.55;
  font-style: normal; font-size: 0.92em;
  border-bottom: 1px dotted currentColor;
}

/* ── THE VO RANK BADGE (#340) ──────────────────────────────────────────────────────────────────
   Joins the vendor `.status-badge` family (sb-trial, sb-abn, sb-emg) rather than starting a new
   one, so it inherits the shape every other chip on the horse line already has. Tinted with the
   accent because it is OUR number — the composite rating rank — where the others report a state
   the provider gave us. */
.status-badge.sb-vo {
  background: color-mix(in srgb, var(--accent) 14%, var(--bg-card));
  color: var(--accent-light, var(--accent));
  border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  font-variant-numeric: tabular-nums;
}

/* ⚠ PROVISIONAL READS AS PROVISIONAL (#445). Before a race is predicted the badge carries the VO
   Speed rank as a stand-in; a dashed edge and the muted ink say it is not the model's order yet.
   V2 carries a `voRankProvisional` flag for the same reason — an unmarked stand-in is worse than
   none, because the reader cannot tell an opinion from a placeholder for one. */
.status-badge.sb-vo.sb-vo-prov {
  color: var(--text-mid);
  background: transparent;
  border-style: dashed;
  border-color: color-mix(in srgb, var(--accent) 30%, transparent);
}

/* ── THE BOARD FILTERS, ABOVE THE BOARD (#350) ─────────────────────────────────────────────────
   Vendor switch markup (.toggle-label / .toggle-track / .toggle-thumb), so these read as the same
   control the rest of the product uses. Only the row's own layout is new. */
.rm-hidden { margin: 0 0 0.75rem; display: flex; flex-direction: column; gap: 0.35rem; }
.rm-filters { display: flex; flex-wrap: wrap; gap: 0.75rem 1.25rem; align-items: center; }

/* The sentence is a footnote to the switches, not a heading for them. */
.rm-hidden-said { margin: 0; font-size: var(--fs-sm); color: var(--text-muted); }

/* ── THE RECORD (#351) ──────────────────────────────────────────────────────────────────────────
 * ⚠ BUILT FROM THE VENDOR SHEET'S OWN CUSTOM PROPERTIES, like everything else here, so a palette
 * change moves this card too instead of leaving it behind on last season's colours.
 *
 * ⚠⚠ THE THREE STATES ARE VISUALLY DIFFERENT, NOT JUST TEXTUALLY. A percentage, a bare count and
 * "no record yet" must not read as three versions of one number — that is the whole point of the
 * card. The bar exists only in the state that has a rate to draw.  */
.form-record .section-header { display: flex; align-items: baseline; gap: .6rem; }
.fr-what { font-size: var(--fs-xs); color: var(--text-muted); }
.fr-rows { display: flex; flex-direction: column; gap: .1rem; }

.fr-row {
  display: grid;
  grid-template-columns: 4.6rem minmax(0, 1fr);
  grid-template-areas: "flow main" ".    sub";
  align-items: center;
  gap: .15rem .8rem;
  padding: .55rem 0;
  border-bottom: 1px solid var(--border);
}
.fr-row:last-child { border-bottom: 0; }

.fr-flow {
  grid-area: flow;
  font-family: var(--mono); font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: .06em; color: var(--text-mid);
}
.fr-main { grid-area: main; display: flex; align-items: center; gap: .7rem; min-width: 0; }
.fr-sub  { grid-area: sub;  font-size: var(--fs-2xs); color: var(--text-muted); }

/* the publishable state: a number and a bar */
.fr-pct {
  font-size: var(--fs-sm); font-weight: 700; color: var(--text);
  font-variant-numeric: tabular-nums; min-width: 3.4rem;
}
.fr-bar {
  flex: 1 1 auto; min-width: 0; height: .34rem; border-radius: 999px;
  background: color-mix(in srgb, var(--text-muted) 22%, transparent);
  overflow: hidden;
}
.fr-fill { display: block; height: 100%; border-radius: 999px; background: var(--accent); }

/* ⚠ HELD BACK: a count, no bar. Nothing here may look like a rate. */
.fr-count {
  font-size: var(--fs-sm); font-weight: 700; color: var(--text);
  font-variant-numeric: tabular-nums;
}
.fr-of { font-weight: 400; font-size: var(--fs-2xs); color: var(--text-muted); }

/* ⚠ NEVER RAN: stated in words, and deliberately quiet — it is not a score of zero. */
.fr-none { font-size: var(--fs-xs); color: var(--text-muted); font-style: italic; }

.fr-foot {
  margin: .7rem 0 0; font-size: var(--fs-2xs); color: var(--text-muted);
  border-top: 1px solid var(--border); padding-top: .55rem;
}

/* ⚠⚠ CARD #812 — the day recap teaser. Compact by design (operator's own choice): an opening
 * line, one chart-bars.js row (not a fifth hand-rolled chart), and a link — never the full recap
 * inline. `.day-recap-teaser` itself carries no layout of its own beyond spacing; `.cb-*` (the
 * bar) is chart-bars.js's own shared markup, styled once already for the /tips/day/ page (see
 * tips-archive-page.js's shell()) — restated here since app.html loads `/style.css`, not that
 * page's inline <style> block, and the two never shared one stylesheet before this card. */
.day-recap-teaser .drt-opening { margin: 0 0 .6rem; color: var(--text); font-size: var(--fs-sm); }
.day-recap-teaser .drt-chart { margin: 0 0 .6rem; }
.day-recap-teaser .drt-link { font-weight: 600; text-decoration: none; }
.day-recap-teaser .drt-link:hover { text-decoration: underline; }
.day-recap-teaser .cb-row { display: flex; align-items: center; gap: .6rem; }
.day-recap-teaser .cb-label { flex: 0 0 6rem; font-size: var(--fs-sm); color: var(--text-muted); }
.day-recap-teaser .cb-bar { position: relative; flex: 1 1 auto; height: .55rem;
  background: var(--bg-input, var(--border)); border-radius: 3px; overflow: hidden; }
.day-recap-teaser .cb-fill { position: absolute; inset: 0 auto 0 0; border-radius: 3px; }
.day-recap-teaser .cb-fill-hi { background: var(--accent); }
.day-recap-teaser .cb-value { flex: 0 0 auto; font-size: var(--fs-sm); font-variant-numeric: tabular-nums;
  color: var(--text-muted); }

/* ── #857 — the race page's prediction tabs were real <md-tabs>/<md-primary-tab> (see the
 * now-deleted render.js's `flowTabs()` — #1126), the same swap card #799 already made for Best
 * Bets. Card #341's own
 * "restore the filled pill" override that lived here is gone with the `.bb-flowtoggle`/`.bb-flowbtn`
 * markup it targeted — nothing on the site emits that markup any more. */

/* ── STAKING PLAN CARDS, MOVED HERE FROM 30+ INLINE `style="…"` ATTRIBUTES (#726) ────────────────
 * ⚠⚠ NOT A VISUAL CHANGE ON ITS OWN — `staking-plans.js` built every plan card (outlay header, the
 * stake/block/note row shapes) as one giant literal string of one-off inline styles, up to six per
 * flow. No test asserts on the inline style text itself (checked before touching this), so the
 * values move here unchanged except for what "reduce the size" actually asked for: tighter padding
 * and margin between stacked cards, so six of them read as a compact list rather than six full-size
 * standalone cards.
 * ⓘ `.sp-plan` overrides `.stat-card`'s vendor base (`padding: 0.75rem; text-align: center`) —
 * the same class already sat on the element, unused for anything but the vendor's background/border. */
.sp-plan { text-align: left; padding: 0.55rem 0.75rem; background: var(--bg-card);
  border: 1px solid var(--border); border-left: 3px solid var(--accent);
  border-radius: 8px; margin-bottom: 0.4rem; }
.sp-head { display: flex; justify-content: space-between; gap: 0.6rem; align-items: baseline; }
/* ⚠ 0.85rem, MATCHING `--fs-body` — not the token itself: SP-27 reads a literal `Nrem` out of this
   rule's own text to prove nothing outsizes the outlay, and a `var(--fs-body)` here would be
   invisible to that regex (it falls back to an assumed 1rem, the exact silent-inherit trap SP-27's
   own comment warns about). Same computed size as the rest of the page's body text either way. */
.sp-head-kind { color: var(--text); font-size: 0.85rem; }
/* ⚠⚠ #726 — 1.05rem, NOT SHRUNK WITH THE REST OF THE CARD. SP-27 exists because a prior mutation
   demoted this to 0.7rem grey text and the suite stayed green: "the OUTLAY is the most prominent
   figure on the card" is a real, tested product decision, not a default this file may erode while
   compacting everything around it. Every OTHER `.sp-*` size below this is smaller than the browser
   default (1rem) on purpose; this one stays above it so the property holds without depending on
   what an unstyled sibling happens to inherit. */
.sp-head-total { color: var(--text); font-weight: 700; font-size: 1.05rem;
  font-variant-numeric: tabular-nums; }
/* ⓘ "Outlay" said once, under the figure, rather than "at $100.00" beside it (#726's own carry
   forward of the reasoning already in staking-plans.js: the word is the label, the number the
   answer). */
.sp-sub { display: flex; justify-content: space-between; gap: 0.6rem; margin-bottom: 0.35rem; }
.sp-bettype { color: var(--text-muted); font-size: 0.76rem; }
.sp-tag { color: var(--text-muted); font-size: 0.68rem; text-transform: uppercase;
  letter-spacing: 0.04em; flex: 0 0 auto; }
/* THREE ROW KINDS, because one layout cannot carry three jobs — staking-plans.js's own comment on
   `line()` states which is which: `sp-row` is a runner + the money against it, `sp-block` is a
   label above a full-width value (selections, which are prose), `sp-note` is a plain sentence. */
.sp-row { display: flex; justify-content: space-between; gap: 1rem; align-items: baseline;
  padding: 0.1rem 0; }
.sp-row-label { color: var(--text); min-width: 0; font-size: 0.85rem; }
.sp-row-value { color: var(--text); font-weight: 600; flex: 0 0 auto; font-size: 0.85rem;
  font-variant-numeric: tabular-nums; }
.sp-block { margin-top: 0.3rem; }
.sp-block-label { color: var(--text-muted); font-size: 0.72rem; text-transform: uppercase;
  letter-spacing: 0.03em; }
.sp-block-value { color: var(--text); line-height: 1.3; font-size: 0.85rem; }
.sp-note { color: var(--text-muted); font-size: 0.78rem; margin-top: 0.3rem; line-height: 1.3; }

/* #31 THE MODEL'S OWN PROSE, beneath the attribution bullets rather than instead of them.
   `about` answers "which factors moved this runner"; the note is Prime's written read of it. Both
   are shown because neither answers the other's question — and a paid model's reasoning is the
   product. The page rendered only the composed sentence ("Prime rates it on its form rating.")
   while a form summary, a reason and a case for each bet type sat on the wire unread.
   ⚠ Palette tokens only. --text-mid is the colour the bullets already use, so the note reads as
   part of the same block rather than as an aside. */
.tp-note {
  margin: .4rem 0 0; color: var(--text-mid); font-size: var(--fs-sm); line-height: 1.55;
}

/* #31 PRIME'S RACE-LEVEL VERDICT — what the MODEL named, beside what our blend ranked.
   ⚠⚠ TWO DIFFERENT CLAIMS, NEVER MERGED. Measured at Wangaratta R2: the model called "You Said
   What" its top pick and Kokonda the each-way value, while the blend ranked Kokonda first and
   abstained on each-way entirely. Presenting either as the other would invent a recommendation
   nobody made, so the heading names whose read it is. */
.tp-verdict {
  margin-top: .75rem; padding: .6rem .7rem;
  border: 1px solid var(--border); border-radius: 8px; background: var(--bg);
}
.tp-verdict-head {
  font-size: var(--fs-xs); font-weight: 700; color: var(--text-mid); margin-bottom: .3rem;
}
.tp-verdict-summary {
  margin: 0; color: var(--text); font-size: var(--fs-sm); line-height: 1.55;
}
.tp-verdict-picks {
  margin: .35rem 0 0; color: var(--text-mid); font-size: var(--fs-sm);
}

/* ── THE TIPS ARCHIVE (#341) ────────────────────────────────────────────────────────────────────
 * ⓘ Built from the vendor sheet's own custom properties, like everything else in this overlay, so
 * a palette change moves this page too. */
.arc-controls {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.6rem;
  padding: 0 0 0.9rem;
}
.arc-field { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.arc-lab {
  font-size: var(--fs-2xs); color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.arc-input {
  font: inherit; font-size: var(--fs-sm);
  padding: 0.34rem 0.5rem; border: 1px solid var(--border);
  border-radius: var(--radius); background: var(--bg-card); color: var(--text);
  /* ⚠ A SELECT OF 300 TRACK NAMES MUST NOT SET THE PAGE WIDTH. `max-width` with `min-width: 0` is
   * what lets it shrink inside the flex row — the same failure the meet-nav had, one control over. */
  min-width: 0; max-width: 13rem;
}
.arc-check { display: flex; align-items: center; gap: 0.4rem; font-size: var(--fs-sm); }
.arc-clear { font-size: var(--fs-sm); padding: 0.34rem 0.7rem; }
/* ⚠ #740 — a COUNT, not a rate; see renderArchive()'s own docblock for why strike-rate/ROI is not
 * here. Sits under the controls row, not inside it — it summarises the list below, it is not
 * itself a filter. */
.arc-model-stats {
  display: flex; flex-wrap: wrap; gap: 0.4rem 1rem;
  font-size: var(--fs-sm); color: var(--text-muted);
  padding: 0 0 0.9rem;
}
.arc-model-stat strong { color: var(--text); }

.arc-section { border-top: 1px solid var(--border); }
.arc-head {
  display: flex; align-items: baseline; gap: 0.5rem; width: 100%;
  background: none; border: 0; cursor: pointer; font: inherit; text-align: left;
  padding: 0.75rem 0.1rem; color: var(--text);
}
.arc-head:hover { background: var(--bg-hover); }
.arc-caret { color: var(--text-muted); font-size: var(--fs-xs); }
.arc-head-label { font-weight: 700; }
/* ⚠ THE COUNT IS PART OF THE HEADING, not a footnote — it is what a reader chooses on before
 * paying for the scroll, and "40 meetings · 2 tipped" answers a different question from "40". */
.arc-head-count { margin-left: auto; font-size: var(--fs-2xs); color: var(--text-muted); }

.arc-body { padding: 0 0 0.6rem; }
.arc-day { padding: 0.35rem 0 0.5rem; }
.arc-day-head {
  font-size: var(--fs-2xs); color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.05em; padding: 0.2rem 0.1rem;
}
.arc-meets { display: flex; flex-direction: column; }
.arc-meet {
  display: flex; align-items: baseline; gap: 0.6rem;
  padding: 0.5rem 0.1rem; border-top: 1px solid var(--border);
  text-decoration: none; color: inherit;
}
.arc-meet:hover { background: var(--bg-hover); }
.arc-track { font-weight: 600; }
.arc-meta { font-size: var(--fs-2xs); color: var(--text-muted); }
/* ⚠ TIPPED AND UNTIPPED ARE BOTH STATED, and they look different. 2,722 of 2,752 meetings here
 * carry no tip — leaving those blank would let a reader assume the absence was an oversight, and
 * a chip that only ever appears when we DID tip makes the archive look like a tip sheet. */
.arc-tips {
  margin-left: auto; font-size: var(--fs-2xs); font-weight: 700;
  color: var(--accent-light, var(--accent));
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-radius: 999px; padding: 0.05rem 0.45rem; white-space: nowrap;
}
.arc-untipped {
  margin-left: auto; font-size: var(--fs-2xs); color: var(--text-muted);
  font-style: italic; white-space: nowrap;
}

/* ── #363 Hard Qualifiers — the day's central view ─────────────────────────────────────────────
   ⚠ OUR SHEET, NOT THE VENDORED ONE. `src/vendor/styles.css` is hash-pinned by CMP-06; anything
   this client adds lives here. Tokens are the vendored sheet's own, so the page follows the
   product's theme rather than declaring a second palette. */
.hq-funnel {
  display: flex; flex-wrap: wrap; gap: 1px;
  background: var(--border); border: 1px solid var(--border); border-radius: var(--radius-sm);
  overflow: hidden; margin: 0 0 .75rem;
}
.hq-f { flex: 1 1 120px; background: var(--bg-card); padding: .7rem .85rem; }
.hq-f-n { display: block; font-size: 1.35rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
.hq-f-l { display: block; font-size: var(--fs-xs); color: var(--text-muted); margin-top: .15rem; }
/* ⚠ The unassessed count is MARKED, because it is the one number a reader is most likely to
   misread as a rejection. Colour is support, never the message — the label says it in words. */
.hq-f-warn .hq-f-n { color: var(--warn, #8A6A1F); }
.hq-f-tot { background: var(--bg-subtle, var(--bg)); }
.hq-note { font-size: var(--fs-sm); color: var(--text-muted); margin: 0 0 1.25rem; max-width: 70ch; }

.hq-list { display: grid; gap: .6rem; }
.hq-card {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-card); padding: .85rem 1rem;
}
.hq-head { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; flex-wrap: wrap; }
.hq-horse { font-weight: 700; font-size: 1.06rem; text-decoration: none; color: var(--text); }
.hq-horse:hover, .hq-horse:focus-visible { text-decoration: underline; }
.hq-score { text-align: right; white-space: nowrap; }
.hq-score b { font-size: 1.25rem; font-variant-numeric: tabular-nums; }
.hq-of { display: block; font-size: var(--fs-xs); color: var(--text-muted); }
.hq-where { font-size: var(--fs-sm); color: var(--text-muted); margin-top: .2rem; }
.hq-time { font-size: var(--fs-sm); margin-top: .45rem; font-variant-numeric: tabular-nums; }
.hq-why { margin: .5rem 0 0; padding-left: 1.1rem; font-size: var(--fs-sm); color: var(--text-muted); }
/* Best Bets board (#548, card contrast/rename #788). Cards mirror .hq-card's rhythm; the
 * qualifier chip colours through --green so both themes resolve, same token as every other
 * agreement state.
 *
 * ⚠⚠ RENAMED FROM `.bb-*` TO `.bbx-*` — card #788. `vendor/styles.css` (hash-pinned, v2's own
 * home-page tile component) already defines `.bb-card`/`.bb-horse`/`.bb-filters`/`.bb-f`/
 * `.bb-clear`/`.bb-flow` for an unrelated surface, and this page's markup silently inherited
 * whatever this file didn't re-declare — concretely `.bb-card:hover` changing the whole card's
 * border/background (vendor/styles.css:2107, correct on a clickable tile, false here on an
 * `<article>` that isn't one) and `.bb-flow`'s `display: flex` (vendor/styles.css:1868, already
 * worked around once for the flow-toggle row, #726) breaking this card's flow chip onto its own
 * line. Card #725 named the fix for the same shape of collision on `.vo-follow`/`.follow-control`:
 * "the fix a colliding selector needs is not matching it, not outrunning it." Renamed rather than
 * re-declared every inherited property.
 */
.bb-funnel { color: var(--text-dim); font-size: var(--fs-sm); margin: .3rem 0 .8rem; }
.bbx-controls { margin: 0 0 1rem; display: flex; flex-direction: column; gap: .6rem; }
.bbx-filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--md-space-2, 0.75rem);
  align-items: center;
}
.bbx-f { display: flex; flex-direction: column; gap: .15rem; font-size: var(--fs-xs);
  color: var(--text-dim); }
.bbx-f.bbx-class-group {
  display: inline-flex;
  flex-direction: row;
  align-items: center;
  gap: var(--md-space-1, 0.5rem);
}
.bbx-class-group .bbx-f-label {
  font-size: var(--md-label-large, 14px);
  font-weight: 600;
  color: var(--md-on-surface-variant);
  margin-right: var(--md-space-1, 0.25rem);
}
.bbx-class-group .bbx-f {
  display: inline-flex;
  margin: 0;
}
.bbx-class-group .bbx-f .bbx-chk {
  height: 48px;
  min-height: 48px;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  padding: 0 var(--md-space-2, 0.75rem);
  border-radius: var(--md-shape-sm, 8px);
  border: 1px solid var(--md-outline-variant);
  background: var(--md-surface);
  font-size: var(--md-label-large, 14px);
  font-weight: 500;
  color: var(--md-on-surface);
  gap: var(--md-space-1, 0.4rem);
  cursor: pointer;
  user-select: none;
  transition: border-color var(--md-motion-short) var(--md-motion-standard),
              background var(--md-motion-short) var(--md-motion-standard);
}
.bbx-class-group .bbx-f .bbx-chk:hover {
  background: var(--md-surface-container-high);
  border-color: var(--md-outline);
}
/* ⚠⚠ CARD #803 — the old `.bbx-f select` rule styled a native `<select>` that no longer exists
 * anywhere under `.bbx-f`; every single-value field is now a real `<md-outlined-select>`, which
 * styles its own text-field chrome from its own tokens (`--md-sys-color-*`, bridged in #798) —
 * there is nothing left here for this file to hand-style. `--md-outlined-select-text-field-
 * container-shape` is the one thing worth overriding: MWC's own default is a small rounded
 * rectangle, not the `--md-shape-sm` this page's other compact controls (chips) already use. */
md-outlined-select {
  --md-outlined-select-text-field-container-shape: var(--md-shape-sm);
  --md-outlined-field-top-space: 10.5px;
  --md-outlined-field-bottom-space: 10.5px;
  height: 48px;
  min-width: 9rem;
}
/* ⚠ FROM THE #945 RESCUE, AND THE ONLY PART OF ITS CONFLICTING BLOCK KEPT. An empty
 * placeholder <md-select-option> otherwise renders as a blank row in the opened menu.
 * The rest of that block was a restyle of `.bbx-clear` from a base that predates #953 —
 * whose own comment cites `.bbx-clear` being PLAIN as the precedent it follows, so taking
 * the rescue's richer version would have quietly falsified a merged card's reasoning. */
md-select-option:empty {
  display: none;
}
.bbx-clear { font-size: var(--fs-sm); align-self: center; }
/* ⚠ #953 — PLAIN, NOT AN MWC BUTTON. This bar is otherwise real MWC controls (#803), but a second
 * component here would need its own `--md-*` token bridge for one small text-style action — the
 * existing `.bbx-clear` link already sets the precedent for a plain, small, self-styled control
 * living beside the MWC ones, so this matches it rather than reaching for `<md-text-button>`. */
.bbx-save { font-size: var(--fs-sm); align-self: center; background: none; border: none;
  color: var(--accent); cursor: pointer; padding: 0; text-decoration: underline; }
.bbx-save:disabled { color: var(--text-dim); cursor: default; text-decoration: none; }
.bbx-save-msg { font-size: var(--fs-xs); color: var(--text-dim); align-self: center; }
.bbx-list { display: grid; gap: var(--md-space-3); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }

/* ⚠⚠ CARD #793 (M3 pilot 2/4) — M3's "ELEVATED CARD", NOT "OUTLINED". Was a flat, bordered
 * rectangle (`border: 1px solid var(--border); border-radius: var(--radius)` — var(--radius)
 * resolves to 0 in the live theme, the sitewide flat-identity override #788's own research
 * found). M3 draws a distinction between an outlined card (border, no shadow) and an elevated
 * one (shadow, no border); this reverses the flat identity per the operator's own direct call
 * (see the M3 pilot plan) by moving to the elevated treatment — background lifts off the page on
 * a tonal shadow instead of being fenced in by a hairline, and lifts further on hover as an
 * affordance (the card itself carries no link, so this is a resting-vs-attended state, not a
 * false clickability claim — see card #788's own removal of `.bbx-card:hover`'s border/background
 * change for exactly that false-affordance concern, which this motion-only hover does not
 * reintroduce).
 *
 * ⚠⚠ CARD #802 (MWC 5) — this is now a REAL `<md-elevated-card>` (the last accepted `labs/`
 * component), not a hand-styled `<article>`. `container-elevation` is Google's own numeric
 * elevation-LEVEL token (0-5, consumed internally by the card's own `<md-elevation>` part), not
 * a box-shadow literal — swapping `box-shadow: var(--md-elevation-N)` for
 * `--md-elevated-card-container-elevation: N` is not a rename, it's handing the resting/hover
 * shadow math over to Google's real elevation component instead of this project's own
 * hand-approximated box-shadow pair (still used elsewhere, e.g. `--md-elevation-2` on
 * `.bbx-card:hover`'s OWN pre-#802 rule above, now dead for this selector but left defined —
 * other, not-yet-converted surfaces may still reference it). `container-shadow-color` is left at
 * its component default (a neutral black, per M3's own spec — the tonal *tint* comes from
 * `container-color`, not the shadow) rather than bridged to a token that doesn't exist yet.
 * `::part(elevation)` is how the transition on hover reaches inside the card's shadow DOM — the
 * `<md-elevation>` element carries `part="elevation"` in MWC's own template specifically so a
 * light-DOM consumer can reach it (`labs/card/internal/card.js`), and per-property `transition`
 * (not `all`, which the assist-chip block below did) since only the level/opacity are meant to
 * animate here. Semantics: the `<article>` element the card used to *be* is now nested INSIDE
 * `<md-elevated-card>` (see `betCard()`) so screen readers still get the region it always had —
 * MWC's card has no ARIA role of its own to inherit.
 */
.bbx-card {
  --md-elevated-card-container-color: var(--md-surface-container);
  --md-elevated-card-container-shape: var(--md-shape-lg);
  --md-elevated-card-container-elevation: 1;
  padding: var(--md-space-4);
}
.bbx-card::part(elevation) {
  transition: box-shadow var(--md-motion-short) var(--md-motion-standard),
    opacity var(--md-motion-short) var(--md-motion-standard);
}
.bbx-card:hover { --md-elevated-card-container-elevation: 2; }

.bbx-head { display: flex; align-items: baseline; gap: var(--md-space-2); flex-wrap: wrap; }

/* ⚠ TIERED, NOT FLAT — card #788. Before this, only the horse name and win probability were
 * elevated; track/race#/distance/going/class/jump-time/price were one flat tier of small grey
 * text, with price — arguably the second most important number on a betting card — styled
 * identically to going. Primary: horse, price, jump time. Secondary: win probability, qualifier
 * chip. Tertiary: track/race#/distance/going/class (bbx-where).
 *
 * ⓘ #793 moves only the SPACING values below onto the --md-space-* scale — the tier structure,
 * colours, and weights above are untouched here (colour/shape for .bbx-q/.bbx-flow is card 3). */
.bbx-horse { font-weight: 700; text-decoration: none; color: var(--text); }
.bbx-horse:hover, .bbx-horse:focus-visible { text-decoration: underline; }
.bbx-pct { font-family: var(--mono); font-weight: 700; margin-left: auto; }
/* ⚠⚠ CARD #800 (MWC 4) — REAL `<md-assist-chip>`, `disabled` (SEE render.js's OWN COMMENT, before
 * #1126 deleted the file: neither
 * chip has ever had a click action, and `disabled` is the honest way to say that with a real MWC
 * component rather than a `<span>` styled to merely look like one). MWC's own `disabled` styling
 * dims everything by default ("this is unavailable") — wrong for a chip that is simply
 * informational, never available — so every `-disabled-*-opacity` token is overridden back to
 * full opacity here; the READABLE colour comes from the `-disabled-*-color` tokens instead of the
 * normal (enabled) ones, since `disabled` is what routes the component's internal template to
 * them. `.bbx-q` (qualifier chip): `--md-success`/`--md-success-container`, the genuine role #792
 * introduced for this, `elevated` for a filled look. `.bbx-flow` (CORE/PRIME/BOOST): neutral
 * `--md-outline-variant`/`--md-on-surface-variant`, outlined (not elevated). Both `--md-shape-sm`. */
.bbx-q {
  --md-assist-chip-container-shape: var(--md-shape-sm);
  --md-assist-chip-elevated-container-color: var(--md-success-container);
  --md-assist-chip-elevated-disabled-container-color: var(--md-success-container);
  --md-assist-chip-elevated-disabled-container-opacity: 1;
  --md-assist-chip-disabled-label-text-color: var(--md-on-success-container);
  --md-assist-chip-disabled-label-text-opacity: 1;
}
.bbx-where { font-size: var(--fs-sm); color: var(--bb-meta); margin-top: var(--md-space-1); }
.bbx-price { font-family: var(--mono); font-weight: 700; font-size: var(--fs-md, 1rem);
  color: var(--text); margin-top: var(--md-space-2); }
.bbx-time { font-size: var(--fs-sm); color: var(--text-mid, var(--text)); margin-top: var(--md-space-1);
  font-variant-numeric: tabular-nums; }
.bbx-flow {
  --md-assist-chip-container-shape: var(--md-shape-sm);
  --md-assist-chip-disabled-label-text-color: var(--md-on-surface-variant);
  --md-assist-chip-disabled-label-text-opacity: 1;
  --md-assist-chip-disabled-outline-color: var(--md-outline-variant);
  --md-assist-chip-disabled-outline-opacity: 1;
}

/* ⚠⚠ #789 — TOUCH TARGET OVERLAY, NOT A VENDOR EDIT. `.bs-seg-btn` (vendor/styles.css:6753, the
 * Win/E-W/Place buttons) renders at `min-height: 26px` — the vendor sheet's own comment claims this
 * is "a touch larger than a static badge, for tapability", but `.btn` (vendor/styles.css:1829),
 * every other button in this product's family, already carries a 44px floor. `vendor/styles.css` is
 * hash-pinned (CMP-06) — cards #712 and #729 both raised a vendor control's sizing this same way,
 * from `style.css`, cascading after the vendor `<link>`. */
.bs-seg-btn { min-height: 44px; }

/* ⚠ GROUP SEPARATION — the Follow star and the Win/E-W/Place row used to sit in one
 * undifferentiated run (`.runner-above-actions`, vendor/styles.css:5100, a plain flex row with no
 * gap) — nothing told a reader these are two different controls. `.runner-above-actions` currently
 * has exactly one caller (Best Bets' own `betCard()`); scoped to `.betslip-add-wrap` specifically
 * rather than the container's own `gap`, so a future second child of `.runner-above-actions` is not
 * silently assumed to want the same spacing. */
.runner-above-actions .betslip-add-wrap { margin-left: .6rem; }

/* ⚠⚠ FOCUS VISIBILITY — card #789. No `:focus-visible` rule in either stylesheet matched any of
 * these four controls before this. The ring is lifted verbatim from the one designed
 * `:focus-visible` treatment already in the vendor sheet for this exact kind of interactive chip —
 * `.bb-flowtoggle[role="tablist"] .bb-flowbtn:focus-visible` (vendor/styles.css:1899) — rather than
 * inventing a second ring style for the same product. */
.bbx-horse:focus-visible, .vo-follow:focus-visible, .bs-seg-btn:focus-visible,
.bbx-chk input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px;
}

/* ══════════════════════════════════════════════════════════════════════════════════════════════
 * ── CARD #794 (M3 pilot 3/4) — BUTTONS, BETSLIP SEGMENT, AND THE FLOW TOGGLE ────────────────────
 *
 * ⚠⚠ SCOPED TO BEST BETS' OWN DOM, DELIBERATELY — NOT A SITEWIDE RESTYLE. `.vo-follow` and
 * `.bs-seg-btn` were SHARED classes: `followControl()`/`betslipControl()` (src/render.js, deleted
 * by #1126) were also called by the general `runnerCard()` for every OTHER page's runner rows, not
 * just Best Bets'
 * `betCard()`. The M3 pilot plan is explicit — "Best Bets first... other pages are separate,
 * later cards" — so every rule below is scoped through an ancestor that only exists in Best
 * Bets' own markup (`.runner-above-actions` and `.bbx-controls`, both confirmed to have exactly
 * one caller — `betCard()`/`filterForm()` — via grep), rather than the bare shared class name.
 * A future card can lift this scoping once the operator actually approves a sitewide rollout;
 * doing it silently here would redesign every other page's Follow/betslip buttons uninvited.
 *
 * ⚠⚠ SPECIFICITY — a second time, harder than the flow toggle below. The light theme's own
 * flat-identity override zeroes radius via `:root[data-theme="light"] [class*="btn"]`
 * (vendor/styles.css:404-428) — `:root` is a PSEUDO-CLASS, not free, so that selector is
 * `(0,3,0)` (pseudo-class + attribute + attribute), not the `(0,2,0)` a quick read suggests. A
 * plain `.runner-above-actions .vo-follow` is only `(0,2,0)` and LOSES to it regardless of
 * source order — confirmed the hard way, live in a browser, not just by reading the two rules.
 * Every selector below repeats `:root[data-theme="light"]` to clear that bar.
 *
 * ⚠ `.bs-seg-btn` GETS SHAPE ONLY, NEVER COLOUR. Its selected states (`.sel-win`/`.sel-ew`/
 * `.sel-place`, vendor/styles.css:5085-5087) already use the product's own established, MEANINGFUL
 * bet-type colours (`--win`/`--ew`/`--green`) — not ad-hoc literals the M3 pass is meant to clean
 * up. Recolouring them into primary/secondary M3 roles would erase a real semantic distinction
 * (which bet type is selected) for a generic one; only the shape moves.
 */

/* ══════════════════════════════════════════════════════════════════════════════════════════════
 * ── CARD #799 (MWC 3) — REAL COMPONENTS REPLACE THE HAND-STYLED CSS ABOVE ───────────────────────
 *
 * Follow, the betslip Win/E-W/Place control, and the flow toggle are now real
 * `<md-filled-tonal-button>`/`<md-outlined-segmented-button-set>`/`<md-tabs>` elements (see
 * `followControl()`/`betslipControl()`/`filterForm()`, which lived in `src/render.js` until #1126
 * deleted it) — their shape, elevation
 * and base colour come from the component itself via the `--md-sys-color-*` bridge (#798), not
 * from hand-written `border-radius`/`color` rules the way cards #793/#794 needed for plain
 * `<button>`/`<div>` markup. What's left to style here is only what a real MWC component does
 * NOT decide for itself: Follow's on/off STATE (no built-in toggle variant exists — see below)
 * and the locked-flow link sitting beside `<md-tabs>` rather than inside it.
 */

/* Follow — ONE `<md-filled-tonal-button>` for both states (MWC has no built-in "toggle button
 * with a label" variant to swap between); the on/off distinction is carried entirely by
 * overriding the component's OWN per-instance theming custom properties — its real mechanism,
 * not a fake — driven by the same `.fc-on` class `blackbook.js`'s `paint()` already toggles.
 * Rest: a neutral tonal fill (barely there). Followed: the primary-container fill, same
 * pairing `.bbx-q`'s own success-container treatment (#794) already uses for "this is on." */
.vo-follow {
  --md-filled-tonal-button-container-color: var(--md-surface-container-high);
  --md-filled-tonal-button-label-text-color: var(--md-on-surface-variant);
  --md-filled-tonal-button-icon-color: var(--md-on-surface-variant);
  --md-filled-tonal-button-container-shape: var(--md-shape-full);
  /* ⚠⚠ ALSO SETS PLAIN `color`, NOT JUST THE MWC TOKENS — the ORIGINAL, still-live base rule at
   * the top of this file (`.vo-follow { … color: var(--text-muted); }`, kept for every OTHER
   * page's plain-`<button>` Follow control) matches this same class name, and a slotted
   * `<span class="fc-text">`/`<svg>` inherits `color` from ITS OWN light-DOM ancestor chain (the
   * host element), not from anything inside the component's shadow tree. Without repeating
   * `color` here, the slotted label/icon would inherit the OLD rule's `--text-muted`, fighting
   * the `--md-filled-tonal-button-label-text-color`/`-icon-color` tokens above for the exact same
   * visible pixels — confirmed live, not assumed (see the card's own commit notes). */
  color: var(--md-on-surface-variant);
}
.vo-follow.fc-on {
  --md-filled-tonal-button-container-color: var(--md-primary-container);
  --md-filled-tonal-button-label-text-color: var(--md-on-primary-container);
  --md-filled-tonal-button-icon-color: var(--md-on-primary-container);
  /* ⚠⚠ Same reasoning as above — the ORIGINAL base rule's followed-state gold-star literal
   * (kept for every other page's plain-button Follow control) would otherwise win the slotted
   * text/icon's inherited colour by source order, since both rules share the same specificity. */
  color: var(--md-on-primary-container);
}

/* ⚠⚠ CONSISTENT 48px TOUCH TARGETS — M3's OWN accessibility guidance (Material Design's touch
 * target spec calls for 48x48dp minimum), not the ad-hoc 44px `.bs-seg-btn { min-height: 44px; }`
 * card #789 used for the plain-`<button>` betslip control that card predates real MWC components
 * on this page — that rule is now dead for Best Bets (its selector no longer matches anything
 * here) and untouched for every OTHER page's plain betslip buttons.
 *
 * ⚠⚠ THE COMPONENT'S OWN `-container-height` TOKEN IS NOT ENOUGH, AND NEITHER IS `min-height` —
 * FOUND LIVE, NOT ASSUMED, in two rounds. Round 1 (token only): Follow rendered 53px (its own
 * touch-target padding stacks ON TOP of the token), the betslip segmented button rendered 40px
 * (it silently ignored the token for its rendered box height entirely), only the tab matched at
 * 48px — three different heights, not one. Round 2 (`min-height: 48px` added): the segmented
 * button and tab both correctly clamped to 48px, but Follow's own INTRINSIC size (53px, driven by
 * its icon+text content plus internal touch-target padding) already exceeds 48px, so a `min-`
 * height has no effect on it at all — still three different heights, just different ones. A plain
 * `height: 48px` (not `min-`) on all three light-DOM hosts is what actually forces one true,
 * identical box regardless of each component's own internal quirks — confirmed by re-measuring live after
 * adding it, not by reasoning about what "should" happen.
 *
 * Scoped by bare TAG NAME, safe by construction: every page that loads `mwc-bundle.js` wants
 * this same 48px floor on its own Follow/betslip/tab controls, so a shared, un-scoped rule is
 * correct rather than accidental.
 *
 * ⚠⚠ CARD #850 UPDATE — `md-filled-tonal-button.vo-follow`/`md-outlined-segmented-button` ARE NO
 * LONGER BEST-BETS-EXCLUSIVE, AND THAT IS BY DESIGN. The original comment here claimed they
 * matched no other page's markup because only Best Bets loaded `mwc-bundle.js` and passed
 * `{mwc: true}`. #850 is the race page's own `runnerCard()` doing exactly that for the first
 * time — these two rules now also apply there, which is the intended outcome (one consistent
 * Follow/betslip touch target sitewide, not a second copy of this sizing fix).
 *
 * ⚠⚠ CARD #857 UPDATE — `md-primary-tab` IS NO LONGER BEST-BETS-EXCLUSIVE EITHER. The race
 * page's own prediction-flow tabs (`flowTabs()`, in the now-deleted `render.js`) were real
 * `<md-primary-tab>` too, and correctly wanted the SAME 48px touch target Best Bets' tabs already
 * established — one
 * shared rule, not a duplicate. */
md-filled-tonal-button.vo-follow {
  --md-filled-tonal-button-container-height: 48px;
  height: 48px;
}
md-outlined-segmented-button {
  --md-outlined-segmented-button-container-height: 48px;
  height: 48px;
}
md-primary-tab {
  --md-primary-tab-container-height: 48px;
  height: 48px;
}

/* The locked-flow link (available-but-not-entitled, #790/#791's own concept) sits beside
 * `<md-tabs>` rather than inside it — see the comment on `filterForm()`'s own `lockedRow`. Given
 * the M3 shape/opacity language the rest of this pilot already established for a disabled/locked
 * affordance. */
.bbx-flow-locked-row { display: flex; gap: var(--md-space-2); margin-top: var(--md-space-1); }
.bb-flow-locked {
  display: inline-flex; align-items: center; padding: var(--md-space-1) var(--md-space-3);
  border-radius: var(--md-shape-full); border: 1px solid var(--md-outline-variant);
  color: var(--md-on-surface-variant); opacity: 0.65; text-decoration: none;
  font-size: var(--md-label-large);
}
.bb-flow-locked:hover { opacity: 0.85; }

/* ══════════════════════════════════════════════════════════════════════════════════════════════
 * ── CARD #795 (M3 pilot 4/4, FINAL) — FORM CONTROLS, THE CLASS ACCORDION, AND MOTION ────────────
 *
 * ⚠ CHECKBOXES ARE NATIVE, RECOLOURED ONLY — no custom M3 checkbox component. A fully custom
 * checkbox (its own box/check-mark markup, focus/indeterminate states) is a bigger, separate
 * undertaking this pilot does not need to solve to prove the token system — a real scope cut,
 * flagged here per the plan rather than silently dropped. `.bbx-chk` is already Best-Bets-only
 * (grep confirms no other page uses the class), so this needs no extra scoping.
 */
.bbx-chk input[type="checkbox"] { accent-color: var(--md-primary); }

/* ⚠⚠ SCOPED TO BEST BETS' OWN DOM, DELIBERATELY — SAME DISCIPLINE AS CARD #794. `.ui-accordion`
 * is SHARED vendor infrastructure (vendor/styles.css:4452-4461) — also used by staking plans and the
 * predicted-finishing-order panel (both `src/render.js`, deleted by #1126), AND `.recent-starts`
 * on the race page.
 * Scoped via `.bbx-class-group`/`.bbx-fdrop` — the Class filter's own wrapper, and (card #803)
 * the generic multi-select-filter wrapper `checks()` now emits for State/Track/Going — both
 * confirmed Best-Bets-only via grep, so this restyle touches only Best Bets' own accordions, not
 * the shared component everywhere else it appears.
 *
 * ⓘ NO SPECIFICITY FIGHT HERE, UNLIKE CARD #794's BUTTONS — `.ui-accordion` is not in the light
 * theme's `[class*="btn"]`/badge/chip/pill zeroing list (vendor/styles.css:404-428), so its own
 * inline radius (8px/10px) was never force-flattened; a plain scoped override already wins via
 * source order once its specificity is at or above the vendor rule's own (0,2,0)/(0,3,0), which
 * adding `.bbx-class-group`/`.bbx-fdrop` as an ancestor already clears. Verified live in a
 * browser regardless — card #794 found a real bug that a source-text-only check missed, so this
 * pilot no longer trusts specificity arithmetic without also checking `getComputedStyle` on the
 * rendered page.
 *
 * ⚠⚠ CARD #803 — `.bbx-class-group` was the ONLY scoping wrapper before this card; `.bbx-fdrop`
 * is added here as a second, equally-scoped wrapper rather than renaming the first (CMD-10, the
 * fence protecting this scoping discipline, already names `.bbx-class-group` explicitly — widened
 * to accept either, not replaced, so a stray future `.ui-accordion` restyle missing BOTH still
 * fails it).
 */
.bbx-fdrop {
  position: relative;
}
.bbx-class-group .ui-accordion, .bbx-fdrop .ui-accordion {
  position: relative;
}
.bbx-class-group .ui-accordion-sum, .bbx-fdrop .ui-accordion-sum {
  height: 48px;
  min-height: 48px;
  box-sizing: border-box;
  padding: 0 var(--md-space-2, 0.75rem);
  display: inline-flex;
  align-items: center;
  gap: var(--md-space-1, 0.35rem);
  border-radius: var(--md-shape-md);
  border: 1px solid var(--md-outline-variant);
  background: var(--md-surface);
  color: var(--md-on-surface);
  font-family: inherit;
  font-size: var(--md-label-large, 14px);
  font-weight: 500;
  cursor: pointer;
  user-select: none;
  transition: border-color var(--md-motion-short) var(--md-motion-standard),
              background var(--md-motion-short) var(--md-motion-standard);
}
/* Trailing caret affordance rotating smoothly with standard M3 motion */
.bbx-class-group .ui-accordion-sum::before, .bbx-fdrop .ui-accordion-sum::before {
  order: 2;
  margin-left: 0.25rem;
  content: "▾";
  font-size: 11px;
  color: var(--md-on-surface-variant);
  display: inline-block;
  transition: transform var(--md-motion-short) var(--md-motion-standard);
}
.bbx-class-group .ui-accordion[open] > .ui-accordion-sum::before,
.bbx-fdrop .ui-accordion[open] > .ui-accordion-sum::before {
  transform: rotate(180deg);
}
.bbx-class-group .ui-accordion[open], .bbx-fdrop .ui-accordion[open] {
  border-radius: var(--md-shape-md);
  border-color: var(--md-primary);
  background: transparent;
  border: none;
}
.bbx-class-group .ui-accordion[open] > .ui-accordion-sum, .bbx-fdrop .ui-accordion[open] > .ui-accordion-sum {
  border-color: var(--md-primary);
  background: var(--md-surface-container-high);
}
.bbx-class-group .ui-accordion[open] > .ui-accordion-sum::after, .bbx-fdrop .ui-accordion[open] > .ui-accordion-sum::after {
  display: none;
}
.bbx-class-group .ui-accordion-sum:hover, .bbx-fdrop .ui-accordion-sum:hover {
  border-color: var(--md-outline);
  background: var(--md-surface-container-high);
}
/* Floating M3 dropdown menu body */
.bbx-class-group .ui-accordion-body, .bbx-fdrop .ui-accordion-body {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 100;
  min-width: 180px;
  max-height: 280px;
  overflow-y: auto;
  background: var(--md-surface-container);
  border: 1px solid var(--md-outline-variant);
  border-radius: var(--md-shape-sm, 8px);
  box-shadow: var(--md-elevation-2);
  padding: var(--md-space-1, 0.35rem) var(--md-space-2, 0.5rem);
}
.bbx-class-group .ui-accordion-body .bbx-chk, .bbx-fdrop .ui-accordion-body .bbx-chk {
  display: flex;
  align-items: center;
  gap: var(--md-space-1, 0.4rem);
  padding: 0.35rem 0.5rem;
  border-radius: var(--md-shape-xs, 4px);
  font-size: var(--fs-sm, 13px);
  color: var(--md-on-surface);
  cursor: pointer;
  white-space: nowrap;
}
.bbx-class-group .ui-accordion-body .bbx-chk:hover, .bbx-fdrop .ui-accordion-body .bbx-chk:hover {
  background: var(--md-surface-container-high);
}

/* ⚠⚠ CARD #803 — the selected-count badge on a closed dropdown's summary; without it, a filter
 * with 3 states checked and one with 0 look IDENTICAL when collapsed, which is worse than the
 * always-expanded grid it replaces (that one at least showed the checked boxes). Elevated/tonal,
 * matching `.bbx-q`'s own success-role chip treatment elsewhere on this page, so a reader who
 * already knows what a "count is active" pill looks like recognises this one too. */
.bbx-fdrop-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.25em; height: 1.25em; padding: 0 .35em;
  border-radius: var(--md-shape-full);
  background: var(--md-primary); color: var(--md-on-primary);
  font-size: var(--fs-2xs, .7rem); font-weight: 700; font-variant-numeric: tabular-nums;
}

/* The model line under a qualifier (#560) — the lens half of the page. Chips per flow; the ✓
   agreement state colours through the token so both themes resolve it. */
.hq-model { margin: .45rem 0 0; display: flex; flex-wrap: wrap; gap: .35rem .5rem; font-size: var(--fs-sm); }
.hq-flow { background: var(--bg-hover, rgba(128,128,128,.12)); border: 1px solid var(--border);
  border-radius: 4px; padding: .05rem .45rem; color: var(--text-mid, var(--text)); white-space: nowrap; }
.hq-flow-agree { border-color: var(--green); color: var(--green); font-weight: 600; }
.hq-model-none { margin: .45rem 0 0; font-size: var(--fs-sm); color: var(--text-dim); font-style: italic; }
.hq-why li { margin: .1rem 0; }
/* ⓘ Dimmer than a miss, and labelled — a criterion nobody could judge must not read as one the
   horse failed. Same rule the score's denominator keeps. */
.hq-cannot { font-size: var(--fs-xs); color: var(--text-dim); margin: .5rem 0 0; }

/* ── #364 /tips — the public record. Server-rendered, so these must stand alone. ─────────────── */
.tips-hero { padding: 2.5rem 0 1.25rem; }
.tips-hero h1 { margin: 0 0 .5rem; font-size: clamp(1.9rem, 4vw, 2.6rem); letter-spacing: -.02em; }
.tips-lede { margin: 0; color: var(--text-muted); max-width: 60ch; font-size: 1.05rem; }
.tips-summary { display: flex; flex-wrap: wrap; gap: 1px; background: var(--border);
  border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; margin: 1.25rem 0 .75rem; }
.ts-fig { flex: 1 1 150px; background: var(--bg-card); padding: .9rem 1rem; }
.ts-n { display: block; font-size: 1.7rem; font-weight: 700; line-height: 1.05; font-variant-numeric: tabular-nums; }
.ts-l { display: block; font-size: var(--fs-xs); color: var(--text-muted); margin-top: .2rem; }
/* ⚠ The basis line is not fine print. A strike rate without the share it covers is the number a
   marketing page should never print on its own — see v2's rule, carried into public-tips.js. */
.tips-basis { font-size: var(--fs-sm); color: var(--text-muted); max-width: 78ch; margin: 0 0 2rem; }
.tips-card { margin: 0 0 1.75rem; }
.tips-date { font-size: 1.05rem; margin: 0 0 .5rem; display: flex; gap: .75rem; align-items: baseline; flex-wrap: wrap; }
.tips-count { font-size: var(--fs-sm); font-weight: 400; color: var(--text-muted); }
.tips-scroll { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-sm); }
.tips-table { border-collapse: collapse; width: 100%; font-size: var(--fs-sm); min-width: 480px; }
.tips-table th { text-align: left; padding: .6rem .8rem; border-bottom: 1px solid var(--border);
  font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); }
.tips-table td { padding: .55rem .8rem; border-bottom: 1px solid var(--border); }
.tips-table tr:last-child td { border-bottom: 0; }
.t-track { font-weight: 600; }
.t-race, .t-time { color: var(--text-muted); margin-left: .35rem; }
.t-horse { font-weight: 600; }
.t-price { font-variant-numeric: tabular-nums; }
/* ⓘ A tip we could not price is SHOWN and excluded from the return — never hidden, because a record
   that drops what it cannot score is a record that only drifts one way. */
.t-none { color: var(--text-dim); font-style: italic; }
.t-win { color: var(--win, #2E6F53); font-weight: 700; }
.tip-won td { background: color-mix(in srgb, var(--win, #2E6F53) 7%, transparent); }
.tips-empty { padding: 1.5rem; border: 1px solid var(--border); border-radius: var(--radius-sm); }
.tips-more { margin: 2rem 0 3rem; color: var(--text-muted); }

/* ── #380 Model panel — weights, discrimination and calibration on each model's admin page. ──
   ⚠ NOT ONE COLOUR LITERAL: every colour here is a palette token, including inside the SVG, so the
   charts follow the theme like the rest of the page. A chart is exactly where a hex sneaks in. */
.mp-panel { margin-top: 1.2rem; }
.mp-h { margin: 1.1rem 0 0.4rem; font-size: 1.02rem; }
.mp-block { margin-bottom: 0.5rem; }
.mp-table { width: 100%; border-collapse: collapse; font-size: 0.86rem; }
.mp-table th { text-align: left; font-weight: 600; color: var(--text-muted);
  border-bottom: 1px solid var(--border); padding: 0.3rem 0.5rem 0.3rem 0; white-space: nowrap; }
.mp-table td { padding: 0.35rem 0.5rem 0.35rem 0; border-bottom: 1px solid var(--border-light);
  vertical-align: middle; }
.mp-sig { min-width: 8rem; }
.mp-key { display: block; font-size: 0.72rem; color: var(--text-muted); }
.mp-dim { color: var(--text-muted); }
.mp-na { color: var(--text-muted); font-style: italic; font-size: 0.8rem; }

/* The bar: fill runs to the ratio, the line marks the declared weight (always at x = 1/3). */
.mp-bar-wrap { position: relative; width: 8rem; height: 0.62rem; background: var(--bg-input);
  border-radius: 3px; overflow: hidden; }
.mp-fill { position: absolute; inset: 0 auto 0 0; border-radius: 3px; }
.mp-fill-hi   { background: var(--chart-1); }
.mp-fill-mid  { background: var(--chart-3); }
.mp-fill-lo   { background: var(--chart-4); }
.mp-fill-zero { background: var(--danger); }
.mp-neutral { position: absolute; top: -2px; bottom: -2px; width: 2px; background: var(--text-mid); opacity: 0.85; }
.mp-over { position: absolute; right: 2px; top: -2px; color: var(--text); font-weight: 700; font-size: 0.7rem; }

.mp-chip { display: inline-block; padding: 0.08rem 0.42rem; border-radius: 999px;
  font-size: 0.72rem; font-weight: 600; border: 1px solid var(--border); color: var(--text-mid); }
.mp-boosted    { background: rgba(var(--green-rgb), 0.14); color: var(--green-ink); border-color: transparent; }
.mp-suppressed { background: var(--bg-hover); }
.mp-at-zero    { background: rgba(var(--danger-rgb), 0.14); color: var(--danger); border-color: transparent; }
.mp-weak       { background: rgba(var(--danger-rgb), 0.08); color: var(--danger); border-color: transparent; }
.mp-insufficient, .mp-unset { background: var(--bg-light); color: var(--text-muted); }

.mp-alert { margin: 0 0 0.5rem; font-size: 0.86rem; color: var(--danger); }
.mp-foot { margin: 0.4rem 0 0; font-size: 0.74rem; color: var(--text-muted); line-height: 1.45; }

.mp-chart { width: 100%; height: auto; overflow: visible; }
.mp-grid { stroke: var(--border-light); stroke-width: 1; }
.mp-diag { stroke: var(--border); stroke-width: 1; stroke-dasharray: 3 3; }
.mp-axis { fill: var(--text-muted); font-size: 9px; }
.mp-line1 { stroke: var(--chart-1); stroke-width: 2; }
.mp-line2 { stroke: var(--chart-2); stroke-width: 1.5; }
.mp-dot { fill: var(--chart-1); opacity: 0.75; }
.mp-legend { font-size: 0.76rem; color: var(--text-muted); display: flex; align-items: center;
  gap: 0.35rem; margin-bottom: 0.25rem; flex-wrap: wrap; }
.mp-k { display: inline-block; width: 0.8rem; height: 0.18rem; border-radius: 2px; }
.mp-k1 { background: var(--chart-1); }
.mp-k2 { background: var(--chart-2); margin-left: 0.7rem; }
.mp-cal-title { font-size: 0.86rem; font-weight: 600; margin-bottom: 0.2rem; }

/* ⚠ Two calibration charts side by side on a wide screen, stacked on a narrow one — and the table
   scrolls in its OWN container so the page body never scrolls sideways. */
.mp-cals { display: grid; grid-template-columns: 1fr; gap: 0.9rem; }
@media (min-width: 720px) { .mp-cals { grid-template-columns: 1fr 1fr; } }
@media (max-width: 719px) {
  .mp-block:has(.mp-table) { overflow-x: auto; }
  .mp-table { min-width: 34rem; }
}

/* ⚠⚠ THE RAIL HIDES ITSELF UNTIL SOMETHING FILLS IT (#back-and-rail).
   The divided layout is built on every non-admin page, but this product has no
   /api/prime-vectors collection yet — prime-vectors.html still says so — so the rail V2 fills
   with Prime Vectors has nothing honest to show. An empty aside would hold a column of screen
   and display nothing, which reads as a page that failed to load rather than a feature not yet
   built. `:empty` is exact here: the moment anything is appended to #railMount the rule stops
   applying and the column appears, with no code change and no flag to remember to flip. */
.app-rail:empty { display: none; }
/* ⚠⚠ #753 — WITHOUT THIS, `SHELL.setShellFull()`'s `.hidden = true` (#747) DOES NOTHING. The
   `hidden` attribute only hides an element via the browser's own UA-origin `[hidden] { display:
   none }` rule, and author CSS always beats UA CSS regardless of specificity — vendor/styles.css's
   own `.app-rail { display: block; ... }` (inside its `@media (min-width: 900px)` block, not
   editable here — CMP-06 checksums that file byte-for-byte) silently won every time, so the rail
   stayed on screen, hidden or not. `[hidden]` is one selector more specific than the bare class, so
   it wins outright once THIS file (loaded after vendor/styles.css) is in the cascade at all. */
.app-rail[hidden] { display: none; }
/* ── #387 Admin panel template — the shared blocks every admin surface is built from.
   ⚠ NOT ONE COLOUR LITERAL: palette tokens only, so a new surface inherits the theme for free. */
.ap-section { margin-top: 1.2rem; }
.ap-h { margin: 1.1rem 0 0.3rem; font-size: 1.02rem; }
.ap-note { margin: 0 0 0.5rem; font-size: 0.84rem; color: var(--text-muted); }
.ap-foot { margin: 0.4rem 0 0; font-size: 0.74rem; color: var(--text-muted); line-height: 1.45; }

/* ⚠ THE TABLE SCROLLS IN ITS OWN BOX so the page body never scrolls sideways on a narrow screen. */
.ap-scroll { overflow-x: auto; }
.ap-table { width: 100%; border-collapse: collapse; font-size: 0.86rem; }
.ap-table th { text-align: left; font-weight: 600; color: var(--text-muted);
  border-bottom: 1px solid var(--border); padding: 0.3rem 0.5rem 0.3rem 0; white-space: nowrap; }
.ap-table td { padding: 0.35rem 0.5rem 0.35rem 0; border-bottom: 1px solid var(--border-light);
  vertical-align: middle; }
.ap-num { text-align: right; }

/* ⓘ THE ADMIN PAGE WRAPPER. Sections already carry their own spacing; this only bounds the line
   length and gives the fixed bottom nav room beneath the last row — the clearance `admin-page`
   exists for. Under 1100px that bar is fixed to the foot of the viewport, and without the padding
   the last table row sits behind it. */
.ap-page { max-width: 72rem; margin: 0 auto; padding: 0.5rem 0 4.5rem; }
.ap-title { margin: 0 0 0.9rem; font-size: 1.35rem; }

/* ⚠ A BATCH ID IS FOR COPYING, so it wraps rather than truncating. An id shown with an ellipsis
   looks selectable and yields a value that is not the batch. */
.ap-id { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.74rem;
  color: var(--text-mid); word-break: break-all; }

/* ⚠⚠ EMPTY AND REFUSED LOOK DIFFERENT ON PURPOSE. "there is nothing here" is a fact about the
   world; "we cannot tell you" is a fact about us, and a reader who cannot tell them apart goes
   looking for the wrong fix. */
.ap-empty { margin: 0.3rem 0; font-size: 0.84rem; }
.ap-empty.ap-empty { color: var(--text-muted); }
.ap-empty.ap-refused { color: var(--danger); }

.ap-chip { display: inline-block; padding: 0.08rem 0.42rem; border-radius: 999px;
  font-size: 0.72rem; font-weight: 600; border: 1px solid var(--border); color: var(--text-mid); }
.ap-chip-good { background: rgba(var(--green-rgb), 0.14); color: var(--green-ink); border-color: transparent; }
.ap-chip-bad  { background: rgba(var(--danger-rgb), 0.14); color: var(--danger); border-color: transparent; }
.ap-chip-idle { background: var(--bg-light); color: var(--text-muted); }

/* ⚠ #441 — THE ADMIN PANEL'S FIRST BUTTON, and it belongs in the shared vocabulary rather than on
   one page. Every ap-* control here was read-only until a batch could be recovered from the LLM ops
   page; a page-local `.lo-button` would be the second button's problem, not the first's.
   ⓘ Sized to sit inside a table row without changing its height — it shares a cell with chips and
   figures, and a control that reflows the row it lives in makes the table harder to scan. */
.ap-btn { display: inline-block; padding: 0.14rem 0.6rem; border-radius: 5px;
  font-size: 0.74rem; font-weight: 600; line-height: 1.35; cursor: pointer;
  border: 1px solid var(--border); background: var(--bg-light); color: var(--text); }
.ap-btn:hover:not(:disabled) { border-color: var(--text-mid); }
/* ⚠ THE DISABLED STATE IS REAL AND VISIBLE. `bind()` disables the button for the duration of the
   request, and a control that looks identical while in flight invites the second press this
   endpoint is idempotent against — idempotent is the safety net, not the interaction design. */
.ap-btn:disabled { opacity: 0.55; cursor: default; }

.ap-bar { position: relative; width: 8rem; height: 0.62rem; background: var(--bg-input);
  border-radius: 3px; overflow: hidden; }
.ap-fill { position: absolute; inset: 0 auto 0 0; border-radius: 3px; }
.ap-fill-hi { background: var(--chart-1); } .ap-fill-mid { background: var(--chart-3); }
.ap-fill-lo { background: var(--chart-4); } .ap-fill-zero { background: var(--danger); }
.ap-neutral { position: absolute; top: -2px; bottom: -2px; width: 2px; background: var(--text-mid); opacity: 0.85; }
.ap-over { position: absolute; right: 2px; top: -2px; color: var(--text); font-weight: 700; font-size: 0.7rem; }

/* The source column: a column name is code, and reads as code. */
.mp-src code { font-size: 0.72rem; color: var(--text-mid); word-break: break-word; }
.mp-src { max-width: 16rem; }
.mp-cands { margin-top: 0.8rem; font-size: 0.86rem; }
.mp-cands summary { cursor: pointer; color: var(--text-mid); }


/* ── #415 Admin pages use the STANDARD shell — header, footer and bottom nav, like every page.
   ⚠ THE ONE THING THE OLD admin-host CHROME GOT RIGHT: the bottom nav is `position:fixed` at the
   foot of the viewport below 1100px, so without clearance the last control on an admin page sits
   underneath it. The old fix was to hide the nav; this reserves room for it instead. */
@media (max-width: 1099px) {
  :root.admin-page .app-shell { padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px)); }
}

/* ── THE ACCOUNT PAGE, COMPACTED — card #506 ─────────────────────────────────────────────────────
 *
 * ⚠ SCOPED TO [data-acct-panel], NOT TO THE COMPONENTS. `notif-channel` and `notif-row` are carried
 * v2 components used by the notification settings too; retuning them globally would change a surface
 * this card never looked at. The attribute is already on every panel (#503 put the ids beside it),
 * so the scope costs nothing and cannot leak.
 *
 * ⓘ Everything here is spacing. No colours, no borders, no fonts — the carried sheet owns how these
 * components LOOK, and this only says how tightly they stack on a page that has five panels of them.
 */
[data-acct-panel] .section-title { margin: 0 0 0.4rem; }
[data-acct-panel] .section-explainer { margin: 0.4rem 0; padding: 0.55rem 0.7rem; }
[data-acct-panel] .fb-form { gap: 0.6rem; }
[data-acct-panel] .notif-channel { margin: 0.5rem 0; padding: 0.5rem 0.7rem; }
[data-acct-panel] .notif-channel-head { margin-bottom: 0.25rem; }

/* ⚠ A SETTINGS ROW IS TWO COLUMNS, NOT A FLEX RUN. As flex, a long label and its control shared the
 * line until the label wrapped and the control drifted away from it. Label left, control right,
 * aligned down the panel — which is what makes a list of settings readable at a glance. */
[data-acct-panel] .notif-row {
  display: grid;
  /* ⚠ THREE COLUMNS: label, control, and the "default" marker. It was two, and the marker landed in
   * a third cell — which grid wraps onto a NEW ROW, doubling the height of every setting. Compacting
   * the panel and then doubling every row inside it is worse than leaving it alone. */
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 0.6rem;
  margin: 0.1rem 0;
}
/* ⓘ `fb-input` is width:100% by design — right for a form field, wrong for a number sitting at the
 * end of a settings row. */
[data-acct-panel] .notif-row .fb-input { width: auto; max-width: 7rem; }
/* ⓘ Control and its unit share ONE cell. As a fourth column they squeezed the label to three lines
 * on a 412px phone — the row got shorter and the label got taller, which is not a trade. */
[data-acct-panel] .notif-row .acct-ctl { display: flex; align-items: center; gap: 0.35rem; }
[data-acct-panel] .notif-row .fb-lbl { font-weight: 400; }

/* The "default" marker: present, and never competing with the value it qualifies. */
/* ⓘ No `grid-column` — it takes the third cell by flow. Pinning it to column 2 is what pushed it
 * onto its own row. */
[data-acct-panel] .notif-row .pricenote { justify-self: end; font-size: 0.72rem; white-space: nowrap; }

/* ⚠ THE VALUE IS NOT THE MUTED HALF. `fb-form-compact` already renders `.fb-lbl` as small uppercase
 * muted — that is what the compact variant is FOR. The first cut of #506 put `fb-opt` (also muted)
 * on the value beside it, so label and value were the same weight and colour and nothing carried the
 * emphasis. v2 renders these values at `--text-mid`; this is that, as a class rather than a style
 * attribute. #514. */
[data-acct-panel] .acct-val { color: var(--text-mid); }

/* ── Prime Vectors (#572) ─────────────────────────────────────────────────────────────────────
 * ⚠⚠ THE ONE NAMING BRIDGE. The carried vendor sheet defines this product's visual system under
 * its v2 codename tokens; the operator's ruling for the rebuild is ZERO occurrences of that name
 * in new code. These aliases are the single sanctioned exception — PVN-02 pins the old name to
 * exactly this marked block and nowhere else in src/ outside vendor/. All new markup and all new
 * rules use .pv-* / --pv-* only.
 * pvn-bridge-begin */
:root {
  --pv-panel: var(--tulloch-panel);
  --pv-card: var(--tulloch-card);
  --pv-card-hover: var(--tulloch-card-hover);
  --pv-ink: var(--tulloch-ink);
  --pv-edge: var(--tulloch-edge);
  --pv-win: var(--tulloch-win);
  --pv-miss: var(--tulloch-miss);
  --pv-muted: var(--tulloch-muted);
  --pv-live: var(--tulloch-live);
}
/* pvn-bridge-end */

/* ⚠⚠ #729 — `--pv-meta`, A NEW TOKEN, NOT A BRIDGE ALIAS (stays outside the pvn-bridge markers
 * above on purpose — it names nothing from the vendored v2 palette, so it does not belong inside
 * the block PVN-02 is meant to scope "tulloch" references to).
 *
 * `--pv-edge` (`--tulloch-edge`, vendor/styles.css:377, `#5f758e` in light theme) was designed as a
 * BORDER hairline — every one of its uses in the vendored sheet itself is `border: 1px solid`,
 * never text — but this file's own `.pv-slab .pv-sub` / `.pv-row .pv-meta` / `.pv-empty` reused it
 * for READABLE TEXT too (`.pv-chip`'s text was already `--pv-ink`; only its border used `--pv-edge`,
 * which stays unchanged here — borders carry no AA text-contrast requirement). Measured against the
 * slab's `#3b1f2b` card background: `--pv-edge` computes to ~3.1:1, failing WCAG AA's 4.5:1 for
 * normal-size text — this is the track/race-number/date line under every Prime Vectors selection, the
 * text a reader needs to tell selections apart.
 *
 * ⓘ `#8598ad` is `--tulloch-edge`'s own hue and saturation (211.9°, 19.8%), lightness raised from
 * 46% to 60% — computed to 5.01:1 against `#3b1f2b`, comfortably clearing 4.5:1 while still reading
 * as the same muted slate-grey family rather than a disconnected new colour.
 *
 * ⓘ Dark theme's `--tulloch-edge` is untouched and out of scope: it is `rgba(176,164,255,0.45)`
 * composited over a semi-transparent card, not a flat hex, and dark theme is not reachable in this
 * build at all (theme-boot's own THEMES = ["light"] resolves every preference to light) — nothing
 * a customer can see today regresses either way.
 */
:root { --pv-meta: #8598ad; }

/* ⚠⚠ #788 — `--bb-meta`, same method as `--pv-meta` above (#729). `--text-muted` (vendor/
 * styles.css:344, `#8b97a8` in light theme) is the "dimmest only" token, and Best Bets' `.bbx-where`
 * (track/race#/distance/going/class/jump-time) reused it for the line a customer reads to tell one
 * best bet apart from the next. Measured against `--bg-card`'s `#ffffff`: `--text-muted` computes to
 * 2.96:1, failing WCAG AA's 4.5:1 for normal-size text.
 *
 * ⓘ `#627083` is `--text-muted`'s own hue and saturation (215.2°, 14.3%), lightness lowered from
 * 60.2% to 45% (raised in the sense of being pushed AWAY from white, toward legible — the direction
 * that matters against this light background) — computed to 5.04:1 against `#ffffff`, comfortably
 * clearing 4.5:1 while staying in the same muted slate-grey family as the value it replaces, not a
 * disconnected new colour.
 */
:root { --bb-meta: #627083; }

/* The dark "terminal" slab — deliberately dark in light mode; brand contrast, v2's own design. */
.pv-slab { background: var(--pv-panel); padding: 0.9rem; }
.pv-slab .pv-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; }
.pv-slab .pv-title { color: var(--pv-ink); font-weight: 800; letter-spacing: 0.06em; font-size: var(--fs-sm); }
.pv-slab .pv-sub { color: var(--pv-meta); font-size: var(--fs-xs); }
.pv-rows { display: flex; flex-direction: column; gap: 0.45rem; margin-top: 0.6rem; }
.pv-row { display: block; background: var(--pv-card); color: var(--pv-ink); padding: 0.55rem 0.65rem;
          border: 1px solid var(--pv-edge); text-decoration: none; }
a.pv-row:hover { background: var(--pv-card-hover); }
.pv-row .pv-line1 { display: flex; justify-content: space-between; gap: 0.5rem; align-items: baseline; }
.pv-row .pv-horse { font-weight: 700; }
.pv-row .pv-odds { font-variant-numeric: tabular-nums; }
.pv-row .pv-meta { color: var(--pv-meta); font-size: var(--fs-xs); margin-top: 0.15rem; }
.pv-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.35rem; }
.pv-chip { border: 1px solid var(--pv-edge); color: var(--pv-ink); font-size: var(--fs-xs);
           padding: 0.05rem 0.4rem; }
.pv-status { font-size: var(--fs-xs); letter-spacing: 0.08em; }
.pv-status-won { color: var(--pv-win); } .pv-status-unplaced { color: var(--pv-miss); }
.pv-status-awaiting { color: var(--pv-live); } .pv-status-upcoming { color: var(--pv-muted); }
.pv-empty { color: var(--pv-meta); font-size: var(--fs-sm); padding: 0.6rem 0.2rem; }
.pv-locked { color: var(--pv-ink); font-size: var(--fs-sm); padding: 0.6rem 0.65rem;
             background: var(--pv-card); border: 1px solid var(--pv-edge); }
.pv-locked a { color: var(--pv-ink); }

/* The record block sits on the PAGE ground, not the slab — it is public prose. */
.pv-record { margin: 1rem 0; }
.pv-headline { font-size: 2.2rem; font-weight: 800; font-variant-numeric: tabular-nums; }
.pv-headline-building { color: var(--text-muted); font-size: 1.4rem; font-weight: 700; }
.pv-figure { color: var(--text-mid); font-size: var(--fs-sm); }
.pv-note { color: var(--text-dim); font-size: var(--fs-xs); max-width: 62ch; margin-top: 0.3rem; }
.pv-chart { margin: 0.8rem 0; }
.pv-chart svg { width: 100%; height: auto; display: block; }
.pv-seam { stroke: var(--text-dim); stroke-dasharray: 3 3; }
.pv-table-wrap { overflow-x: auto; }
.pv-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.pv-table th, .pv-table td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--border-light); }
.pv-table td.pv-num { text-align: right; font-variant-numeric: tabular-nums; }
.pv-units-pos { color: var(--win); } .pv-units-neg { color: var(--danger); }
.pv-era { margin-top: 1.4rem; padding-top: 0.8rem; border-top: 1px solid var(--border); }
.pv-era-tag { display: inline-block; border: 1px solid var(--border); color: var(--text-mid);
              font-size: var(--fs-xs); letter-spacing: 0.06em; padding: 0.1rem 0.45rem; margin-bottom: 0.4rem; }

/* ── Prime Vectors M3 Modernization ───────────────────────────────────────────────────────────
 * Prime Vectors surfaces inside real Google <md-elevated-card> components, styled
 * with M3 tonal tokens (--md-surface-container, --md-shape-lg, --md-space-*).
 */
.pv-board {
  display: flex;
  flex-direction: column;
  gap: var(--md-space-4);
  margin: var(--md-space-3) 0;
}
.pv-card {
  --md-elevated-card-container-color: var(--md-surface-container);
  --md-elevated-card-container-shape: var(--md-shape-lg);
  --md-elevated-card-container-elevation: 1;
  padding: var(--md-space-4);
}
.pv-card::part(elevation) {
  transition: box-shadow var(--md-motion-short) var(--md-motion-standard),
    opacity var(--md-motion-short) var(--md-motion-standard);
}
.pv-card:hover {
  --md-elevated-card-container-elevation: 2;
}
.pv-summary-card {
  /* M3 elevated summary surface */
}
.pv-selections-card {
  /* M3 elevated selections surface */
}
.pv-results-card {
  /* M3 elevated results surface */
}
.pv-era-card {
  /* M3 elevated v2 era surface */
}
.pv-summary-body {
  margin: 0;
}
.pv-selections-body,
.pv-results-body {
  background: transparent;
  padding: 0;
}
.pv-results-body .pv-head,
.pv-selections-body .pv-head {
  padding-bottom: var(--md-space-2);
}
.pv-results-body .pv-title,
.pv-selections-body .pv-title {
  color: var(--md-on-surface);
  font-size: var(--md-label-large);
  letter-spacing: 0.08em;
}
.pv-results-body .pv-table th {
  color: var(--md-on-surface-variant);
  border-bottom: 1px solid var(--md-outline-variant);
  font-size: var(--md-label-medium);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.pv-results-body .pv-table td {
  border-bottom: 1px solid var(--md-outline-variant);
  color: var(--md-on-surface);
}
.pv-era-body {
  margin: 0;
  padding-top: 0;
  border-top: none;
}

/* ── Quaddie M3 Modernization ───────────────────────────────────────────────────────────────────
 * Quaddie strike-rate history surfaces inside real Google <md-elevated-card> components, styled
 * with M3 tonal tokens (--md-surface-container, --md-shape-lg, --md-space-*).
 */
.qd-history {
  display: flex;
  flex-direction: column;
  gap: var(--md-space-4);
  margin: var(--md-space-3) 0;
}
.qd-card {
  --md-elevated-card-container-color: var(--md-surface-container);
  --md-elevated-card-container-shape: var(--md-shape-lg);
  --md-elevated-card-container-elevation: 1;
  padding: var(--md-space-4);
  max-width: 100%;
  box-sizing: border-box;
}
.qd-card::part(elevation) {
  transition: box-shadow var(--md-motion-short) var(--md-motion-standard),
    opacity var(--md-motion-short) var(--md-motion-standard);
}
.qd-card:hover {
  --md-elevated-card-container-elevation: 2;
}
.qd-summary-card {
  /* M3 elevated summary surface */
}
.qd-tickets-card {
  /* M3 elevated tickets table surface */
  overflow: hidden;
}
.qd-tickets-card .table-wrap {
  width: 100%;
  max-width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  margin: 0;
}
.qd-summary-body {
  margin: 0;
}
.qd-tickets-body {
  background: transparent;
  padding: 0;
  max-width: 100%;
  overflow: hidden;
}
.qd-tickets-body .pv-head {
  padding-bottom: var(--md-space-2);
}

/* ⚠ #835 — same per-component padding convention as .bbx-card/.qd-card above: a bare
   <md-elevated-card> gives its light-DOM slotted content no padding of its own, so every real
   usage names it explicitly rather than relying on a shared default that doesn't exist. */
.wn-releases-card {
  --md-elevated-card-container-color: var(--md-surface-container);
  --md-elevated-card-container-shape: var(--md-shape-lg);
  --md-elevated-card-container-elevation: 1;
  padding: var(--md-space-4);
}
.qd-tickets-body .pv-title {
  color: var(--md-on-surface);
  font-size: var(--md-label-large);
  letter-spacing: 0.08em;
}
.qd-tickets-body .pv-table th {
  color: var(--md-on-surface-variant);
  border-bottom: 1px solid var(--md-outline-variant);
  font-size: var(--md-label-medium);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  white-space: nowrap;
}
.qd-tickets-body .pv-table td {
  border-bottom: 1px solid var(--md-outline-variant);
  color: var(--md-on-surface);
  white-space: nowrap;
}

/* ⚠ #837/#838 — [data-digest-root]/[data-my-horses-root] (digest.js's/my-horses.js's own
   render()) each hold several cards; a bare <section> used to get its spacing from a vendor
   default that does not apply to md-elevated-card. */
[data-digest-root] {
  display: flex;
  flex-direction: column;
  gap: var(--md-space-4);
}
[data-my-horses-root] {
  display: flex;
  flex-direction: column;
  gap: var(--md-space-4);
}
/* ⚠ #831/#833/#834/#836/#837/#838/#839/#840/#841 — same per-component padding convention as
   .bbx-card/.qd-card above: a bare <md-elevated-card> gives its light-DOM slotted content no
   padding of its own, so every real usage names it explicitly. Found live (confirmed by #835's
   mutation-verified fence, extended here) -- without this, push.js's rendered states, the static
   prefs block, and each page's empty/not-built state all render jammed against the card's edge.
   ⚠ #839's own .hq-list/.hq-card (the individual qualifier articles) are pre-existing,
   already-styled classes, unchanged by this card — only the new outer wrapper needed it. */
.push-card, .alerts-prefs-card, .movers-notbuilt-card, .specials-notbuilt-card, .hbw-card, .digest-card, .my-horses-card, .hq-list-card, .hq-state-card, .predictions-notbuilt-card, .archive-card {
  --md-elevated-card-container-color: var(--md-surface-container);
  --md-elevated-card-container-shape: var(--md-shape-lg);
  --md-elevated-card-container-elevation: 1;
  padding: var(--md-space-4);
}

/* ── Base Chrome M3 Modernization ─────────────────────────────────────────────────────────────
 * Scoped to pages marked with data-m3="true" (card #825).
 * Modernizes the top site header, side navigation drawer / rail, and universal footer
 * using Material 3 surfaces, outline tokens, state layers, and typography.
 */
body[data-m3="true"] {
  background-color: var(--md-surface);
  color: var(--md-on-surface);
}

/* ── 1 · Top Header (.site-header) ─────────────────────────────────────────────────────────── */
body[data-m3="true"] .site-header {
  background-color: var(--md-surface);
  border-bottom: 1px solid var(--md-outline-variant);
  box-shadow: var(--md-elevation-1);
}
body[data-m3="true"] .sh-top {
  background: var(--md-surface);
  border-bottom: 1px solid var(--md-outline-variant);
}
body[data-m3="true"] .sh-logo-word {
  color: var(--md-primary);
  font-family: inherit;
  font-weight: 700;
}
/* ⚠⚠ CARD #863 — `.sh-search .sb-field`/`.sh-search .sb-ico` REMOVED, NOT MOVED. Both were dead
 * before this card touched anything: `search.js`'s real markup has never emitted a `.sb-field`
 * wrapper or an `.sb-ico` icon — this rule pair styled elements that do not exist, one of the
 * carried sheet's own "573 classes with no markup behind them" (PROVENANCE.md), just in this app's
 * own stylesheet instead of the vendor one. `.sb-input` is a real `<md-filled-text-field>` now (was
 * a bare `<input>`) — it IS its own filled, rounded box by definition (Material's own search
 * guidance: "search bars are rounded, use tonal surface"), so there is no wrapper left to give this
 * treatment to even if the dead rule had ever been alive.
 *
 * ⚠ `--md-filled-text-field-*` CUSTOM PROPERTIES, NOT `color`/`font-size` ON THE HOST — AND NOT THE
 * LOWER-LEVEL `--md-filled-field-*` NAMES EITHER. Both a plain `color:`/`font-size:` on the host
 * AND the field-level tokens were tried and found not to reach the actually-rendered text/shape —
 * the shape especially: `--md-filled-field-container-shape` resolved on this element's own
 * `getComputedStyle` but never reached the doubly-nested internal `<md-filled-field>`'s own shadow-
 * rendered surface (checked live: computed border-radius stayed 4px regardless). The TEXT-FIELD-
 * level tokens (`--md-filled-text-field-*`, this component's own outermost, documented custom-
 * property surface) are the ones its internal CSS actually maps down through every nested layer —
 * confirmed live after switching to them. `::placeholder` is dropped for a related but different
 * reason: a light-DOM pseudo-element selector cannot reach a shadow-rendered `<input>`'s placeholder
 * at all; `--md-filled-text-field-input-text-placeholder-color` is the real, reachable equivalent.
 */
body[data-m3="true"] .sh-search .sb-input {
  --md-filled-text-field-container-shape: var(--md-shape-full);
  --md-filled-text-field-container-color: var(--md-surface-container-high);
  --md-filled-text-field-top-space: 0.45rem;
  --md-filled-text-field-bottom-space: 0.45rem;
  --md-filled-text-field-leading-space: 1rem;
  --md-filled-text-field-trailing-space: 1rem;
  --md-filled-text-field-input-text-color: var(--md-on-surface);
  --md-filled-text-field-input-text-font: inherit;
  --md-filled-text-field-input-text-size: var(--md-body-medium);
  --md-filled-text-field-input-text-placeholder-color: var(--md-on-surface-variant);
  /* ⚠⚠ THE UNDERLINE ("active indicator") IS ZEROED AT REST/HOVER, EXPLICITLY RESTORED ON FOCUS.
   * A filled field's signature bottom line reads as a form-field affordance, not a search bar —
   * but removing it everywhere would leave the box with zero focus indication beyond the browser's
   * own caret. ⚠ THE FOCUS HEIGHT MUST BE SET EXPLICITLY, NOT LEFT UNSET — checked live: leaving
   * `--md-filled-text-field-focus-active-indicator-height` alone still rendered 0px on focus, since
   * its own internal fallback chain reads back the SAME base `--md-filled-text-field-active-
   * indicator-height` this rule already forces to 0 when nothing more specific is set. Setting it
   * to a real value here is what actually restores a visible focus indicator.
   */
  --md-filled-text-field-active-indicator-height: 0px;
  --md-filled-text-field-hover-active-indicator-height: 0px;
  --md-filled-text-field-focus-active-indicator-height: 2px;
  --md-filled-text-field-focus-active-indicator-color: var(--md-primary);
  width: 100%;
}
body[data-m3="true"] .sb-results {
  background: var(--md-surface-container);
  border: 1px solid var(--md-outline-variant);
  border-radius: var(--md-shape-sm);
  box-shadow: var(--md-elevation-2);
}
body[data-m3="true"] .sb-item {
  color: var(--md-on-surface);
  font-size: var(--md-body-medium);
  border-radius: var(--md-shape-xs);
}
body[data-m3="true"] .sb-item + .sb-item {
  margin-top: 1px;
}
body[data-m3="true"] .sb-item:hover,
body[data-m3="true"] .sb-item.sb-active {
  background: var(--md-surface-container-high);
}
body[data-m3="true"] .sb-item .sb-kind {
  color: var(--md-on-surface-variant);
  font-size: var(--md-label-small);
  text-transform: uppercase;
}
body[data-m3="true"] .sh-star,
body[data-m3="true"] .sh-acct-btn {
  background: var(--md-surface-container);
  border: 1px solid var(--md-outline-variant);
  border-radius: var(--md-shape-full);
  color: var(--md-on-surface-variant);
  height: 36px;
  transition: background var(--md-motion-short) var(--md-motion-standard),
    color var(--md-motion-short) var(--md-motion-standard);
}
body[data-m3="true"] .sh-star:hover,
body[data-m3="true"] .sh-acct-btn:hover {
  background: var(--md-surface-container-high);
  color: var(--md-on-surface);
}
body[data-m3="true"] .sh-acct-menu {
  background: var(--md-surface-container);
  border: 1px solid var(--md-outline-variant);
  border-radius: var(--md-shape-sm);
  box-shadow: var(--md-elevation-2);
}
body[data-m3="true"] .sh-acct-item {
  color: var(--md-on-surface);
  font-size: var(--md-body-medium);
  border-radius: var(--md-shape-xs);
}
body[data-m3="true"] .sh-acct-item:hover {
  background: var(--md-surface-container-high);
  color: var(--md-on-surface);
}
body[data-m3="true"] .sh-acct-sep {
  background: var(--md-outline-variant);
}
body[data-m3="true"] .sh-jump {
  background: var(--md-surface-container-low);
}
body[data-m3="true"] .jh-label {
  color: var(--md-on-surface-variant);
  font-size: var(--md-label-small);
}
body[data-m3="true"] .jh-race {
  background: var(--md-surface);
  border: 1px solid var(--md-outline-variant);
  border-radius: var(--md-shape-sm);
  color: var(--md-on-surface);
  transition: background var(--md-motion-short) var(--md-motion-standard);
}
body[data-m3="true"] .jh-race:hover {
  background: var(--md-surface-container-high);
}
body[data-m3="true"] .jh-r-time {
  color: var(--md-primary);
}
body[data-m3="true"] .jh-r-time.jh-off {
  color: var(--md-on-surface-variant);
}

/* ── 2 · Side Main Menu / Drawer (.top-nav) ─────────────────────────────────────────────────── */
body[data-m3="true"] .top-nav {
  background: var(--md-surface-container-low);
  border-right: 1px solid var(--md-outline-variant);
}
body[data-m3="true"] .tn-inner {
  padding: var(--md-space-3) var(--md-space-2);
}
body[data-m3="true"] .tn-link {
  color: var(--md-on-surface-variant);
  font-size: var(--md-label-large);
  border-radius: var(--md-shape-full);
  padding: 0.5rem 0.9rem;
  transition: background var(--md-motion-short) var(--md-motion-standard),
    color var(--md-motion-short) var(--md-motion-standard);
}
body[data-m3="true"] .tn-link:hover {
  background: var(--md-surface-container-high);
  color: var(--md-on-surface);
}
body[data-m3="true"] .tn-link.tn-active {
  background: var(--md-primary-container);
  color: var(--md-on-primary-container);
  font-weight: 600;
}
body[data-m3="true"] .tn-dropdown {
  position: relative;
  width: 100%;
}
body[data-m3="true"] .tn-drop-header {
  display: flex;
  align-items: center;
}
body[data-m3="true"] .tn-drop-header .tn-drop-link {
  flex: 1;
  border-radius: var(--md-shape-full) 0 0 var(--md-shape-full);
}
body[data-m3="true"] .tn-caret-btn {
  color: var(--md-on-surface-variant);
  border-radius: 0 var(--md-shape-full) var(--md-shape-full) 0;
  padding: 0.5rem 0.75rem;
  transition: background var(--md-motion-short) var(--md-motion-standard),
    color var(--md-motion-short) var(--md-motion-standard);
}
body[data-m3="true"] .tn-caret-btn:hover {
  background: var(--md-surface-container-high);
  color: var(--md-on-surface);
}
body[data-m3="true"] .tn-drop-header:hover .tn-drop-link,
body[data-m3="true"] .tn-drop-header:hover .tn-caret-btn {
  background: var(--md-surface-container-high);
  color: var(--md-on-surface);
}
body[data-m3="true"] .tn-caret-btn.tn-caret-active {
  color: var(--md-primary);
  background: var(--md-surface-container-high);
}
/* ⚠⚠ CARD #861 — REMOVED THE `.tn-drop-menu` BOX-MODEL SKIN THAT USED TO LIVE HERE (background,
 * border, border-radius, box-shadow, padding, the desktop/mobile width variants, the scrollbar
 * pseudo-elements). `.tn-drop-menu` now sits on a real `<md-menu>`, and the ungated
 * `.tn-drop-menu { display: contents; }` rule above (near the `--md-sys-color-*` bridge) makes
 * that host generate no box at all — every property that used to paint here would be a no-op on
 * it now, the same way padding on a `<template>` is. The actual popup surface — background, shape,
 * elevation, scrolling — is `<md-menu>`'s OWN internal shadow-DOM rendering, themed through that
 * same bridge; there is nothing left here for this ruleset to style. `.tn-drop-item` below is
 * unaffected — `<md-menu-item>`'s own host is `display: flex`, a real box, so its skin still
 * applies exactly as before. */
body[data-m3="true"] .tn-drop-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  color: var(--md-on-surface-variant);
  font-size: var(--md-label-large);
  font-weight: 500;
  line-height: 1.35;
  border: none;
  border-top: none;
  border-radius: var(--md-shape-full);
  margin: 0.15rem 0.25rem;
  padding: 0.55rem 0.85rem;
  text-decoration: none;
  transition: background var(--md-motion-short) var(--md-motion-standard),
    color var(--md-motion-short) var(--md-motion-standard);
}
body[data-m3="true"] .tn-drop-item:first-child {
  border-top: none;
}
body[data-m3="true"] .tn-drop-item:hover {
  background: var(--md-surface-container-high);
  color: var(--md-on-surface);
}
body[data-m3="true"] .tn-drop-item-active {
  background: var(--md-primary-container);
  color: var(--md-on-primary-container);
  font-weight: 600;
}
body[data-m3="true"] .tn-drop-heading {
  color: var(--md-on-surface-variant);
  font-size: var(--md-label-small);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 0.6rem 0.85rem 0.25rem;
  border-top: none;
}
body[data-m3="true"] .tn-divider {
  height: 1px;
  background: var(--md-outline-variant);
  margin: var(--md-space-2) 0.5rem;
  border: none;
}
body[data-m3="true"] .tn-soon-at {
  background: var(--md-surface-container-highest, #e4e4e7);
  color: var(--md-on-surface-variant);
  border: 1px solid var(--md-outline-variant);
  border-radius: var(--md-shape-full);
  font-size: var(--md-label-small);
  font-weight: 600;
  letter-spacing: 0.04em;
  padding: 0.1rem 0.5rem;
  flex-shrink: 0;
}

/* ── 3 · Universal Footer (.mk-foot) ─────────────────────────────────────────────────────────── */
body[data-m3="true"] footer[data-mk-footer] {
  background: var(--md-surface-container-low);
  border-top: 1px solid var(--md-outline-variant);
  color: var(--md-on-surface-variant);
  margin-top: var(--md-space-6);
}
body[data-m3="true"] .mk-foot-h {
  color: var(--md-on-surface);
  font-size: var(--md-label-large);
  font-weight: 700;
  letter-spacing: 0.05em;
}
body[data-m3="true"] .mk-foot-col a {
  color: var(--md-on-surface-variant);
  font-size: var(--md-body-medium);
  transition: color var(--md-motion-short) var(--md-motion-standard);
}
body[data-m3="true"] .mk-foot-col a:hover {
  color: var(--md-primary);
}
body[data-m3="true"] .mk-foot-tag {
  color: var(--md-on-surface-variant);
  font-size: var(--md-body-medium);
}
body[data-m3="true"] .mk-foot-bar {
  border-top: 1px solid var(--md-outline-variant);
  color: var(--md-on-surface-variant);
  font-size: var(--md-label-small);
}
body[data-m3="true"] .mk-foot-cr {
  color: var(--md-on-surface-variant);
}

@media (max-width: 1099px) {
  /* Provide clearance so universal footer and 18+ regulatory bar remain fully visible above .bottom-nav */
  body[data-m3="true"] footer[data-mk-footer] {
    padding-bottom: calc(54px + env(safe-area-inset-bottom, 0px) + var(--md-space-4));
  }
}

/* ── 4 · The marketing pages (.mk-*) ──────────────────────────────────────────────────────────
 *
 * ⚠⚠ OVERRIDES, NOT EDITS. Every `.mk-*` rule lives in `src/vendor/styles.css` — 123 of them in a
 * carried sheet that CMP-06 asserts byte-identical against PROVENANCE.md. That sheet is not ours to
 * change, and the fix for a marketing style has to land HERE, gated the same way section 3 already
 * gates the universal footer. CMP-06 has caught exactly this being written in the wrong file before.
 *
 * ⚠⚠ THE PAGES HAD NO CORNER RADIUS AT ALL, AND THAT WAS THE WHOLE VISUAL GAP. Measured on
 * production at 375 and 1280: the computed border-radius map on welcome, pricing and features is
 * EMPTY — not "mostly square", not "a few literals", zero rounded elements on the page. Meanwhile
 * the colour layer was already M3: `--md-sys-color-primary` resolves on these pages because #798's
 * bridge sits at bare `:root`, so the palette never needed touching. One token was the difference.
 *
 * ⓘ THE RADII ARE THE APP'S OWN VOCABULARY, NOT GENERIC MD3. Read off what this repo already does
 * under `body[data-m3="true"]`: surfaces take `--md-shape-sm`, list items `--md-shape-xs`, pills and
 * controls `--md-shape-full`, accordion groups `--md-shape-md`. Marketing follows the app because
 * matching the app IS the request — a visitor should not be able to tell they crossed a boundary at
 * sign-in. Generic MD3 would put cards at 12dp and disagree with every app page in this repo.
 *
 * ⚠ AND `data-m3="true"` IS THE ROLLBACK. It is one attribute per page; removing it from a `<body>`
 * returns that page to the carried sheet exactly, with no CSS to revert. The same per-page kill
 * switch the app rollout used, which is why the flag is worth keeping rather than replacing with a
 * bare `.mk-` selector.
 */

/* Surfaces — a card, a stat, a tier, a comparison cell. One radius, so they read as one family. */
body[data-m3="true"] .mk-tier,
body[data-m3="true"] .mk-cell,
body[data-m3="true"] .mk-stat,
body[data-m3="true"] .mk-trust-item,
body[data-m3="true"] .mk-blog-card,
body[data-m3="true"] .mk-hero-media img {
  border-radius: var(--md-shape-sm);
}

/* ⓘ The hero image sets `border-radius: 0` explicitly in the carried sheet, so it needs the same
 *  specificity to move — which the `body[...]` prefix already gives it. */

/* Controls and badges. ⚠ `--md-shape-full` rather than a step: MD3 buttons are pills, and this repo
 * already made that call for `.tn-link`, `.sh-acct-btn` and `.jh-race`. A marketing CTA at 8px
 * beside an app whose every button is fully rounded is the mismatch this card exists to remove. */
body[data-m3="true"] .mk-cta,
body[data-m3="true"] .mk-cta-ghost,
body[data-m3="true"] .mk-cta-lg,
body[data-m3="true"] .mk-tier-badge,
body[data-m3="true"] .mk-badge-live,
body[data-m3="true"] .mk-nav-toggle {
  border-radius: var(--md-shape-full);
}

/**
 * ⚠⚠ "LOG IN" WAS TOUCHING THE SIGN-UP BUTTON — 0px, measured, at 1280 on production and on main.
 *
 * The nav's own `gap: 1.3rem` is fine and does its job: 23px between every `.mk-nav-link`. But the
 * two auth controls are wrapped in a `<span>` that computes to `display: block`, so the flex gap
 * never reaches them — the link ends at x=1104 and the button's border begins at x=1104. The
 * button's 18px padding keeps its OWN text clear, which is why this reads as the link running
 * underneath the button rather than as missing space.
 *
 * ⓘ I recorded this as an overlap first, from a screenshot. Measuring the rectangles found no
 * intersection at 1024/1100/1280/1440 — they are exactly flush. Same symptom, different cause, and
 * only one of the two descriptions leads to this fix.
 *
 * ⚠ Fixed on the WRAPPER, not by putting a margin on the button: the button is used elsewhere
 * (hero, pricing tiers, CTA band) where it is not preceded by anything and a margin would be dead
 * space. The defect belongs to this grouping, so the fix does too.
 */
body[data-m3="true"] .mk-nav-links > span {
  display: inline-flex;
  align-items: center;
  gap: var(--md-space-3);
}

/* ── 5 · Marketing and the app agree on the blue, and disagree on the bar ─────────────────────
 *
 * ⚠⚠ THE TWO HEADERS STAY DIFFERENT ON PURPOSE, AND THAT IS AN OPERATOR DECISION, NOT AN OVERSIGHT.
 * Marketing's bar is white and the app's is Oxford Navy, because the change of colour is what tells
 * a reader they have crossed from the public site into the signed-in product. Anyone tempted to
 * "fix" that inconsistency should read this first: it is the one difference carrying meaning.
 *
 * ⚠ EVERYTHING ELSE ABOUT THEM WAS DIFFERENT BY ACCIDENT, WHICH IS WHAT THIS SECTION FIXES.
 * Measured through a single origin serving both, at 1280:
 *
 *                     marketing        app
 *     bar height      52px             56px      (AppShell `shTop`)
 *     logo            34 x 26          37 x 28   (AppShell `logo`)
 *     brand blue      #383961          #003262   (`palette.ts` `primary`)
 *
 * None of those three was decided; they are what two surfaces built months apart drift into. The
 * app's numbers win because they are the ones written down as constants with a card behind them —
 * `#003262` is the operator's Oxford Navy, named in `palette.ts` as the brand's own.
 *
 * ── ⚠⚠ WHY `data-site`, WHEN `data-m3` ALREADY EXISTS ─────────────────────────────────────────
 *
 * `data-m3="true"` does NOT mean "marketing". It is on eighteen app pages too — `qualifiers.html`,
 * `account.html`, `app.html` and the rest of the gated vanilla surface all carry it, and they read
 * `--accent` as well. Retoning the token under `[data-m3]` alone would have repainted the brand blue
 * across the signed-in vanilla app in the same edit, silently, on a card that says "marketing".
 *
 * ⓘ So the surface names itself. `data-site="marketing"` is on the ten pages this card restyled and
 * nowhere else, and MK-01 asserts the pair together — which also keeps the one kill switch honest,
 * since removing `data-m3` still reverts everything here.
 */
body[data-m3="true"][data-site="marketing"] {
  /* ⓘ The brand navy, from `palette.ts`'s `primary` — borrowed, not re-picked, so there is one
     source for it and this file is a consumer rather than a second opinion. */
  --accent: #003262;
  --accent-btn: #003262;
  --accent-light: #003262;
  --accent-rgb: 0, 50, 98;
  --md-primary: #003262;
  /**
   * ⚠ THE HOVER IS DERIVED, NOT PICKED, AND THE ARITHMETIC IS HERE BECAUSE IT HAD TO BE. The app has
   * no hover token to borrow — Paper draws its own state layer as an overlay — so the only honest
   * option was to keep the RELATIONSHIP the carried sheet already used and move it onto the new base.
   * That pair was #383961 -> #2b2c4d: rgb(56,57,97) -> (43,44,77), a multiply of ~0.78 per channel.
   * The same factor on rgb(0,50,98) gives (0,39,76) = #00274c.
   * ⓘ Recorded rather than rounded to something that looked right, so the next person retoning this
   * changes the base and re-derives, instead of guessing at two independent colours.
   */
  --accent-hover: #00274c;
}

/**
 * 52 -> 56 and 26 -> 28, to the app's own constants.
 *
 * ⚠⚠ 55, NOT 56, AND THE DIFFERENCE IS THE BORDER. AppShell's `shTop` is `{ height: 56,
 * borderBottomWidth: 1 }` and React Native Web boxes are `border-box`, so the app's 56 INCLUDES its
 * hairline. The marketing bar puts that hairline on the `<header>` and the row inside it, so
 * `min-height: 56` on the row measured 57 total. Matching the number is not matching the bar.
 *
 * ⚠ AND THE MOBILE PADDING HAD TO GO TOO. `header { padding: 1rem 0.75rem }` below the breakpoint
 * added 32px on top, so at 375 the "56px" bar measured 89 while the app's stayed 56 — the width
 * where the two sit closest together in a reader's memory, because it is the one they scroll on.
 * ⓘ I only found it by measuring BOTH widths; at 1280 alone this looked finished and was 33px out.
 *
 * ⓘ `:has(.mk-nav)` rather than a bare `header`: a marketing page may carry an article header, and
 * this rule is about the site bar specifically.
 */
body[data-m3="true"][data-site="marketing"] header:has(.mk-nav) {
  padding-top: 0;
  padding-bottom: 0;
}
body[data-m3="true"][data-site="marketing"] .mk-nav {
  min-height: 55px;
  padding-top: 0;
  padding-bottom: 0;
}
body[data-m3="true"][data-site="marketing"] .mk-brand-logo {
  height: 28px;
}

/* ── the Prime Vectors tune page (#573) — on the .ap-* admin kit, no colour literals ─────────── */
.pva-page .pva-lockwarn { border: 1px solid var(--danger); color: var(--text); padding: 0.6rem 0.8rem;
  margin: 0.8rem 0; font-size: var(--fs-sm); }
.pva-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1.2rem; }
.pva-g { margin: 0.2rem 0 0.5rem; font-size: var(--fs-sm); letter-spacing: 0.04em; }
.pva-row { display: block; margin: 0.45rem 0; }
.pva-row .pva-k { display: inline-block; min-width: 11rem; font-size: var(--fs-sm); }
.pva-row .pva-v { display: inline-block; min-width: 2.6rem; text-align: right;
  font-variant-numeric: tabular-nums; font-size: var(--fs-sm); }
.pva-row .pva-hint { display: block; color: var(--text-dim); font-size: var(--fs-xs); max-width: 60ch;
  margin: 0.1rem 0 0 0; }
.pva-tick input { margin-right: 0.4rem; }
.pv-slider { width: 11rem; vertical-align: middle; accent-color: var(--accent); }
.pva-xc label { margin-right: 0.6rem; font-size: var(--fs-sm); }
.pva-step { margin-top: 1rem; }
.pva-step .pva-n { display: inline-block; width: 1.5rem; height: 1.5rem; line-height: 1.5rem;
  text-align: center; background: var(--accent); color: var(--on-accent); border-radius: 50%;
  font-size: var(--fs-xs); margin-right: 0.35rem; }
.pva-danger .pva-n { background: var(--danger); }
.pva-done .pva-n { background: var(--text-dim); }
.pva-opt { color: var(--text-dim); font-size: var(--fs-xs); border: 1px solid var(--border);
  padding: 0 0.35rem; margin-left: 0.4rem; }
.pva-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin: 0.6rem 0; }
.pva-out { font-size: var(--fs-sm); color: var(--text-mid); }
.pva-err { color: var(--danger); }
.pva-confirm .pva-q { font-size: var(--fs-sm); color: var(--danger); margin-right: 0.4rem; }
.pva-picks { font-size: var(--fs-sm); margin: 0.4rem 0 0 1.2rem; }

/* ── The sticky venue column must stay OPAQUE (#610) ────────────────────────────────────────────
 *
 * ⚠⚠ OVERRIDE, NOT AN EDIT TO THE VENDORED SHEET. `src/vendor/styles.css` is a carried copy,
 * checksummed in PROVENANCE.md and asserted byte-identical by CMP-06 — which caught this fix in the
 * wrong file before it left the branch. The vendored rule stays as it is; this corrects it here.
 *
 * The vendored sheet has `.rm-venue.rm-uncov, .rm-cell.rm-uncov { opacity: 0.45 }`. `.rm-venue` is
 * `position: sticky` and hides the scrolling row behind an opaque `--bg-card`, and `opacity`
 * applies to the element as a whole, background included — so at 0.45 the column went 55%
 * see-through and the row scrolled visibly behind the track name. Reported from the live site the
 * evening the first fully-abandoned meeting rendered: "TIP", "R2", "R3", "01:50pm" and "ABN" all
 * legible on top of the venue name.
 *
 * ⚠ `.rm-venue` ALREADY CARRIED `z-index: 2` FOR THIS SYMPTOM, with a comment saying so — "badges
 * bleed over the track name on scroll". That fix was correct and remained in force throughout.
 * Opacity defeated it without touching it: the stacking order was right and the cell was simply
 * transparent. A guard still working, guarding the other half of the problem.
 *
 * ⓘ The scrolling cells keep whole-element opacity, which is right: they are not sticky, they paint
 * no background, and nothing is hidden behind them. If one is ever made sticky it acquires this
 * defect silently — which is what CLI-156 exists to catch.
 */
.rm-venue.rm-uncov { opacity: 1; }
.rm-venue.rm-uncov > * { opacity: 0.45; }

/* ── THE FORM RECORD BOARD (#608) — every flow's strike WITH the favourite's beside it ──────────
 * The disclosure the operator ruled for: boost keeps serving, its record published where it is
 * sold. One row per flow; the pair of figures is one visual unit, because the point of the block
 * is the comparison, not either number alone. */
.frb { display: flex; flex-direction: column; gap: 0.6rem; margin: 0.9rem 0; }
.frb-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem 0.9rem;
  background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
  padding: 0.7rem 1rem; }
.frb-row-empty { opacity: 0.75; }
.frb-flow { flex: 0 0 5.5rem; font-weight: 700; color: var(--text); }
.frb-figs { display: flex; align-items: baseline; gap: 0.55rem; }
.frb-fig { color: var(--text); }
.frb-fig b { font-size: 1.15rem; font-variant-numeric: tabular-nums; }
.frb-fav b { color: var(--green, #34d399); }
.frb-vs { color: var(--text-muted); font-size: var(--fs-sm, 0.85rem); }
.frb-building { color: var(--text-muted); font-style: italic; }
.frb-why { color: var(--text-muted); font-style: italic; }
.frb-counts { flex-basis: 100%; color: var(--text-muted); font-size: var(--fs-sm, 0.85rem); }
.frb-note { color: var(--text-muted); font-size: var(--fs-sm, 0.85rem); line-height: 1.5;
  margin: 0.2rem 0 0; }

/* ── #675: the header betslip star's count pip ──────────────────────────────────────────────────
 * The button, the star and its gold on-state are the vendored v2 classes (.betslip-btn,
 * .star-icon, .active); only the count needs a home. Positioned off the button's corner the way
 * the v2 header wore it. */
.sh-star { position: relative; margin-right: 0.35rem; }
.sh-star-count { position: absolute; top: -2px; right: -4px; min-width: 1rem; height: 1rem;
  padding: 0 0.2rem; border-radius: 4px; background: var(--accent, #383961); color: #fff;
  font-size: 0.66rem; font-weight: 700; line-height: 1rem; text-align: center;
  font-variant-numeric: tabular-nums; }

/* ── #688: the admin "View as" control, moved into the account dropdown ─────────────────────────
 * `.va-bar` carried ZERO rules anywhere — measured, `grep -rn "\.va-bar" src/vendor/*.css` returns
 * nothing — so it rendered as a bare unstyled row wherever `document.body.appendChild` happened to
 * drop it. It now lives inside `.sh-acct-menu`, which already lays out `.sh-acct-item` rows; this
 * gives the row itself a flex layout so its icon, label and <select> line up instead of wrapping
 * as three loose inline elements, and matches the dropdown's own padding/type scale rather than
 * inventing a second one. */
.va-bar { display: flex; align-items: center; gap: 0.4rem; padding: 0.5rem 0.6rem;
  font-size: var(--fs-sm); color: var(--text-mid); }
/* ⚠ Without this the label wraps ("View" / "as") the moment the select needs room for "Vector
 * Boost" — measured in a real browser, not assumed. The icon and label hold their own width;
 * the select is the one thing allowed to give ground (`min-width: 0` below). */
.va-bar > span { white-space: nowrap; flex: none; }
.va-bar select { flex: 1 1 auto; min-width: 0; background: var(--bg-card); color: var(--text);
  border: 1px solid var(--border); border-radius: 6px; padding: 0.3rem 0.4rem; font: inherit;
  font-size: var(--fs-sm); }
/* ⚠ `.va-on` MUST BE OBVIOUS AT A GLANCE. An admin who forgets they are previewing a tier is an
 * admin reading someone else's payload as their own — the tint is the reminder, not decoration. */
.va-bar.va-on { color: var(--accent-light, var(--accent));
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border-radius: 6px; margin: 0 -0.1rem; }

/* ── #696: the component library page ────────────────────────────────────────────────────────── */
.adc-section { margin-bottom: 1.5rem; }
.adc-section-note { color: var(--text-muted); font-size: var(--fs-sm); margin: 0.2rem 0 1rem; max-width: 60ch; }
.adc-demo-row { display: flex; align-items: center; gap: 1rem; padding: 0.6rem 0;
  border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.adc-demo-row:last-child { border-bottom: none; }
.adc-demo-label { flex: 0 0 min(320px, 100%); color: var(--text-mid); font-size: var(--fs-sm); }
.adc-demo-render { display: flex; align-items: center; gap: 0.4rem; }
.adc-empty { color: var(--text-dim); font-style: italic; font-size: var(--fs-sm); }
.adc-row { padding: 0.6rem 0; border-bottom: 1px solid var(--border);
  display: grid; grid-template-columns: 10rem 1fr; gap: 0.2rem 1rem; }
.adc-row:last-child { border-bottom: none; }
.adc-name { font-family: var(--mono); font-size: var(--fs-sm); color: var(--accent-light, var(--accent));
  grid-row: 1 / 3; align-self: start; }
.adc-sites { color: var(--text-dim); font-size: var(--fs-2xs, 0.75rem); font-family: var(--mono); }
.adc-note { grid-column: 2; color: var(--text-mid); font-size: var(--fs-sm); margin: 0; }
@media (max-width: 640px) {
  .adc-row { grid-template-columns: 1fr; }
  .adc-name { grid-row: auto; }
  .adc-note { grid-column: 1; }
}

/* ── #848: the M3 section of the component library page — token catalog + the "bundled, unused"
 * flag on adc-row's own .adc-sites line (md-filled-tonal-button today). .adc-demo-render (an
 * existing #696 class) carries the live example under a catalog row's note, so it needs the same
 * grid-column as .adc-note to sit under it rather than falling into .adc-name's 10rem column. */
.adc-row .adc-demo-render { grid-column: 2; margin-top: 0.4rem; }
@media (max-width: 640px) { .adc-row .adc-demo-render { grid-column: 1; } }
.mdc-unused-flag { color: var(--warn, #b45309); font-style: italic; }
.mdc-swatch-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 0.5rem; margin-bottom: 1rem; }
.mdc-swatch { height: 4rem; border-radius: var(--md-shape-sm); border: 1px solid var(--border);
  display: flex; align-items: flex-end; padding: 0.35rem; box-sizing: border-box; }
.mdc-swatch-label { font-family: var(--mono); font-size: var(--fs-2xs, 0.7rem);
  background: rgba(255,255,255,0.85); color: #111; padding: 0.1rem 0.3rem; border-radius: 3px; }
.mdc-shape-grid { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-bottom: 1rem; }
.mdc-shape-box { width: 4.5rem; height: 4.5rem; background: var(--md-surface-container);
  border: 1px solid var(--md-outline-variant); display: flex; align-items: center;
  justify-content: center; font-size: var(--fs-2xs, 0.75rem); color: var(--text-mid); }
.mdc-space-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.2rem 0; }
.mdc-space-label { flex: 0 0 8rem; font-family: var(--mono); font-size: var(--fs-2xs, 0.75rem);
  color: var(--text-dim); }
.mdc-space-bar { height: 0.9rem; background: var(--md-primary); border-radius: 2px; }
.mdc-elevation-grid { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem; }
.mdc-elevation-box { padding: 1rem 1.25rem; background: var(--md-surface-container);
  border-radius: var(--md-shape-lg); font-size: var(--fs-sm); color: var(--text-mid); }

/* ── #710: the account page's blackbook row — an honest "not entered" state ─────────────────── */
.bs-row-muted { font-style: italic; opacity: 0.75; }

/* ── #710: two pre-existing orphans, found by STY-02 — real classes with no rule, not new ones ─
 * .bs-settle (#699's betRow) and .fc-text (every Follow button since #589) had never been styled;
 * nothing checked either page until the account page's fence started scanning them. */
.bs-settle { font-size: 0.72rem; color: var(--text-dim); text-transform: uppercase;
  letter-spacing: 0.02em; }
.fc-text { font-size: 0.78rem; }

/* ══════════════════════════════════════════════════════════════════════════════════════════════
 * ── THE M3 TOKEN LAYER (card #792) — ADDITIVE ONLY, NOTHING CONSUMES THESE YET ──────────────────
 *
 * Card #792 is 1 of 4 in the M3 design-system pilot on Best Bets
 * (see /home/philm/.claude/plans/stateless-watching-pony.md). This card ships the token layer
 * alone, on purpose — zero rendering diff on any page, provably so, because no selector below
 * this block references a `--md-*` custom property yet. Cards 2-4 spend these tokens on
 * `.bbx-card`, the buttons/chips/flow-toggle, and the form controls/accordion respectively.
 *
 * ⚠ `--md-*` (Material Design), NOT `--pv-*`/`--bb-*`. Those two prefixes are page-scoped
 * contrast fixes for ONE token each (cards #729/#788); this is a full role system, deliberately
 * differently-named so it can never be confused with either.
 *
 * ⚠⚠ vendor/styles.css IS HASH-PINNED (CMP-06) AND CANNOT BE EDITED — every token here lives in
 * this overlay file, the same discipline this file's own header (line 1) already states.
 *
 * ⚠⚠ CARD #797 — REAL GOOGLE COLOUR SCIENCE, NOT AN HSL APPROXIMATION. The values below were
 * originally computed by hand-rolled HSL-lightness stepping (card #792's own first draft). The
 * operator asked directly whether real Google code was used; told no; asked for it. This card
 * replaces every hex below with values computed by Google's own `material-color-utilities`
 * library (the actual HCT/CAM16 perceptual colour space M3 itself is built on) — used as a
 * DEV-TIME-ONLY generator (never a runtime dependency: `npm install`ed in a scratch directory,
 * run once, deleted — the same category as the one-off scripts `--pv-meta`/`--bb-meta` already
 * used, just with real colour science backing the numbers this time instead of hand-rolled HSL).
 *
 * ⚠⚠ BRAND CHROMA PRESERVED, NOT GOOGLE'S DEFAULT STYLE. `material-color-utilities`'s own
 * `CorePalette.of(seed)` (what `themeFromSourceColor()` uses) does not just relight a seed colour
 * — it also RECALCULATES SATURATION to Google's own default "TonalSpot" style target, which
 * measurably drifted `--accent` (#383961, chroma 28.5) toward a distinctly more saturated
 * #5355a9. Operator's own direct call, asked and confirmed (AskUserQuestion): keep the brand's
 * own chroma. So each seed goes through `Hct.fromInt(seed)` → `TonalPalette.fromHueAndChroma
 * (hue, chroma)` — the real HCT tone-stepping engine, but locked to the seed's OWN hue and
 * chroma rather than Google's style defaults. (Sanity check this round-trips correctly: the same
 * palette's tone at the seed's own HCT tone value reproduces the exact seed hex, e.g. primary's
 * palette at tone 25.7 = #383961 again.)
 *
 * ── Color roles — each seed's OWN tonal palette, at M3's standard light-scheme tone
 * assignments: main=T40, on-main=T100, container=T90, on-container=T10. ──────────────────────
 * Primary seed: --accent (#383961, "Twilight Indigo", hue 284.8° chroma 28.5). Error seed:
 * --danger (#d31f1e, "Flag Red", hue 25.3° chroma 91.3 — a very high-chroma red; #d31f1e itself
 * was already AA-compliant at 5.27:1 white-on-red, this is not a contrast rescue, it's putting
 * every fill role through the same real tone-stepping rule).
 *
 * ⓘ SUCCESS IS A GENUINE, CUSTOM ROLE — M3 explicitly allows extending its five core roles with
 * more. The existing codebase choice ("`--green` is aliased to indigo, `--text` colour reused
 * for a hard-qualifier VERDICT class" — literally commented `success = indigo, never green`
 * elsewhere) is NOT touched by this — that quirk stays exactly as it is everywhere it already
 * ships. `--md-success` is additive, seed #146c43 (hue 159.5° chroma 41.9), for newly-restyled
 * Best Bets components only (the qualifier chip, card 3 of the CSS-only pilot, #794).
 *
 * ── Neutral/surface — Google's own FIXED M3 rule (not a "style" choice like accent chroma):
 * `CorePalette.of(primary seed)`'s n1 (neutral)/n2 (neutral-variant) palettes, at M3's standard
 * surface tone assignments (surface=T98, surface-container-low=T96, surface-container=T94,
 * surface-container-high=T92, on-surface=T10, on-surface-variant=T30, outline=T50,
 * outline-variant=T80). This is why Best Bets' surfaces carry a faint, deliberate cool-violet
 * cast rather than being flat neutral grey — that's the real M3 algorithm's own behaviour, tying
 * even "grey" surfaces back to the brand hue at very low chroma. ──────────────────────────────
 */
:root {
  /* primary — Option 1 Neutral Zinc: deep slate navy with zero purple cast */
  --md-primary: #3b4b68;
  --md-on-primary: #ffffff;             /* vs --md-primary: 8.78:1 (clears WCAG AA 4.5:1) */
  --md-primary-container: #dde3ed;
  --md-on-primary-container: #141c2b;   /* vs --md-primary-container: 13.23:1 (clears WCAG AA 4.5:1) */

  /* error — seed #d31f1e, hue 25.3° chroma 91.3 (brand chroma preserved) */
  --md-error: #bf0812;
  --md-on-error: #ffffff;               /* vs --md-error T40: 6.44:1 */
  --md-error-container: #ffdad5;
  --md-on-error-container: #410001;     /* vs --md-error-container T90: 13.26:1 */

  /* success — a genuine new role, seed #146c43, hue 159.5° chroma 41.9 (a real green, not
     --green's indigo alias) — its own HCT tone (40.0) happens to already equal T40, so the main
     fill IS the seed hex unchanged */
  --md-success: #146c43;
  --md-on-success: #ffffff;             /* vs --md-success T40: 6.45:1 */
  --md-success-container: #a2f4c0;
  --md-on-success-container: #002110;   /* vs --md-success-container T90: 13.26:1 */

  /* neutral/surface — Option 1 Neutral Zinc: clean monochrome greys with zero tint */
  --md-surface: #fafafa;                /* pure clean neutral background */
  --md-surface-container-low: #f4f4f5;  /* zinc-100; header / rail / footer ground */
  --md-surface-container: #ededf0;      /* clean neutral card slab */
  --md-surface-container-high: #e4e4e7; /* zinc-200; search field & hover surface */
  --md-on-surface: #18181b;             /* zinc-900; vs --md-surface-container: 15.16:1 */
  --md-on-surface-variant: #52525b;     /* zinc-600; vs --md-surface-container: 6.62:1,
                                            vs --md-surface: 7.41:1 */
  --md-outline: #a1a1aa;                /* zinc-400 — borders & separators */
  --md-outline-variant: #d4d4d8;        /* zinc-300 — subtle divider */

  /* ── Type scale — ALIASES onto the existing 8-step --fs-* scale (vendor/styles.css:128-135),
   * not a parallel set of numbers. Formalises --fs-md, which is CONSUMED with a fallback
   * (src/style.css, `var(--fs-md, 1rem)`) but was never actually defined anywhere until now. */
  --fs-md: 1rem;
  --md-display: var(--fs-h1);
  --md-headline: var(--fs-h2);
  --md-title-large: var(--fs-h3);
  --md-title-medium: var(--fs-h4);
  --md-body-large: var(--fs-md);
  --md-body-medium: var(--fs-body);
  --md-label-large: var(--fs-sm);
  --md-label-medium: var(--fs-xs);
  --md-label-small: var(--fs-2xs);

  /* ── Shape scale — M3's own named steps. Formally replaces the phantom --radius-sm, which was
   * CONSUMED with inconsistent fallbacks (6px/8px/5px/4px) across both stylesheets but never
   * actually defined anywhere. */
  --md-shape-none: 0;
  --md-shape-xs: 4px;
  --md-shape-sm: 8px;
  --md-shape-md: 12px;
  --md-shape-lg: 16px;
  --md-shape-xl: 28px;
  --md-shape-full: 999px;

  /* ── Spacing scale — 4px grid, M3's own convention. Scoped to Best Bets' own rules in cards
   * 2-4; not a sitewide replacement of every ad-hoc rem literal until this pilot is proven. */
  --md-space-1: 4px;
  --md-space-2: 8px;
  --md-space-3: 12px;
  --md-space-4: 16px;
  --md-space-5: 20px;
  --md-space-6: 24px;
  --md-space-7: 28px;
  --md-space-8: 32px;

  /* ── Elevation — M3's "surface tint overlay" model (a tonal tint, not shadow-alone), levels
   * 0-3. `color-mix()` is already used once in this codebase (card #789's focus-ring hover
   * state) — modern CSS, zero dependency, exactly the mechanism this needs. Levels 4-5 are not
   * defined: nothing at this component scale (a card, a button) needs them. */
  --md-elevation-0: none;
  --md-elevation-1: 0 1px 2px rgba(0, 0, 0, 0.08), 0 1px 3px 1px rgba(0, 0, 0, 0.05);
  --md-elevation-2: 0 1px 2px rgba(0, 0, 0, 0.10), 0 2px 6px 2px rgba(0, 0, 0, 0.07);
  --md-elevation-3: 0 1px 3px rgba(0, 0, 0, 0.12), 0 4px 8px 3px rgba(0, 0, 0, 0.08);
  --md-surface-tint-1: color-mix(in srgb, var(--md-primary) 5%, var(--md-surface-container));
  --md-surface-tint-2: color-mix(in srgb, var(--md-primary) 8%, var(--md-surface-container));
  --md-surface-tint-3: color-mix(in srgb, var(--md-primary) 11%, var(--md-surface-container));

  /* ── Motion — M3's standard easing curve, two durations. Small, purposeful transitions
   * (a card's elevation on hover, an accordion opening) — not a sitewide animation pass. */
  --md-motion-short: 150ms;
  --md-motion-medium: 250ms;
  --md-motion-standard: cubic-bezier(0.2, 0, 0, 1);
}

/* ⚠⚠ CARD #798 — THE `--md-sys-color-*` BRIDGE. Real `@material/web` components (vendored as
 * `src/vendor/mwc-bundle.js`, see PROVENANCE.md) read their theme from CSS custom properties
 * named `--md-sys-color-*` — this is the library's own, fixed naming convention, not something
 * this codebase invented. Custom properties pierce Shadow DOM natively (unlike normal CSS
 * rules), so declaring these once at `:root` is enough to theme every MWC component anywhere in
 * the page, with zero Shadow-DOM-piercing tricks needed.
 *
 * ⚠ THIS IS A BRIDGE, NOT A SECOND SOURCE OF TRUTH. Every value here is `var(--md-*)`, aliasing
 * the real role tokens cards #792/#797 already computed with Google's own colour science — the
 * colour work already done is not redone, only exposed under the name MWC's own components
 * expect. Change a brand colour by editing the `--md-primary` etc. block above; this section
 * should never need its own literal value.
 *
 * ⓘ Not every `--md-sys-color-*` role MWC defines has a bridge here — only the ones this pilot's
 * seven components (buttons, checkbox, chips, tabs, labs card, labs segmented-button) actually
 * read. `docs/components/` in the vendored package (not shipped, dev-only reference) lists the
 * full role set if a later card needs to extend this.
 */
:root {
  --md-sys-color-primary: var(--md-primary);
  --md-sys-color-on-primary: var(--md-on-primary);
  --md-sys-color-primary-container: var(--md-primary-container);
  --md-sys-color-on-primary-container: var(--md-on-primary-container);
  --md-sys-color-secondary: var(--md-primary);
  --md-sys-color-on-secondary: var(--md-on-primary);
  --md-sys-color-secondary-container: var(--md-primary-container);
  --md-sys-color-on-secondary-container: var(--md-on-primary-container);
  --md-sys-color-error: var(--md-error);
  --md-sys-color-on-error: var(--md-on-error);
  --md-sys-color-error-container: var(--md-error-container);
  --md-sys-color-on-error-container: var(--md-on-error-container);
  --md-sys-color-surface: var(--md-surface);
  --md-sys-color-surface-container-low: var(--md-surface-container-low);
  --md-sys-color-surface-container: var(--md-surface-container);
  --md-sys-color-surface-container-high: var(--md-surface-container-high);
  --md-sys-color-on-surface: var(--md-on-surface);
  --md-sys-color-on-surface-variant: var(--md-on-surface-variant);
  --md-sys-color-outline: var(--md-outline);
  --md-sys-color-outline-variant: var(--md-outline-variant);
}

/* ⚠⚠ CARD #861 — `.tn-drop-menu` NOW SITS ON A REAL `<md-menu>`, AND MUST NOT BE `display:none`.
 * `src/vendor/styles.css` still carries `.tn-drop-menu { display: none; }` / `.tn-drop-menu.tn-
 * drop-open { display: block; }` — the OLD hidden-by-CSS-class toggle #861 replaced with the real
 * component's own `open` PROPERTY. That old pair is not simply obsolete, it is actively harmful
 * now: `<md-menu>`'s own shadow style is `:host { display: contents; }`, but the vendor sheet's
 * unconditional `display: none` — loaded first, equal specificity, source order decides the tie —
 * wins over it, and since #861 never adds `tn-drop-open` (or any class) to signal open state
 * any more, the group panel would never render, at any viewport, on any page, `data-m3` or not.
 * PROVENANCE.md is explicit that a layout defect in the carried sheet gets fixed by changing
 * markup/classes, never by hand-editing that file — so the fix lives here, ungated (not
 * `body[data-m3="true"]`, unlike the rest of this menu's M3 skin below): admin pages load
 * `mwc-bundle.js` and get this same real `<md-menu>` without opting into `data-m3` chrome, so an
 * override gated on that attribute would leave admin's own group menus permanently invisible.
 */
.tn-drop-menu { display: contents; }

/* ⚠ CARD #842 — account.html's panels (plan, profile, preferences, data, leave, billing, and the
 * unavailable/disabled states) as real `<md-elevated-card>`. Static info/form panels, not
 * clickable rows, so no hover-elevation treatment — same reasoning as `.archive-card`. */
.acct-card {
  --md-elevated-card-container-color: var(--md-surface-container);
  --md-elevated-card-container-shape: var(--md-shape-lg);
  --md-elevated-card-container-elevation: 1;
  padding: var(--md-space-4);
}
/* ⚠ CARD #844 — app.html's two record panels (the record, today so far) as real
 * <md-elevated-card>. margin-bottom replaces the removed .landing-section rule of the same value
 * — these are plain sibling elements in <main>, not a JS-rendered list, so there is no shared
 * flex/gap parent to carry the spacing instead. */
.app-card {
  --md-elevated-card-container-color: var(--md-surface-container);
  --md-elevated-card-container-shape: var(--md-shape-lg);
  --md-elevated-card-container-elevation: 1;
  padding: var(--md-space-4);
  margin-bottom: 1rem;
}
/* ⚠ CARD #845 — index.html's day board (renderDay()'s three return paths — empty, filtered-empty,
 * and the real matrix) as a real <md-elevated-card>. max-width/box-sizing match .qd-card: the
 * race matrix is wide and already manages its own horizontal scroll (.race-matrix in
 * vendor/styles.css), so the card must not fight it for width. */
.day-card {
  --md-elevated-card-container-color: var(--md-surface-container);
  --md-elevated-card-container-shape: var(--md-shape-lg);
  --md-elevated-card-container-elevation: 1;
  padding: var(--md-space-4);
  max-width: 100%;
  box-sizing: border-box;
}
/* ⚠ CARD #850 — renderRace()'s outer wrapper as a real <md-elevated-card>. */
.race-card {
  --md-elevated-card-container-color: var(--md-surface-container);
  --md-elevated-card-container-shape: var(--md-shape-lg);
  --md-elevated-card-container-elevation: 1;
  padding: var(--md-space-4);
}
/* ⚠ CARD #852 — renderNext()'s outer wrapper as a real <md-elevated-card>. */
.next-card {
  --md-elevated-card-container-color: var(--md-surface-container);
  --md-elevated-card-container-shape: var(--md-shape-lg);
  --md-elevated-card-container-elevation: 1;
  padding: var(--md-space-4);
}
/* ⚠ CARD #853 — renderTrack()'s outer wrapper as a real <md-elevated-card>. */
.track-card {
  --md-elevated-card-container-color: var(--md-surface-container);
  --md-elevated-card-container-shape: var(--md-shape-lg);
  --md-elevated-card-container-elevation: 1;
  padding: var(--md-space-4);
}
/* ⚠ CARD #854 — renderResults()'s outer wrapper as a real <md-elevated-card>. */
.results-card {
  --md-elevated-card-container-color: var(--md-surface-container);
  --md-elevated-card-container-shape: var(--md-shape-lg);
  --md-elevated-card-container-elevation: 1;
  padding: var(--md-space-4);
}
/* ⚠ CARD #855 — renderMeet()'s outer wrapper as a real <md-elevated-card>. */
.meet-card {
  --md-elevated-card-container-color: var(--md-surface-container);
  --md-elevated-card-container-shape: var(--md-shape-lg);
  --md-elevated-card-container-elevation: 1;
  padding: var(--md-space-4);
}
/* ⚠ CARD #856 — renderHorse()'s three return shapes as real <md-elevated-card>: the single
 * form-state card (denied/unknown/empty), and the two sibling form/predictions cards in the
 * real-content branch. margin-bottom replaces the removed .home-section-primary's own 1.25rem
 * (vendor/styles.css:2895-2897) — these are two sibling elements, not a JS-rendered list, so
 * there is no shared flex/gap parent to carry the spacing instead. */
.horse-card {
  --md-elevated-card-container-color: var(--md-surface-container);
  --md-elevated-card-container-shape: var(--md-shape-lg);
  --md-elevated-card-container-elevation: 1;
  padding: var(--md-space-4);
  margin-bottom: 1.25rem;
}
