:root {
  /* Green-grey dark theme — the night inverse of the sage light theme. Neutrals carry a green
     cast (G is the dominant channel, R the lowest), matching the light theme's G>B>R ordering so
     the green runs consistently across both. */
  --bg: #0e130f;
  --panel: #171e19;
  --panel-2: #1d261f;
  --line: #2b382f;
  --text: #e6ebe7;
  --muted: #8b988f;
  /* --muted, one step up, for text sitting on the tinted --pos-soft / --neg-soft grounds.
     #8b988f reads 4.08:1 on --pos-soft, which is body text under the AA floor. Nothing else on
     the site puts muted text on a tinted panel, so this stays scoped rather than moving --muted
     and repainting every page. */
  --muted-on-soft: #93a097;
  /* Thin-history figures. A fixed colour, not opacity: opacity blends toward the row
     background, so one value cannot hold contrast across both themes. .62 over the
     light board row landed near 2:1, well under the 4.5:1 AA floor at 10px. */
  --prob-thin: #808e86;
  /* Primary = vibrant green (the Live/win colour); secondary = blue */
  --accent: #34d399;
  --accent-2: #10b981;
  --accent-rgb: 52,211,153;
  --blue: #3b82f6;
  --blue-2: #2563eb;
  --blue-rgb: 59,130,246;
  --ok: #34d399;
  --warn: #fbbf24;
  /* Links in prose. Gold, and its own token rather than --warn: a link is not a warning,
     and the two must be free to move apart. */
  --link: #fbbf24;
  --neg: #f87171;
  --pos: #34d399;
  /* Soft badge fills. Flat colours rather than `rgba(hue, .16)` over the panel, because an
     alpha tint blends toward whatever is behind it and one alpha cannot hold 4.5:1 in both
     themes. These dark values ARE the current appearance, computed from the tints they replace,
     so nothing moved here; the light theme is what was broken. Every pair is measured in
     tests/test_colour_contrast.py.

     --info and --special exist because a status pill needs more than win, loss and warning:
     blue for a trial or a neutral note, violet for a comp. They were hardcoded per template. */
  --pos-soft: #1c3b2d;
  --neg-soft: #3b2b27;
  --warn-soft: #3b381b;
  --info: #38bdf8;
  --info-soft: #1c373d;
  --special: #c084fc;
  --special-soft: #322e3d;
  /* Write-ups line on the results chart. Violet: green and red are taken by the P&L line it
     overlays, and blue by the per-market line. */
  --wu-line: #a78bfa;

  /* Scores board surfaces: the league title blends into the table panel; match rows are the
     visible alternating cards (a = lighter, b = darker). */
  --lg-band: #171d19; --board-row-a: #1d261f; --board-row-b: #19221b;

  /* ── Layout system ──────────────────────────────────────────────────────────
     ONE content frame for every page. Pages must not set their own max-width /
     padding / centering — they get it from `main` (or the `.reading` variant for
     long-form text). Two deliberate widths only: data-dense app pages and reading
     pages. `main` is a fixed centered column, so future side-rail content lives in
     the gutters and can never change the content width. */
  --content-max: 1200px;   /* every page — content area is 1160 (max minus the 20px gutters) */
  --reading-max: var(--content-max);   /* one width everywhere; kept as a hook for future long-form */
  --page-pad-x: 20px;      /* horizontal gutter — desktop & tablet */
  --page-pad-x-sm: 16px;   /* horizontal gutter — mobile */
  --page-gap: 16px;        /* vertical rhythm between stacked sections */
  --page-top: 22px;        /* space under the sticky topbar */
  --rail: 180px;           /* reserved side-rail width (ads/widgets/insights) */
}
:root[data-theme="light"] {
  /* Sage grey-green light theme — a clear green-grey cast (the daylight inverse of the
     cool near-black dark theme), deliberately not white. */
  --bg: #cbd8d1;
  --panel: #dbe4df;
  --panel-2: #d0dbd4;
  --line: #b3c3bb;
  --text: #1b2726;
  --muted: #4f5f5b;
  --muted-on-soft: #4f5f5b;   /* light theme clears 6:1 on both soft grounds already */
  --prob-thin: #52625d;
  --accent: #15803d;
  --accent-2: #166534;
  --accent-rgb: 21,128,61;
  --blue: #2563eb;
  --blue-2: #1d4ed8;
  --blue-rgb: 37,99,235;
  --ok: #15803d;
  --warn: #964409;
  /* Amber on a near-white page is unreadable, so light mode gets a deep bronze instead: same
     gold family, contrast that actually passes on --bg. */
  --link: #815207;
  --neg: #b12323;
  --pos: #156a32;
  /* Pale tints, the hue at 10% over white. The badges were shipping the DARK text colour on a
     dark tint over a light panel: a won badge measured 1.37:1 and a paid pill 1.21:1, which is
     text you cannot read at all. These clear 5.5:1 to 6.4:1. */
  --pos-soft: #ebfbf5;
  --neg-soft: #fef1f1;
  --warn-soft: #fff9e9;
  --info: #0369a1;
  --info-soft: #ebf8fe;
  --special: #7e22ce;
  --special-soft: #f9f3ff;
  --wu-line: #6d28d9;
  /* League blends into the light table panel; rows alternate around it. */
  --lg-band: #d9e2dd; --board-row-a: #e7eee9; --board-row-b: #d1dcd5;
}
:root[data-theme="light"] body { background: var(--bg); }
:root[data-theme="light"] .card {
  box-shadow: 0 1px 2px rgba(30,42,42,.05), 0 1px 3px rgba(30,42,42,.07);
  border-color: var(--line);
}
:root[data-theme="light"] .stat {
  background: #d3ded7; border-color: var(--line);
}
/* Alternating match-row bands (a / b / a / b). Band every cell EXCEPT the green chance blocks
   (prob-hi/mid), so uncolored chance cells match the row instead of the dark table base. */
table.board tr.match-row.row-b td:not(.prob-hi):not(.prob-mid) { background: var(--board-row-b); }
:root[data-theme="light"] .topbar {
  box-shadow: 0 1px 3px rgba(30,42,42,.06);
}
:root[data-theme="light"] table.board tr.match-row td { background: var(--board-row-a); }
/* Chance % colour blocks on light: the old .12 mid was invisible on the pale surface. Stronger
   green + a hair of border so both tiers read clearly. Covers desktop AND the mobile card cells
   (the extra :root/[data-theme] specificity beats the mobile-layout rules). */
:root[data-theme="light"] table.board td.prob-hi,
:root[data-theme="light"] table.board tr.match-row td.c-p.prob-hi { background: rgba(21,128,61,.92); color: #fff; }
:root[data-theme="light"] table.board td.prob-mid,
:root[data-theme="light"] table.board tr.match-row td.c-p.prob-mid { background: rgba(21,128,61,.24); }
:root[data-theme="light"] .panel-2, :root[data-theme="light"] .money-input,
:root[data-theme="light"] .league-search { background: var(--panel-2); }
:root[data-theme="light"] .btn.ghost:hover { background: rgba(37,99,235,.06); }
:root[data-theme="light"] .nav-btn:hover { color: var(--accent); }

* { box-sizing: border-box; }
/* `clip`, not `hidden`: hidden forces the other axis to compute to `auto`, turning html/body
   into scroll containers that capture position:sticky (topbar + board headers) and make them
   scroll away. clip contains stray horizontal width without creating a scroll container. */
html { overflow-x: clip; }
body {
  margin: 0;
  font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
  overflow-x: clip;
}
/* Sticky footer, site-wide. The shell lived inline in dashboard.html, so on every other page
   the footer sat directly under the last element instead of at the bottom of the window: on a
   short page (a legal notice, an empty results day) it floated halfway up with dead background
   beneath it.

   EXCLUDES .login-body, and that exclusion is load-bearing rather than tidiness. reCAPTCHA
   appends its image-challenge overlay as a direct child of <body>; with a flex body that
   wrapper becomes a flex ITEM and gets centred and sized by the layout instead of overlaying
   the page. The challenge then exists in the DOM but is invisible behind the card, which turns
   the "I'm not a robot" tick into a dead end and blocks signup entirely. That was a real
   outage; see the .login-body note further down. Those pages get their height from
   .login-shell instead.

   flex: 1 0 auto, not flex: 1. Basis auto means main is at least as tall as its content and
   grows to fill what is left, so it cannot collapse. It creates no scroll container either,
   which the board depends on: an overflow on main would capture its sticky column headers. */
body:not(.login-body) { display: flex; flex-direction: column; min-height: 100vh; }
body:not(.login-body) > main { flex: 1 0 auto; }
/* Mobile browser chrome makes 100vh taller than the visible area, so the footer hides just off
   the bottom until the address bar collapses. dvh is the visible height; the vh line above
   stays as the fallback for anything that does not support it. */
@supports (min-height: 100dvh) {
  body:not(.login-body) { min-height: 100dvh; }
}
/* The auth pages do the same thing one level in, because their <body> has to stay block. */
.page-shell { display: flex; flex-direction: column; min-height: 100vh; }
.page-shell > .login-shell { flex: 1 0 auto; padding: 20px; box-sizing: border-box; }
@supports (min-height: 100dvh) { .page-shell { min-height: 100dvh; } }

.muted { color: var(--muted); }
.small { font-size: 12px; }
/* Sits under the signup button. The button now sells the trial rather than the account, and the
   two objections a visitor has at that exact moment (does this cost me, does it end) are cheaper
   to answer here than to make them open the pricing page. */
.cta-sub { text-align: center; margin: 8px 0 0; }
code { background: var(--panel-2); padding: 1px 5px; border-radius: 4px; }

/* ── login ── */
/* NOT a flex container, deliberately. reCAPTCHA appends its image-challenge overlay as a direct
   child of <body>, and the wrapper Google injects is statically positioned — so with a flex body
   it became a flex item and got centred and sized by the layout instead of overlaying the page.
   The result was a challenge that existed in the DOM but was invisible behind the card, which
   made the "I'm not a human" tick a dead end and blocked signup entirely.
   Centring now happens on .login-shell, an element we own, so anything a third-party script
   appends to <body> is left alone. */
/* No padding here any more. It framed the card back when the card WAS the page; now that a
   header and footer sit inside .page-shell, body padding is added on top of a 100vh shell and
   pushes the whole document 40px past the viewport. The gutter moved onto the shell, where it
   pads the card without adding to the page height. */
.login-body { min-height: 100vh; }
.login-shell {
  /* No min-height any more. It was calc(100vh - 40px), correct when the card WAS the page, and
     with a header and footer around it that made the document taller than the window and pushed
     the footer off the bottom. .page-shell now hands this element whatever height is left, so
     the card centres in it and the footer lands on the bottom edge at any viewport. */
  display: flex; align-items: center; justify-content: center;
}
.login-card {
  width: min(320px, 100%); background: var(--panel); border: 1px solid var(--line);
  border-radius: 14px; padding: 28px; display: flex; flex-direction: column; gap: 12px;
  box-shadow: 0 20px 50px rgba(0,0,0,.4);
}
/* Signup is a different shape of page from sign-in and needs a different width. 320px suits two
   fields; signup has ten, plus a club picker, plus three consent lines that wrap to three rows
   each at that width. The binding constraint is reCAPTCHA: its widget is a fixed 304px iframe, so
   at 320px with 28px padding it was 40px wider than the space it had. That is the crop.
   480px leaves the widget room and lets every consent line sit on one or two rows. */
.login-card.signup-card { width: min(480px, 100%); padding: 32px; gap: 14px; }
@media (max-width: 560px) {
  /* Below the card's own width the gutter is all that matters, and 32px of it on each side eats a
     third of a small phone. */
  .login-card.signup-card { padding: 22px; }
}
.login-card .brand { font-size: 22px; font-weight: 800; display: inline-flex; align-items: center; gap: 9px; }
/* The password-reset cards carry a heading; the sign-in card does not. Sized against the brand
   line above it rather than the browser default, which rendered larger than the wordmark. */
.login-card .auth-h1 { font-size: 19px; font-weight: 700; margin: 4px 0 2px; line-height: 1.25; }
.login-card .brand .brand-logo { width: 30px; height: 30px; }
.login-card label { display: flex; flex-direction: column; gap: 5px; font-size: 13px; color: var(--muted); }
.login-card input[type="text"],
.login-card input[type="email"],
.login-card input[type="password"] {
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  border-radius: 8px; padding: 10px 12px; font-size: 15px;
  transition: border-color .15s, background .15s; font-weight: 500;
}
.login-card input:focus { outline: none; border-color: var(--accent); }
.login-card input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.login-card .hint { text-align: center; margin: 4px 0 0; }
.login-card .checkbox-label {
  display: flex; flex-direction: row; align-items: flex-start; gap: 8px;
  font-size: 12px; color: var(--text); margin-top: 4px;
}
.login-card .checkbox-label input[type="checkbox"] {
  margin-top: 3px; flex-shrink: 0; cursor: pointer;
}
.login-card .checkbox-label span { flex: 1; }
/* The widget is a Google iframe, so its interior cannot be styled from here. Two things are
   still ours. The theme is passed at render time by signup-validate.js, which is what turns the
   white block dark; the widget's own frame is then clipped to the card's corner radius and given
   the card's border, so it reads as one of the fields rather than something pasted on top.
   Reserved height, because the iframe arrives late and the button used to jump. */
.recaptcha-wrapper { margin: 8px 0; text-align: center; }
.recaptcha-wrapper .g-recaptcha {
  display: inline-block; margin-bottom: 8px; min-height: 78px;
  border-radius: 8px; overflow: hidden; border: 1px solid var(--line);
  line-height: 0;
}
.recaptcha-wrapper .g-recaptcha iframe { border-radius: 8px; }
.recaptcha-wrapper.has-error .g-recaptcha { border-color: var(--neg); }
.recaptcha-wrapper p { margin: 4px 0; }
/* The widget is a fixed 304px iframe and Google gives no way to make it narrower, so on a phone it
   is wider than the card holding it and hangs out of the side. With the overflow:hidden above
   rounding its corners, that hanging edge is CLIPPED. That is the crop.
   --rc-scale is set by signup-validate.js, which measures the wrapper. It cannot be done in CSS:
   scale() needs a unitless number and calc((100vw - 84px) / 304) is a length divided by a number,
   so the whole declaration is dropped and nothing happens. Putting it in the script is also where
   it belongs, because the script is what renders the widget in the first place -- with no JS there
   is no captcha to fit.
   transform-origin is top left so the shrink happens into the space that exists, not around a
   centre already outside the card. The layout height stays 78px so overflow:hidden keeps clipping
   the real box rather than the scaled one, and the negative margin takes back the gap the shrink
   leaves underneath. */
.recaptcha-wrapper.is-scaled { text-align: left; }
.recaptcha-wrapper.is-scaled .g-recaptcha {
  transform: scale(var(--rc-scale));
  transform-origin: top left;
  margin-bottom: calc(8px - 78px * (1 - var(--rc-scale)));
}
.error {
  background: rgba(248,113,113,.12); color: var(--neg); padding: 12px 14px; border-radius: 8px;
  font-size: 13px; border: 1px solid rgba(248,113,113,.3); font-weight: 500; margin-bottom: 12px;
}

/* ── per-field validation + live password feedback (signup) ──
   A failed submit used to be one banner and an empty form. Now the offending field says what is
   wrong, in place, and keeps what was typed. Colour is never the only signal: every marked field
   also carries text, because red alone is invisible to a good proportion of readers. */
.login-card label.has-error > input,
.login-card label.has-error .pw-wrap > input { border-color: var(--neg); background: rgba(248,113,113,.07); }
.login-card label.has-error { color: var(--neg); }
.field-error { color: var(--neg); font-size: 12px; font-weight: 500; line-height: 1.4; }
.login-card .checkbox-label.has-error { color: var(--neg); }
.login-card .checkbox-label.has-error input[type="checkbox"] { outline: 2px solid var(--neg); outline-offset: 2px; }

.pw-wrap { position: relative; display: flex; }
/* min-width:0, or the field will not shrink below an <input>'s intrinsic width (roughly its
   default size of 20 characters) however narrow the card gets. A flex item defaults to
   min-width:auto, which is that intrinsic width, so on a 320px phone both password boxes stood
   7px proud of the card. */
.pw-wrap > input { flex: 1; min-width: 0; padding-right: 62px !important; }
.pw-toggle {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  background: none; border: 0; color: var(--muted); cursor: pointer;
  font-size: 12px; font-weight: 600; padding: 4px 8px; border-radius: 6px;
}
.pw-toggle:hover { color: var(--text); background: var(--panel-2); }
.pw-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.pw-meter { display: block; height: 4px; border-radius: 999px; background: var(--line); overflow: hidden; }
.pw-meter > i { display: block; height: 100%; width: 0; border-radius: 999px; transition: width .18s ease, background .18s ease; }
.pw-meter[data-level="0"] > i, .pw-meter[data-level="1"] > i { background: var(--neg); }
.pw-meter[data-level="2"] > i { background: var(--warn); }
.pw-meter[data-level="3"] > i, .pw-meter[data-level="4"] > i { background: var(--accent); }
.pw-strength { min-height: 15px; }

.pw-rules { list-style: none; margin: 2px 0 0; padding: 0; display: grid; gap: 3px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.pw-rules li { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
.pw-rules li .pw-tick {
  width: 13px; height: 13px; border-radius: 50%; flex-shrink: 0;
  border: 1.5px solid var(--line); position: relative;
}
.pw-rules li.ok { color: var(--accent); }
.pw-rules li.ok .pw-tick { border-color: var(--accent); background: var(--accent); }
/* Drawn rather than a glyph so it inherits the panel colour and stays crisp at any zoom. */
.pw-rules li.ok .pw-tick::after {
  content: ""; position: absolute; left: 4px; top: 1px; width: 3px; height: 7px;
  border: solid var(--bg); border-width: 0 2px 2px 0; transform: rotate(45deg);
}

/* ── buttons ── */
.btn {
  appearance: none; border: 1px solid var(--line); background: var(--panel-2);
  color: var(--text); padding: 9px 16px; border-radius: 9px; font-size: 14px;
  cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; gap: 6px;
  transition: filter .15s, background .15s; font-weight: 500;
}
.btn:hover { filter: brightness(1.12); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn:disabled { opacity: .55; cursor: default; }
@media (max-width: 640px) {
  .btn { padding: 11px 14px; font-size: 13px; min-height: 44px; }
}
.btn.primary { background: var(--accent); border-color: var(--accent-2); color: #fff; font-weight: 600; }
.btn.warn-btn { background: rgba(251,191,36,.16); border-color: var(--warn); color: var(--warn); font-weight: 600; }
.btn.ghost { background: transparent; }

/* Present for a screen reader and a crawler, absent for everyone else. Clip rather than
   `display:none` or `visibility:hidden`, both of which remove it from the accessibility tree as
   well as the page and would defeat the point. Not `aria-hidden` either: that hides it from
   assistive tech and does not stop text extraction, which is backwards on both counts. */
.a11y-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ── skip link ──
   Off-canvas until it takes focus, then it lands in the top-left over everything. Moved with
   `top`, NOT `display:none` or `visibility:hidden`, because both of those take it out of the tab
   order and it would never receive focus in the first place. */
.skip-link {
  position: fixed; top: -100px; left: 12px; z-index: 200;
  padding: 10px 16px; border-radius: 0 0 8px 8px;
  background: var(--accent); color: #fff; font-weight: 600; text-decoration: none;
  transition: top .12s ease-out;
}
.skip-link:focus { top: 0; outline: 2px solid var(--text); outline-offset: 2px; }

/* ── topbar ── */
.topbar {
  display: flex; align-items: center; gap: 20px; flex-wrap: wrap; row-gap: 10px;
  /* Desktop keeps everything on ONE row. With wrap on, the theme toggle is the last flex item,
     so as soon as the nav got wide (more nav entries for a signed-in user, or a narrower
     window) the toggle dropped to a second row and the bar grew. Below 640px the mobile
     dropdown takes over and wrapping is fine, so this is scoped to desktop only. */
  /* Full-bleed bar, but its content aligns to the same centred frame as the page body:
     the logo sits at the content's left edge, the toggle at its right edge. */
  padding: 14px max(var(--page-pad-x), calc((100% - var(--content-max)) / 2 + var(--page-pad-x)));
  background: var(--panel); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 50;
}
@media (min-width: 641px) {
  .topbar { flex-wrap: nowrap; }
  /* The nav is the only part allowed to give way; min-width:0 lets a flex item shrink below
     its content width, which it will not do by default. */
  .topbar .nav { min-width: 0; }
  .topbar .brand, .topbar .theme-toggle { flex-shrink: 0; }
}
/* Base (desktop) topbar element rules — defined BEFORE the mobile override so the
   media query at the end wins by source order. */
/* The brand is one link, bird and wordmark together, and its HIT AREA has to match what a
   visitor reads as "the logo". Both were already inside the anchor, but the anchor was only as
   tall as the 28px wordmark while the header band is roughly twice that, so the pointer
   appeared and disappeared as the mouse drifted a few pixels above or below the letters. It
   read as an intermittently clickable title.

   align-self: stretch makes the anchor fill the header's full height, and the padding adds
   slack to either side of the text. The bird's own negative margin still hangs the legs below
   the bar; that is unaffected, because this changes the anchor's box, not the image's. */
.topbar .brand { font-size: 18px; font-weight: 700; flex-shrink: 0; display: inline-flex;
  align-items: center; align-self: stretch; gap: 16px; padding: 4px 10px 4px 0;
  color: var(--text); text-decoration: none; }
.topbar .brand:hover { color: var(--text); }
/* Nothing moves on hover; this is feedback that the whole area is the link, not just the text.
   The wordmark is a three-colour mark, so it cannot take `color` the way the old text did —
   a fill swap would flatten it. A slight lift reads as the same affordance and survives both
   themes. */
.topbar .brand:hover .brand-wordmark { opacity: .82; }
.topbar .brand:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px;
  border-radius: 8px; }
.brand-logo { width: 35px; height: 35px; display: block; flex-shrink: 0; }
/* The header mark is the STANDING bird (oddsprey-brand.svg): head + body + legs. The negative
   bottom margin keeps the header's height driven by the head alone, so the legs hang past the
   bar's bottom edge and overlay the page below. The head-only oddsprey-logo.svg stays the
   favicon. Needs the topbar to stay unclipped (it sets no overflow). */
/* The bird STRADDLES the header's bottom border: body in the bar, legs down onto the page.
   The border has to cross at the leg joint, and that joint is a fixed fraction of the artwork
   (~52% of the width above the image's bottom edge), so the pull scales with the bird instead of
   being a constant: 18px of padding to reach the border (14 on .topbar, 4 on .brand) plus about
   0.52 x width to carry the joint down onto it. At 56px that is 18 + 29 = 47.
   The 52% is measured off the render, not read out of the file: the body polygon ends at y=112
   of 148, but the tie and chest hang visibly lower than that, so the joint a reader SEES sits
   well below the joint the path data implies.
   The negative left margin walks the bird out of the content column into the page gutter, so it
   starts to the LEFT of where the page below begins; --page-pad-x is 20px, so -18 leaves 2px.
   align-self overrides the anchor's align-items:center, which would otherwise float it mid-bar.
   Needs the topbar to stay unclipped (it sets no overflow) and the z-index to sit over the page. */
.brand-logo.standing { width: 56px; height: auto; align-self: flex-end;
  margin-left: -18px; margin-bottom: -39px; position: relative; z-index: 1; }

/* Wide screens only: the page is centred, so there is dead gutter to the left of the content
   column. The bird moves into it, centred at two thirds of the way across, and leaves the flow
   so the wordmark keeps sitting exactly on the content's left edge.
   Gated at 1300px because the gutter has to be wide enough to hold it: the gutter is
   (100vw - 1200)/2 + 20, so two thirds of it only clears a 56px bird from about 1300 up. Below
   that the in-flow rule above still applies and the bird just overhangs by 18px.
   It stays inside the <a>, so it is still part of the link; only its box leaves the flow. */
@media (min-width: 1300px) {
  .brand-logo.standing {
    position: absolute;
    left: calc(max(var(--page-pad-x), (100% - var(--content-max)) / 2 + var(--page-pad-x)) * 2 / 3);
    transform: translateX(-50%);
    /* bottom:0 is the header's bottom border. Only the legs hang below it: the body's
       underside is viewBox y=112 of 148, so the drop is 0.38 x width. Any less and the line
       crosses the shoulders, which reads as wading rather than sitting. */
    bottom: -21px;
    margin: 0;
  }
}
/* Themed page mascots (_osprey.html) — the brand bird floated beside each page's heading. */
.osprey-mascot { display: block; max-width: 100%; height: auto; }
.osprey-hero { float: right; width: clamp(96px, 15vw, 150px); margin: 0 0 6px 18px; }
/* Guide mascot: one leaf flips across the spine and back, so the bird reads as actually reading.
   transform-box: view-box puts transform-origin in the SVG's own coordinates, so the pivot can
   sit exactly on the spine (x=58). The leaf fades out on the far side and resets while invisible,
   which is what keeps the loop from snapping back. */
.osprey-guide .book-page {
  transform-box: view-box; transform-origin: 58px 82px;
  animation: osprey-page-turn 5s ease-in-out infinite;
}
@keyframes osprey-page-turn {
  0%       { transform: scaleX(1);  opacity: 0; }
  8%, 45%  { transform: scaleX(1);  opacity: 1; }
  72%      { transform: scaleX(-1); opacity: 1; }
  82%      { transform: scaleX(-1); opacity: 0; }
  100%     { transform: scaleX(1);  opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .osprey-guide .book-page { animation: none; } }
/* The predictions mascot's scrying pearl breathes a faint halo. drop-shadow so the glow hugs
   the sphere rather than its bounding box. */
.osprey-predictions .osprey-pearl { animation: osprey-pearl-glow 3.4s ease-in-out infinite; }
@keyframes osprey-pearl-glow {
  0%, 100% { filter: drop-shadow(0 0 2px rgba(52,211,153,.35)); }
  50%      { filter: drop-shadow(0 0 7px rgba(52,211,153,.8)); }
}
@media (prefers-reduced-motion: reduce) { .osprey-predictions .osprey-pearl { animation: none; } }
.osprey-hero .osprey-mascot { width: 100%; }
@media (max-width: 520px) { .osprey-hero { width: 78px; margin: 0 0 4px 12px; } }
/* No @font-face here any more. The wordmark was the only thing that ever used Archivo Black,
   and it ships as OUTLINES now (see _header.html and scripts/gen_wordmark.py), so the site
   serves no webfont at all: nothing to download, nothing to swap in, and no way for the mark to
   quietly render in Arial Black because a file failed to load. The woff2 is kept at
   scripts/assets/ as the input the generator reads, not as an asset the browser fetches. */
.brand-wordmark { height: 62px; width: auto; display: block; flex-shrink: 0;
  overflow: visible; transition: opacity .12s ease; }
.brand-wordmark .op-ink     { fill: #dbe4df; }
.brand-wordmark .op-amber   { fill: #b45309; }
.brand-wordmark .op-emerald { fill: #047857; }
:root[data-theme="light"] .brand-wordmark .op-ink     { fill: #171e19; }
:root[data-theme="light"] .brand-wordmark .op-amber   { fill: #f59e0b; }
:root[data-theme="light"] .brand-wordmark .op-emerald { fill: #34d399; }
.header-search-nav { display: flex; align-items: center; gap: 12px; margin: 0 auto; flex-shrink: 0; }
.header-search { margin: 0 !important; }
.header-right { display: flex; align-items: center; gap: 12px; margin-left: auto; flex-wrap: wrap; }
.auth-actions { display: flex; align-items: center; gap: 8px; }
.auth-actions .btn { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.auth-actions .btn .icon { flex: 0 0 15px; }

/* Account dialog: one header button opens this, with Sign In / Sign Up tabs. */
.auth-dialog { width: min(92vw, 380px); border: 1px solid var(--line); border-radius: 14px;
  background: var(--panel); color: var(--text); padding: 0; box-shadow: 0 20px 60px rgba(0,0,0,.4); }
.auth-dialog::backdrop { background: rgba(0,0,0,.55); backdrop-filter: blur(2px); }
.auth-dialog-head { display: flex; align-items: center; border-bottom: 1px solid var(--line); }
.auth-tabs { display: flex; flex: 1 1 auto; }
.auth-tab { flex: 1 1 0; appearance: none; background: none; border: 0; cursor: pointer;
  font: inherit; font-weight: 700; font-size: 15px; color: var(--muted); padding: 15px 10px;
  border-bottom: 2px solid transparent; transition: color .15s, border-color .15s; }
.auth-tab.is-active { color: var(--text); border-bottom-color: var(--accent); }
.auth-x { appearance: none; background: none; border: 0; cursor: pointer; color: var(--muted);
  font-size: 24px; line-height: 1; padding: 8px 14px; }
.auth-x:hover { color: var(--text); }
.auth-pane { display: flex; flex-direction: column; gap: 14px; padding: 22px; margin: 0; }
.auth-pane[hidden] { display: none; }

/* The Sign Up pane carries the full signup form, so the dialog has to grow for it. Sized per
   pane rather than once: 460px around two sign-in fields looks like a mistake, and 380px around
   the signup form crops the reCAPTCHA widget, which is a fixed 304px and cannot be asked to be
   narrower. The class is toggled by show() in the header. */
.auth-dialog.is-signup { width: min(96vw, 460px); }
/* Ten fields do not fit a short window. [open] because a <dialog> is display:none when closed and
   a bare `display:flex` here would make it visible on every page. */
.auth-dialog[open] { display: flex; flex-direction: column; max-height: min(88vh, 920px); }
.auth-dialog-head { flex: 0 0 auto; }
.auth-dialog[open] .auth-pane:not([hidden]) { overflow-y: auto; min-height: 0; }
/* Inside the dialog the form keeps .login-card, because every field style is scoped to it, but
   it is not its own card: the dialog is. So the CHROME comes off, or it renders a second border
   and shadow and 28px of padding inside the pane's 22px. */
.auth-pane .login-card.auth-signup-form {
  width: auto; background: none; border: 0; border-radius: 0; padding: 0; box-shadow: none;
  gap: 12px; margin: 0;
}
.auth-signup-form .field-sub { margin: -2px 0 2px; }
.auth-pane.auth-signup { padding-top: 18px; }

/* reCAPTCHA's image challenge is appended to <body>. A dialog opened with showModal() sits in the
   TOP LAYER, above everything in the normal document, so the challenge renders BEHIND the
   backdrop and cannot be clicked: the tick becomes a dead end and signup is impossible. That
   exact failure has already happened here once, when a flex <body> swallowed the same overlay.
   The header moves the challenge into the dialog when it appears; this makes it sit above the
   form once it is there. */
.auth-dialog .g-recaptcha-challenge-host {
  position: fixed; inset: 0; z-index: 10; display: flex;
  align-items: center; justify-content: center; pointer-events: none;
}
.auth-dialog .g-recaptcha-challenge-host > * { pointer-events: auto; }

/* Server-rendered public track record: homepage strip (crawler-visible) + Results page lede. */
.track-strip { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 18px;
  margin: 0 0 16px; padding: 10px 16px; border: 1px solid var(--line); border-radius: 10px;
  background: var(--panel); overflow: hidden;
  text-decoration: none; color: var(--text); font-size: .9rem; transition: border-color .15s; }
.track-strip:hover { border-color: var(--accent); }

/* ── the day's result, above the board ──────────────────────────────────────
   Shown only on a day that is over. Tinted by outcome so the sign reads before the number does,
   on the soft pair rather than the full-strength one: a solid green bar above the scoreboard
   competes with the board's own won/lost colouring for the same glance.

   The caption is not fine print. It is the answer to "why does this say -12 when the scores look
   green", so it holds 4.5:1 rather than dropping a step. On a tinted ground plain --muted does
   not: see --muted-on-soft. */
.day-pnl { margin: 0 0 14px; padding: 10px 14px; border-radius: 10px;
  border: 1px solid var(--line); background: var(--pos-soft); }
.day-pnl.is-down { background: var(--neg-soft); }
.day-pnl.is-wait { background: var(--panel-2); }
.day-pnl.is-wait .day-pnl-net { color: var(--text); font-size: 1rem; }
.day-pnl-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 16px;
  font-size: .9rem; }
.day-pnl-lead { font-weight: 700; flex: 0 0 auto; }
.day-pnl-net { font-size: 1.15rem; font-weight: 800; color: var(--pos);
  font-variant-numeric: tabular-nums; }
.day-pnl.is-down .day-pnl-net { color: var(--neg); }
.day-pnl-stat { color: var(--muted-on-soft); }
.day-pnl-stat b { color: var(--text); font-weight: 700; font-variant-numeric: tabular-nums; }
/* --text with an underline, not --accent. The panel takes three different grounds and the light
   accent (#15803d) is 3.53:1 on --panel-2, under the floor for a 14px link. An underline also
   carries the affordance without relying on colour alone. */
.day-pnl-more { margin-left: auto; flex: 0 0 auto; color: var(--text); font-weight: 600;
  text-decoration: underline; text-underline-offset: 3px; white-space: nowrap; }
.day-pnl-more:hover { color: var(--accent); }
.day-pnl-note { margin: 6px 0 0; max-width: 68ch; color: var(--muted-on-soft); font-size: .8rem;
  line-height: 1.5; }
@media (max-width: 560px) {
  .day-pnl-more { margin-left: 0; flex-basis: 100%; }
}

.track-strip b { color: var(--accent); font-weight: 800; }
.track-strip-lead { font-weight: 700; flex: 0 0 auto; }
.track-strip-since { color: var(--muted); }
.track-strip-arrow { margin-left: auto; color: var(--accent); font-weight: 600; flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
@media (max-width: 560px) { .track-strip-arrow { margin-left: auto; } }

/* A slow accent sweep across the strip. It is the only moving colour on the page above the fold,
   so it stays under 10% opacity: enough to catch the eye on the way past, not enough to compete
   with the board. */
.track-strip::before { content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(100deg, transparent 30%, color-mix(in srgb, var(--accent) 12%, transparent) 50%,
    transparent 70%);
  background-size: 240% 100%; animation: track-sheen 9s linear infinite; }

@keyframes track-sheen { from { background-position: 140% 0; } to { background-position: -40% 0; } }

/* Rotating window. Fixed height because the panels are stacked absolutely — a panel that grew
   taller than its window would be clipped, so every panel stays one non-wrapping line. */
.track-rot { position: relative; flex: 1 1 auto; min-width: 0; height: 1.5rem; overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, #000 82%, transparent 100%);
  mask-image: linear-gradient(90deg, #000 82%, transparent 100%); }
.track-slide { position: absolute; inset: 0; display: flex; align-items: center; gap: 0 14px;
  white-space: nowrap; opacity: 0; transform: translateY(105%);
  transition: transform .55s cubic-bezier(.22,.8,.28,1), opacity .3s ease; }
.track-slide.on { opacity: 1; transform: none; }
.track-slide.out { opacity: 0; transform: translateY(-105%); }
/* Fallback first: where color-mix() is unsupported the declaration is dropped, and a pill with no
   background at all reads as a rendering fault rather than a label. */
.track-per { font-weight: 700; font-size: .78rem; letter-spacing: .02em; text-transform: uppercase;
  padding: 2px 8px; border-radius: 999px; color: var(--accent); background: var(--panel-2);
  background: color-mix(in srgb, var(--accent) 14%, transparent); }
/* Rides WITH the number, not in the subtitle that mobile drops — the qualifier has to survive
   everywhere the figure does. */
.track-thin { font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
  color: var(--muted); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px;
  white-space: nowrap; }
.track-u { color: var(--muted); }
.track-u .u-long { display: inline; }
.track-u .u-short { display: none; }
.track-sub { color: var(--muted); font-size: .82rem; }

/* The right end warms toward the accent so the numbers read as flowing into the link rather than
   stopping dead next to it. */
.track-strip::after { content: ""; position: absolute; top: 0; bottom: 0; right: 0; width: 42%;
  pointer-events: none;
  background: linear-gradient(90deg, transparent,
    color-mix(in srgb, var(--accent) 9%, transparent)); }
.track-strip b.track-up { color: var(--pos); }
.track-strip b.track-down { color: var(--neg); }

/* The chevrons run toward the link, which is where we want the eye to end up. */
.track-chevs { display: inline-flex; gap: 1px; }
.track-chevs i { font-style: normal; opacity: .25; animation: track-chev 1.6s ease-in-out infinite; }
.track-chevs i:nth-child(2) { animation-delay: .16s; }
.track-chevs i:nth-child(3) { animation-delay: .32s; }

@keyframes track-chev {
  0%, 60%, 100% { opacity: .25; transform: translateX(0); }
  30% { opacity: 1; transform: translateX(2px); }
}

/* On a phone the line has to survive at 360px, so everything the panel label already implies is
   dropped: the strip's own title, the date subtitle, and the words on the link. What is left is
   the three numbers, which is what the strip is for. */
@media (max-width: 640px) {
  .track-strip { gap: 6px 10px; padding: 9px 12px; font-size: .82rem; }
  .track-strip-lead, .track-sub, .track-arrow-txt { display: none; }
  .track-u .u-long { display: none; }
  .track-u .u-short { display: inline; }
  .track-slide { gap: 0 9px; }
  .track-per { font-size: .7rem; padding: 2px 6px; }
  .track-strip::after { width: 30%; }
}

@media (prefers-reduced-motion: reduce) {
  .track-strip::before, .track-chevs i { animation: none; }
  .track-chevs i { opacity: .6; }
  .track-slide { transition: none; }
}
.perf-h1 { margin: 0 0 6px; font-size: 1.5rem; }
.track-lede { margin: 0 0 18px; color: var(--muted); font-size: .95rem; }
.track-lede b { color: var(--text); }
.auth-field { display: flex; flex-direction: column; gap: 6px; font-size: 13px; font-weight: 600;
  color: var(--muted); }
.auth-field input { font: inherit; font-weight: 400; color: var(--text); background: var(--panel-2);
  border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; }
.auth-field input:focus { outline: none; border-color: var(--accent); }
.auth-check { display: flex; align-items: flex-start; gap: 8px; font-size: 12px; color: var(--muted);
  line-height: 1.45; }
.auth-check input { margin-top: 2px; flex: 0 0 auto; }
/* Consent links (Terms, Privacy Notice) carry an underline at rest, not only on hover. These sit
   inside a line of small muted body text, where accent colour alone does not read as a link and
   the consent is legal — the reader has to be able to see what they are agreeing to. */
.auth-check a, .checkbox-label a { color: var(--accent); font-weight: 600;
  text-decoration: underline; text-underline-offset: 2px; }
.auth-check a:hover, .checkbox-label a:hover { color: var(--text); }
.auth-submit { width: 100%; justify-content: center; padding: 11px; font-size: 15px; margin-top: 2px; }
.auth-switch { margin: 2px 0 0; text-align: center; font-size: 13px; color: var(--muted); }
.auth-switch a { color: var(--accent); text-decoration: none; }
.auth-switch a:hover { text-decoration: underline; }
/* Rounded-square outlined icon buttons (DataSight style): primary border + fill on hover */
.mobile-menu-btn, .theme-toggle {
  display: none; appearance: none; border: 1.5px solid var(--accent); background: transparent;
  color: var(--accent); cursor: pointer; border-radius: 8px; padding: 8px;
  min-width: 40px; min-height: 40px; align-items: center; justify-content: center;
  transition: background .15s, color .15s, transform .1s;
}
.theme-toggle { display: flex; font-size: 16px; }
.theme-toggle svg { display: block; }
/* The toggle advertises what it DOES, not where you are: dark theme shows the Sun (tap for
   light), light theme shows the Moon (tap for dark). */
.theme-toggle .ti-moon { display: none; }
.theme-toggle .ti-sun  { display: block; }
:root[data-theme="light"] .theme-toggle .ti-sun  { display: none; }
:root[data-theme="light"] .theme-toggle .ti-moon { display: block; }
/* Same convention for the mobile dropdown row, icon and label together. */
.nav-theme .ti-moon, .nav-theme .tt-dark { display: none; }
.nav-theme .ti-sun,  .nav-theme .tt-light { display: block; }
:root[data-theme="light"] .nav-theme .ti-sun,
:root[data-theme="light"] .nav-theme .tt-light { display: none; }
:root[data-theme="light"] .nav-theme .ti-moon,
:root[data-theme="light"] .nav-theme .tt-dark { display: block; }
.mobile-menu-btn:hover, .theme-toggle:hover {
  background: var(--accent); color: var(--bg);
}
/* Per theme: dark → yellow; light → the UI orange #f59e0b (same as the TIP badge / subscribed
   chip) to match the light theme's warm accents. */
.theme-toggle { --toggle-glow: #fbbf23; }
:root[data-theme="light"] .theme-toggle { --toggle-glow: #f59e0b; }
/* The icon is an ACTION, not a status, so it stays a hollow outline at rest and only fills once
   you reach for it — hover fills the shape and adds the glow, while the button itself turns
   green (rule above). stroke is set explicitly rather than inherited so the hover `color` swap
   can't grey it out. The sun's rays stay unfilled in both states; only its core disc fills. */
.theme-toggle .ti-sun, .theme-toggle .ti-moon { stroke: var(--toggle-glow); fill: none; }
.theme-toggle .ti-sun circle { fill: none; }
.theme-toggle .ti-sun, .theme-toggle .ti-moon,
.theme-toggle .ti-sun circle { transition: fill .15s, stroke .15s, filter .15s; }
.theme-toggle:hover .ti-moon { fill: var(--toggle-glow); }
.theme-toggle:hover .ti-sun circle { fill: var(--toggle-glow); }
.theme-toggle:hover .ti-sun, .theme-toggle:hover .ti-moon {
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--toggle-glow) 70%, transparent));
}
.mobile-menu-btn:active, .theme-toggle:active { transform: scale(.94); }
.mobile-menu-btn:focus-visible, .theme-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.theme-toggle #theme-icon { line-height: 1; }
.mobile-nav { display: none; }

/* Nav link hover dots (DataSight): green + blue dots slide in, staggered */
.nav-btn { --dot-shown: 0; }
.nav-hover-dots { display: inline-flex; align-items: center; gap: 2px; margin-left: 5px; }
.nav-hover-dots i {
  width: 5px; height: 5px; border-radius: 50%; opacity: 0;
  transform: scale(.5) translateX(-3px); transition: opacity .15s ease-out, transform .15s ease-out;
}
.nav-hover-dots i.d-green { background: var(--accent); }
.nav-hover-dots i.d-blue { background: var(--warn); }
.nav-btn:hover .nav-hover-dots i.d-green,
.nav-btn.active .nav-hover-dots i.d-green { opacity: 1; transform: scale(1) translateX(0); transition-delay: 0s; }
.nav-btn:hover .nav-hover-dots i.d-blue,
.nav-btn.active .nav-hover-dots i.d-blue { opacity: 1; transform: scale(1) translateX(0); transition-delay: .075s; }
.nav-btn {
  font-size: 14px; display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 8px 12px; border: none; background: transparent; color: var(--muted); cursor: pointer;
  transition: all .15s; position: relative; white-space: nowrap; min-height: 44px; min-width: 44px;
  font-weight: 500; border-radius: 6px; text-decoration: none;
}
/* Nav icons lift into amber with a faint halo on hover. The icons draw with currentColor, so
   recolouring the svg is enough. drop-shadow (not box-shadow) makes the glow hug the actual
   strokes instead of the icon's box; keep it low-alpha, it should register as warmth not neon. */
.nav-btn svg.icon { width: 16px; height: 16px; flex-shrink: 0;
  transition: color .18s ease-out, transform .18s cubic-bezier(.34,1.56,.64,1), filter .18s ease-out; }
/* The tab you are ON keeps the amber icon so the current section stays obvious, but gets no
   halo and no lift: the glow is reserved for hover, so it always means "you are pointing at
   this" rather than "you are here". Declared BEFORE the hover rule — same specificity, so
   source order is what lets hovering the active tab still light it up. */
.nav-btn.active svg.icon { color: var(--warn); filter: none; transform: none; }
.nav-btn:hover svg.icon, .nav-btn:focus-visible svg.icon {
  color: var(--warn); transform: translateY(-2px) scale(1.14);
  filter: drop-shadow(0 0 5px color-mix(in srgb, var(--warn) 45%, transparent));
}
.nav-btn:hover { color: var(--text); background: transparent; }
.nav-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.nav-btn.active { color: var(--accent); font-weight: 700; }
.nav-btn.active::after { display: none; }

/* Mobile topbar: hamburger + theme toggle only; full nav collapses into dropdown. */
@media (max-width: 640px) {
  .topbar {
    padding: 6px 14px; gap: 8px; row-gap: 0; flex-wrap: nowrap;
    /* The wordmark is centred on the PAGE, not in the space left over beside the controls, so
       the controls come out of the flow and the brand is the only thing left to centre. Doing
       it the other way (auto margins) centres it in an asymmetric gap and looks a few pixels
       wrong on every width. */
    justify-content: center; align-items: center;
  }
  .topbar .brand { font-size: 16px; flex: 0 0 auto; min-width: 0; gap: 0; padding: 0; }
  .topbar .brand .brand-wordmark { height: 58px; }
  .brand-logo { width: 30px; height: 30px; }
  /* The bird leaves the lockup here: centring bird+wordmark together makes the pair too wide to
     clear the controls at 390px, and the ask is for the LOGO to be centred. So the bird is
     parked against the left edge, still straddling the line. .brand is left static on purpose,
     so this anchors to .topbar rather than to the anchor. */
  .brand-logo.standing {
    position: absolute; left: 4px; bottom: -17px;      /* 0.38 x 46, same line as desktop */
    width: 46px; height: auto; margin: 0;
  }
  .mobile-menu-btn { display: flex !important; position: absolute; right: 14px;
    top: 50%; transform: translateY(-50%); }
  .theme-toggle { display: none !important; }   /* it lives in the dropdown here */
  .header-search-nav { display: none !important; }
  .header-right { display: none !important; }
  .mobile-nav { display: none; }
  .mobile-nav.active {
    display: flex; position: absolute; top: 100%; left: 0; right: 0; background: var(--panel);
    border-bottom: 1px solid var(--line); flex-direction: column; gap: 0; z-index: 60;
    box-shadow: 0 8px 24px rgba(0,0,0,.2); order: 4;
  }
  .mobile-nav.active a,
  .mobile-nav.active .nav-theme { display: flex; align-items: center; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--line); color: var(--text); text-decoration: none; font-weight: 500; }
  .mobile-nav.active .nav-theme {
    width: 100%; background: none; border-left: 0; border-right: 0; border-top: 0;
    font: inherit; font-weight: 500; text-align: left; cursor: pointer;
  }
  /* Same amber lift + halo as the desktop nav. There is no hover on touch, so :active (the
     press) is the state that actually fires here; :hover covers hybrid devices. */
  .mobile-nav.active a .icon,
  .mobile-nav.active .nav-theme .icon { flex: 0 0 18px; color: var(--accent);
    transition: color .18s ease-out, transform .18s cubic-bezier(.34,1.56,.64,1), filter .18s ease-out; }
  .mobile-nav.active a:hover .icon, .mobile-nav.active a:active .icon,
  .mobile-nav.active .nav-theme:hover .icon, .mobile-nav.active .nav-theme:active .icon {
    color: var(--warn); transform: translateY(-2px) scale(1.14);
    filter: drop-shadow(0 0 5px color-mix(in srgb, var(--warn) 45%, transparent));
  }
  .mobile-nav.active a:last-child,
  .mobile-nav.active .nav-theme:last-child { border-bottom: none; }
  .mobile-nav.active a:active,
  .mobile-nav.active .nav-theme:active { background: var(--panel-2); }
}
/* Reduced motion keeps the amber recolour + glow (they carry the affordance) but drops the
   movement. Placed after the mobile block so it outranks both nav rules. */
@media (prefers-reduced-motion: reduce) {
  .nav-btn:hover svg.icon, .nav-btn:focus-visible svg.icon,
  .mobile-nav.active a:hover .icon, .mobile-nav.active a:active .icon,
  .mobile-nav.active .nav-theme:hover .icon, .mobile-nav.active .nav-theme:active .icon { transform: none; }
}
.balance-box { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.balance-box label { color: var(--muted); font-size: 13px; }
.money-input { display: flex; align-items: center; background: var(--panel-2); border: 1px solid var(--line); border-radius: 9px; padding: 0 10px; }
.money-input span { color: var(--muted); }
.money-input input {
  width: 110px; background: transparent; border: 0; color: var(--text);
  font-size: 18px; font-weight: 700; padding: 8px 6px; outline: none;
}
.saved-tag { color: var(--ok); font-size: 12px; min-width: 34px; }
.manual-star { color: var(--accent); font-weight: 700; margin-left: 2px; cursor: help; }
.bal-static { font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; }
.balance-box { flex-wrap: wrap; justify-content: flex-end; row-gap: 4px; }
.reset-box { display: flex; align-items: center; gap: 6px; }
.reset-box .money-input input { width: 74px; font-size: 15px; padding: 6px 4px; }
.reset-flag { color: var(--accent); margin-left: 3px; cursor: help; }
.reset-note { flex-basis: 100%; text-align: right; font-size: 12px; color: var(--warn); }
.reset-note.muted { color: var(--muted); }
.cur-form select {
  background: var(--panel-2); color: var(--text); border: 1px solid var(--line);
  border-radius: 8px; padding: 6px 8px; font-size: 13px; cursor: pointer;
}

/* ── freemium locks / role badges ── */
.lock-chip {
  display: inline-flex; align-items: center; gap: 4px; text-decoration: none;
  background: rgba(251,191,36,.12); color: var(--warn); border: 1px solid rgba(251,191,36,.32);
  font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 6px; white-space: nowrap;
}
.lock-chip:hover { filter: brightness(1.12); }
.unlock-chip {
  display: inline-flex; align-items: center; gap: 4px;
  background: rgba(52,211,153,.12); color: var(--ok); border: 1px solid rgba(52,211,153,.3);
  font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 6px; white-space: nowrap;
}
.btn.locked { background: rgba(251,191,36,.12); border-color: rgba(251,191,36,.35); color: var(--warn); font-weight: 600; }
.credit-chip { font-size: 12px; font-weight: 700; padding: 3px 9px; border-radius: 6px;
  background: rgba(59,130,246,.14); color: #93c5fd; white-space: nowrap; }
/* Subscription status pill in the header (trial countdown / active plan / free tier). */
.sub-chip { font-size: 12px; font-weight: 700; padding: 3px 10px; border-radius: 999px;
  white-space: nowrap; text-decoration: none; line-height: 1.6; }
.sub-chip.trial { background: rgba(34,197,94,.16); color: #4ade80; }
/* Darker, more orange than the old bright gold (#fbbf24 read too yellow). */
.sub-chip.subscribed { background: rgba(245,158,11,.15); color: #f59e0b; }
/* Monthly plan close to renewal: a redder tone warns without alarming. */
.sub-chip.subscribed.expiring { background: rgba(249,115,22,.16); color: #f97316; }
.sub-chip.free { background: rgba(148,163,184,.16); color: var(--muted); }
a.sub-chip:hover { filter: brightness(1.12); }
/* Signed-in username replaces the "Account" nav label — cap it so long emails don't wrap. */
.nav-username { display: inline-block; max-width: 130px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; vertical-align: middle; }
.head-right { display: flex; align-items: center; gap: 10px; }

/* ── layout ── */
/* Wide enough that the board's 11 columns fit without a horizontal scrollbar on a laptop;
   still centred and padded on anything narrower. */
/* The single page frame every template inherits. Do not set width/padding/centering on
   a page — use this, or add `class="reading"` for long-form pages. */
main { width: 100%; max-width: var(--content-max); margin: var(--page-top) auto; padding: 0 var(--page-pad-x);
  display: flex; flex-direction: column; gap: var(--page-gap); }
main.reading { max-width: var(--reading-max); }

/* Skeleton loading (Scores + Results shells): an empty table that fades in/out while the real
   content is fetched, then swaps in. */
@keyframes skel-pulse { 0%, 100% { opacity: .4; } 50% { opacity: .8; } }
.skel-wrap { display: flex; flex-direction: column; gap: 16px; }
.skel-table { display: flex; flex-direction: column; gap: 8px; }
.skel-bar { border-radius: 8px; background: var(--panel-2); animation: skel-pulse 1.25s ease-in-out infinite; }
.skel-bar.head { background: var(--panel); height: 40px; }
.skel-row { height: 46px; }
/* Loaded content containers stack their sections (skeleton or real) with the page rhythm. */
/* `#perf-body` became `.perf-body` so every page's <main> can carry the same id="main",
   which is what the skip link targets. `#board-body` was in this selector and in no
   template, so it went with it. */
.perf-body { display: flex; flex-direction: column; gap: var(--page-gap); }
.card {
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 18px 20px;
  box-shadow: 0 2px 8px rgba(0,0,0,.12);
}
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; gap: 16px; flex-wrap: wrap; row-gap: 10px; }
.card h1, .card h2 { margin: 0; font-size: 16px; font-weight: 600; color: var(--text); }
.totals { display: flex; gap: 18px; align-items: baseline; }
.totals strong { font-size: 16px; }
.match-head { align-items: center; }
.match-nav { display: flex; align-items: center; gap: 8px; }
.match-nav h2 { display: flex; align-items: baseline; gap: 8px; }

/* ── day picker: month header over a Mon–Sun grid; only scanned days are selectable ── */
.cal-wrap { position: relative; }
.cal-pop { position: absolute; z-index: 40; top: calc(100% + 6px); left: 0; width: 260px;
  padding: 10px; background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 10px; box-shadow: 0 12px 32px rgba(0,0,0,.45); }
@media (max-width: 640px) {
  .cal-pop { max-width: calc(100vw - 20px); left: 50%; transform: translateX(-50%); }
}
.cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.cal-month { font-weight: 700; font-size: 13px; letter-spacing: .02em; }
.cal-nav { appearance: none; border: 1px solid var(--line); background: var(--panel);
  color: var(--text); border-radius: 6px; width: 24px; height: 24px; cursor: pointer; line-height: 1; }
.cal-nav[disabled] { opacity: .3; cursor: not-allowed; }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.cal-dow { text-align: center; font-size: 10px; text-transform: uppercase; color: var(--muted);
  font-weight: 700; padding: 2px 0 4px; }
.cal-day { appearance: none; border: 0; background: none; color: var(--text); font-size: 12px;
  border-radius: 6px; height: 30px; cursor: pointer; font-variant-numeric: tabular-nums; }
.cal-day:hover:not([disabled]) { background: var(--panel); }
.cal-day[disabled] { color: #4b5361; cursor: default; }
.cal-day.is-today { box-shadow: inset 0 0 0 1px var(--line); font-weight: 700; }
.cal-day.is-current { background: var(--accent); color: #fff; font-weight: 800; }
.cal-empty { height: 30px; }
.match-date { font-weight: 400; }

.scan-card { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.scan-state { display: flex; flex-direction: column; margin-right: auto; }
.scan-state .warn { color: var(--warn); }
.scan-state .ok { color: var(--ok); }
.scan-state .muted { font-size: 13px; }

/* ── tables ── */
/* `safe center`: centre a table that fits, but fall back to start-alignment the moment it's
   wider than the container — otherwise centring splits the overflow to both sides and the left
   columns become unreachable (they scroll off-canvas / widen the page). Wide tables now left-
   align and scroll fully inside this container. */
.table-scroll { overflow-x: auto; overflow-y: visible; -webkit-overflow-scrolling: touch; margin: 0 auto; padding: 0; display: flex; justify-content: safe center; }
@media (max-width: 768px) {
  .table-scroll { overflow-x: visible; }
}
table.bets { width: 100%; border-collapse: collapse; font-size: 14px; }
table.bets th {
  text-align: left; color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase;
  letter-spacing: .03em; padding: 8px 10px; border-bottom: 1px solid var(--line);
}
.th-sort-btn {
  background: none; border: none; padding: 4px 6px; margin: -4px -6px; font: inherit; text-transform: inherit;
  letter-spacing: inherit; color: var(--muted); cursor: pointer; display: inline-flex; align-items: center;
  gap: 4px; border-radius: 3px; transition: all .15s; min-height: 36px;
}
.th-sort-btn:hover, .th-sort-btn[aria-pressed="true"] { color: var(--accent); background: rgba(59,130,246,.08); }
.th-sort-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.th-sort-btn .sort-icon { font-size: 11px; }
table.bets td { padding: 9px 10px; border-bottom: 1px solid rgba(42,49,61,.5); }
table.bets tr:last-child td { border-bottom: 0; }
table.bets .num { text-align: right; font-variant-numeric: tabular-nums; }
table.bets td.stake { font-weight: 700; }
.pos { color: var(--pos); }
.neg { color: var(--neg); }
.model-pct { color: var(--text); font-weight: 600; }
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; }
.stat {
  display: flex; flex-direction: column; gap: 4px; padding: 14px 16px; background: var(--panel-2);
  border: 1px solid var(--line); border-radius: 10px; transition: all .15s;
}
.stat:hover { background: rgba(29,34,43,.6); border-color: rgba(59,130,246,.3); }
.stat-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
  font-weight: 600; font-variant-numeric: tabular-nums;
}
.stat-val { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text); }
@media (max-width: 640px) {
  .stat-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  .stat { padding: 12px 14px; }
  .stat-val { font-size: 20px; }
}
/* Links, in two parts, because "has no class" and "is prose" are not the same thing.

   1. The safety net. There was NO global `a` rule, so any <a> without a class fell through to
      the browser's own styling: blue, and PURPLE once visited, which on the dark theme is
      close to unreadable. `inherit` means such a link takes the colour of whatever it sits in,
      so chrome (nav items, menus, cards) keeps looking like itself and nothing can ever go
      purple. Author styles beat the UA sheet whatever the specificity, so this settles the
      visited state too; it is spelled out anyway because a link changing colour after a click
      is precisely the behaviour being ruled out.

   2. The gold, for READING links only. It takes BOTH conditions: inside a prose container, and
      carrying no class of its own. Prose containers alone is not enough, because gold on `a`
      alone also caught the mobile nav's items, which are classless too. And classless alone is
      not enough either, which is the half that was missing: `a:visited` contributes a
      class-level component to the specificity, so `main :is(… td) a:visited` scored (0,1,3) and
      beat `.link` at (0,1,0). Every date in the results-per-day table turned gold once it had
      been clicked, green again on a device that had not. Any `.btn` sitting in a paragraph did
      the same. A link that was given a class was given a look on purpose; only an unstyled one
      is a reading link.

   Neither sets text-decoration: prose links keep their underline, which is the affordance. */
a { color: inherit; }
a:visited { color: inherit; }

main :is(p, li, figcaption, blockquote, dd, td) a:not([class]),
.faq-a a:not([class]), .legal a:not([class]) {
  color: var(--link);
}
main :is(p, li, figcaption, blockquote, dd, td) a:not([class]):visited,
.faq-a a:not([class]):visited, .legal a:not([class]):visited {
  color: var(--link);
}

.link { color: var(--accent); text-decoration: none; }
.link:hover { text-decoration: underline; }
.tag-risk { background: rgba(248,113,113,.14); color: var(--neg); font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: 5px; text-transform: uppercase; margin-right: 4px; }
.mp-score { margin-top: 8px; font-size: 12.5px; color: var(--muted); }
.mp-score strong { color: var(--text); }
.lg { display: inline-block; margin-left: 6px; padding: 1px 6px; border-radius: 5px; background: var(--panel-2); color: var(--muted); font-size: 11px; }
/* ── livescore-style per-league group header (dashboard match table) ── */
table.bets tr.league-group-row td { padding: 8px 10px; background: var(--panel-2); border-bottom: 1px solid var(--line); }
.lg-group-logo { width: 18px; height: 18px; object-fit: contain; vertical-align: -4px; margin-right: 7px; }
.lg-group-label { font-weight: 700; font-size: 12.5px; text-transform: uppercase; letter-spacing: .03em; color: var(--text); margin-right: 8px; }
/* ── livescore-style match row (time/FT, crest, team, score) ── */
.ls-row { display: flex; align-items: center; gap: 7px; font-variant-numeric: tabular-nums; }
.ls-time { flex: 0 0 40px; font-size: 12px; font-weight: 700; color: var(--muted); }
.ls-team { display: inline-flex; align-items: center; gap: 6px; }
.ls-logo { width: 16px; height: 16px; object-fit: contain; flex-shrink: 0; }
.ls-score { font-weight: 700; color: var(--text); min-width: 18px; text-align: center; }
.mtype { display: inline-block; margin-left: 4px; padding: 1px 6px; border-radius: 5px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .02em; }
.mtype-league { background: var(--panel-2); color: var(--muted); }
.mtype-cup { background: rgba(167,139,250,.16); color: #c4b5fd; }
.mtype-international { background: rgba(59,130,246,.16); color: #93c5fd; }
.mtype-youth { background: rgba(251,191,36,.16); color: var(--warn); }
.value-toggle.on { background: var(--accent); border-color: var(--accent); color: #fff; }
.empty {
  color: var(--muted); padding: 20px 16px; font-size: 14px; text-align: center;
  background: rgba(139,148,163,.06); border-radius: 10px; border: 1px dashed var(--line);
}
.note {
  margin: 12px 0; padding: 12px 14px; border-radius: 8px; font-size: 13px; font-weight: 500;
  border: 1px solid rgba(139,148,163,.3);
}
.note.ok { background: rgba(52,211,153,.12); color: var(--ok); border-color: rgba(52,211,153,.3); }

/* ── manual result entry ── */
.settle-form { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 0; }
.settle-form input[type="number"] {
  width: 46px; background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  border-radius: 7px; padding: 6px 4px; font-size: 14px; text-align: center;
}
.settle-form .dash { color: var(--muted); }
.settle-form .score-in, .settle-form .ht-in { display: inline-flex; align-items: center; gap: 5px; }
.settle-form .score-group { display: inline-flex; flex-direction: column; gap: 3px; }
.settle-form .score-lbl { font-size: 11px; font-weight: 600; }

/* ── date navigation ── */
.datenav { display: flex; align-items: center; gap: 8px; }
.btn.sm { padding: 5px 10px; font-size: 13px; }
.btn.disabled { opacity: .3; cursor: default; pointer-events: none; }
.cur-date { font-weight: 600; min-width: 150px; text-align: center; }

/* ── analyst predictions ── */
.preds { display: flex; flex-direction: column; gap: 12px; }
.match-pred { border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; background: var(--panel-2); }
.mp-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.mp-teams { font-weight: 600; }
.analyst-call { color: var(--muted); font-size: 13px; }
.analyst-call strong { color: var(--text); font-size: 18px; margin: 0 4px; font-variant-numeric: tabular-nums; }
.model-grid { margin-left: auto; }
.conf { font-size: 11px; font-weight: 700; padding: 1px 7px; border-radius: 6px; }
.conf-high { background: rgba(52,211,153,.16); color: var(--ok); }
.conf-medium { background: rgba(251,191,36,.16); color: var(--warn); }
.conf-low { background: rgba(139,148,163,.16); color: var(--muted); }
.mp-summary { margin-top: 8px; font-size: 13.5px; color: #c4ccd8; line-height: 1.55; }
.mp-missing { margin-top: 6px; font-size: 12px; color: var(--muted); }
.tag-missing { background: rgba(251,191,36,.14); color: var(--warn); font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: 5px; text-transform: uppercase; letter-spacing: .03em; margin-right: 4px; }

/* ── market selector + tags ── */
.market-select {
  background: var(--panel-2); color: var(--text); border: 1px solid var(--line);
  border-radius: 9px; padding: 8px 12px; font-size: 14px; font-weight: 600;
}
.tag-value { background: rgba(59,130,246,.18); color: #93c5fd; font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: 5px; text-transform: uppercase; margin-left: 5px; }
.tag-auto { background: rgba(52,211,153,.18); color: var(--ok); font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: 5px; text-transform: uppercase; margin-left: 5px; }
.tag-manual { background: rgba(167,139,250,.18); color: #c4b5fd; font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: 5px; margin-left: 6px; }
.tag-cs3 { background: rgba(56,189,248,.18); color: #7dd3fc; font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: 5px; text-transform: uppercase; margin-left: 6px; }
.cs3-list { list-style: none; margin: 0 0 4px; padding: 0; font-size: 13px; }
.cs3-list li { display: flex; align-items: baseline; gap: 6px; }
.cs3-combined { font-size: 14px; margin-bottom: 4px; }
.cs3-combined strong { color: var(--accent); }
.cs3-rule { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
.cs3-unit { display: inline-flex; align-items: center; gap: 6px; }
.cs3-unit input { width: 72px; }
.inline { display: inline; margin: 0; }

/* ── auto-place matrix editor ── */
.matrix-panel summary { cursor: pointer; list-style: none; display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.matrix-panel summary::-webkit-details-marker { display: none; }
.matrix-panel .m-title { font-size: 16px; font-weight: 700; }
.matrix-panel .m-title::before { content: "▸ "; color: var(--muted); }
.matrix-panel[open] .m-title::before { content: "▾ "; }
.matrix-table { margin-top: 14px; max-width: 560px; }
.matrix-table input[type="number"] { width: 64px; background: var(--panel-2); border: 1px solid var(--line); color: var(--text); border-radius: 7px; padding: 6px 8px; font-size: 14px; text-align: right; }
.switch { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); font-size: 13px; cursor: pointer; }
.switch input { accent-color: var(--accent); width: 15px; height: 15px; }

/* ── results ── */
.won, .lost { font-size: 11px; font-weight: 700; padding: 1px 7px; border-radius: 6px; }
.won { background: rgba(52,211,153,.16); color: var(--ok); }
.lost { background: rgba(248,113,113,.16); color: var(--neg); }
/* Correct-score result: gold = single top-pick bet landed exactly; green = the 3-way
   trio landed on one of its 3 legs; red = missed; grey = fixture voided (cancelled). */
.cs-result { font-size: 12px; font-weight: 700; padding: 1px 8px; border-radius: 6px; display: inline-flex; align-items: center; gap: 5px; }
.cs-gold { background: rgba(251,191,36,.18); color: var(--warn); }
.cs-green { background: rgba(52,211,153,.16); color: var(--ok); }
.cs-red { background: rgba(248,113,113,.16); color: var(--neg); }
.cs-void { background: var(--panel-2); color: var(--muted); }
.cs-mode-toggle { display: flex; gap: 4px; margin-bottom: 6px; }
.res-actual { font-size: 13px; margin-bottom: 3px; }
.badge-won { color: var(--ok); font-weight: 700; font-size: 12px; margin-left: 6px; }
.badge-lost { color: var(--neg); font-weight: 700; font-size: 12px; margin-left: 6px; }
.profit { font-weight: 700; margin-left: 8px; font-variant-numeric: tabular-nums; }
.pl { font-variant-numeric: tabular-nums; }

/* ── reasoning write-up: click ⓘ to expand a row underneath the match ── */
.info-btn {
  appearance: none; border: none; background: transparent; margin-left: auto; padding: 6px;
  color: var(--accent); font-weight: 700; font-size: 14px; cursor: pointer; line-height: 1;
  flex-shrink: 0; border-radius: 4px; min-width: 40px; min-height: 40px;
  display: flex; align-items: center; justify-content: center;
  transition: all .15s;
}
.info-btn:hover { background: rgba(59,130,246,.12); }
.info-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── crown: this row is one of today's curated Write-Ups ── */
.crown-link {
  margin-left: auto; padding: 6px; color: var(--warn); flex-shrink: 0; border-radius: 4px;
  min-width: 32px; min-height: 32px; display: flex; align-items: center; justify-content: center;
  transition: all .15s;
}
.crown-link:hover { background: rgba(251,191,36,.14); }
.crown-link:focus-visible { outline: 2px solid var(--warn); outline-offset: 2px; }
.crown-link + .info-btn { margin-left: 0; }
.info-row td, table.board tr.info-row td { padding: 14px 16px; background: var(--panel-2);
  border-bottom: 1px solid var(--line); border-radius: 6px; }
.info-panel { font-size: 13px; color: var(--text); line-height: 1.5; max-width: 640px; }
/* ── the result, at the top of an opened row ──
   HT next to FT, because the difference between "no goals came" and "0-0 at the break" is most
   of what a goals bet turns on, and HT was previously only a hover title (unreachable on touch). */
.res-panel { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.res-box { display: flex; align-items: baseline; gap: 7px; background: var(--panel-2);
  border-radius: 10px; padding: 6px 12px; font-variant-numeric: tabular-nums; }
.res-box .l { color: var(--muted); font-size: 10px; font-weight: 700; letter-spacing: .06em; }
.res-box b { font-size: 16px; font-weight: 800; }
.res-box.ft b { font-size: 19px; }
/* A word as well as a colour: the FT win/loss indicator already had to be changed once for being
   colour-only, and the same rule applies here. */
.res-verdict { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  padding: 4px 10px; border-radius: 999px; white-space: nowrap; }
.res-verdict.won { background: rgba(52,211,153,.16); color: #34d399; }
.res-verdict.lost { background: rgba(224,96,58,.16); color: #e0603a; }
.res-verdict.void { background: rgba(127,127,127,.16); color: var(--muted); }
.tip-title { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); margin-bottom: 8px; }
.tip-row { font-size: 13px; margin-bottom: 6px; }
.tip-row strong { font-variant-numeric: tabular-nums; }
.tip-sec { font-size: 12.5px; color: #c4ccd8; margin-top: 8px; }

/* "Why we tip this". The four figures read left to right as the argument itself: our chance,
   the price, what that price implies, and the gap between them. Tabular numerals so the column
   of percentages lines up, and the value is the only one that carries colour. */
.why-panel { margin-bottom: 14px; }
.why-row { padding: 8px 0; border-bottom: 1px solid var(--line, rgba(255,255,255,.08)); }
.why-row:last-of-type { border-bottom: 0; }
.why-pick { font-size: 13px; margin-bottom: 6px; }
.why-nums { display: flex; flex-wrap: wrap; gap: 6px 18px; }
.why-n { display: flex; flex-direction: column; line-height: 1.25; }
.why-n b { font-size: 15px; font-variant-numeric: tabular-nums; }
.why-n i { font-style: normal; font-size: 10px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted); }
.why-n.pos b { color: var(--ok, #34d399); }
.why-n.neg b { color: var(--bad, #f87171); }
.why-note { margin: 8px 0 0; }
.why-basis { font-size: 12.5px; color: #c4ccd8; margin-top: 8px; }
.why-none { margin-bottom: 14px; }
.why-none strong { font-size: 13px; }
.why-none p { margin: 4px 0 0; }
/* One column on a phone would be four tall rows; two keeps the whole argument on one screen. */
@media (max-width: 720px) {
  .why-nums { gap: 8px 14px; }
  .why-n b { font-size: 14px; }
}
.mp-head { margin-top: 8px; }
.mp-top3 { list-style: none; margin: 4px 0 0; padding-left: 0; font-size: 13px; }
.mp-top3 li { margin-bottom: 3px; display: flex; align-items: baseline; gap: 6px; }
.mp-top3 .mp-rank { display: inline-block; min-width: 52px; font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .03em; color: var(--muted); }
.mp-top3 strong { font-variant-numeric: tabular-nums; }
.mp-combined strong { color: var(--text); font-variant-numeric: tabular-nums; }

/* ── availability + analyst details ── */
.avail-groups { border-top: 1px solid var(--line); margin-top: 6px; padding-top: 6px; max-width: 480px; }
.avail-h { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; margin: 8px 0 2px; }
.avail-h.a-ok { color: var(--ok); }
.avail-h.a-no { color: var(--warn); }
details.mini { display: inline-block; }
details.mini summary { cursor: pointer; color: var(--muted); font-size: 12px; list-style: none; }
details.mini summary::-webkit-details-marker { display: none; }
details.mini[open] summary { color: var(--text); }
ul.avail { list-style: none; padding: 8px 0 2px; margin: 6px 0 0; border-top: 1px solid var(--line); max-width: 460px; }
ul.avail li { font-size: 12px; padding: 2px 0; }
.a-ok { color: #b9c2cf; }
.a-no { color: var(--warn); }
.conf { font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: 6px; margin-left: 5px; }
.conf-high { background: rgba(52,211,153,.16); color: var(--ok); }
.conf-medium { background: rgba(251,191,36,.16); color: var(--warn); }
.conf-low { background: rgba(139,148,163,.16); color: var(--muted); }
.mp-summary { margin-top: 8px; font-size: 13px; color: #c4ccd8; line-height: 1.5; max-width: 520px; }
.mp-missing { margin-top: 6px; font-size: 12px; color: var(--muted); max-width: 520px; }
.tag-missing { background: rgba(251,191,36,.14); color: var(--warn); font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: 5px; text-transform: uppercase; margin-right: 4px; }

/* ── results-over-time chart ── */
.chart-toggle { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.chart-mode-radio { position: absolute; opacity: 0; width: 0; height: 0; }
.chart-toggle-btn {
  cursor: pointer; padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 600;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--muted); user-select: none;
  min-height: 40px; transition: all .15s;
}
.chart-mode-radio:checked + .chart-toggle-btn { background: var(--accent); border-color: var(--accent-2); color: #fff; }
.chart-mode-radio:focus-visible + .chart-toggle-btn { outline: 2px solid var(--accent); outline-offset: 2px; }
.chart-toggle-btn:hover:not(:disabled) { border-color: var(--accent); }
@media (max-width: 640px) {
  .chart-toggle-btn { flex: 1; min-width: 100px; }
}
.chart-hint-cum, .chart-hint-bank { display: none; }
.card:has(#chart-mode-cum:checked) .chart-hint-cum { display: inline; }
.card:has(#chart-mode-bank:checked) .chart-hint-bank { display: inline; }
.chart-panel { display: none; }
.card:has(#chart-mode-cum:checked) .chart-panel-cum { display: block; }
.card:has(#chart-mode-bank:checked) .chart-panel-bank { display: block; }
.chart-wrap { display: flex; flex-direction: column; gap: 4px; margin-top: 10px; }
.chart-body { display: flex; gap: 6px; }
.chart-yaxis {
  display: flex; flex-direction: column; justify-content: space-between;
  height: 160px; width: 44px; flex-shrink: 0; text-align: right; white-space: nowrap;
  font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums;
}
.chart-col { flex: 1; min-width: 0; }
.chart-plot { position: relative; height: 160px; }
@media (max-width: 480px) {
  .chart-plot { height: 140px; }
  .chart-yaxis { width: 48px; font-size: 10px; }
  .chart-xaxis { font-size: 10px; }
}
.results-chart { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.chart-zero { stroke: var(--line); stroke-width: 1; stroke-dasharray: 4 4; vector-effect: non-scaling-stroke; }
.chart-line { stroke-width: 2; vector-effect: non-scaling-stroke; }
.chart-line.pos { stroke: var(--pos); }
.chart-line.neg { stroke: var(--neg); }
.chart-month-line { stroke: var(--line); stroke-width: 1; stroke-dasharray: 2 3; opacity: .7; vector-effect: non-scaling-stroke; }
.chart-pt { position: absolute; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px; border-radius: 50%; cursor: pointer; }
.chart-pt.pos { background: var(--pos); }
.chart-pt.neg { background: var(--neg); }
.chart-reset-pt { position: absolute; top: -4px; width: 9px; height: 9px; margin-left: -4.5px; border-radius: 50%; background: var(--warn); border: 2px solid var(--panel); cursor: pointer; z-index: 2; }
/* Rule-change annotation. Deliberately NOT a result colour: this marks a configuration change,
   not money, and reusing --pos/--neg here would read as a claim about the day. */
.chart-rule-line { stroke: var(--info); stroke-width: 1; stroke-dasharray: 5 4; opacity: .5; vector-effect: non-scaling-stroke; }
.chart-rule-pt { position: absolute; bottom: -6px; width: 12px; height: 12px; margin-left: -6px;
  cursor: pointer; z-index: 2; }
/* The diamond lives on a child so the marker box itself stays unrotated: a rotated parent would
   drag the tooltip into its coordinate system and skew it 45deg. */
.chart-rule-pt::before { content: ""; position: absolute; inset: 1px; background: var(--info);
  border: 2px solid var(--panel); transform: rotate(45deg); }
.chart-rule-key { background: var(--info); transform: rotate(45deg); }
.chart-rule-pt .tip-body { white-space: normal; width: max-content; max-width: min(340px, 68vw); }
.chart-rule-pt.edge .tip-body { left: auto; right: 0; transform: none; }
.chart-rule-pt:hover .tip-body, .chart-rule-pt:focus .tip-body { display: block; }
.rule-items { display: block; margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--line);
  font-size: 11.5px; line-height: 1.5; font-variant-numeric: tabular-nums; }
.rule-items span { display: block; }

/* Write-ups overlay: dashed, so it reads as a subset of the P&L line it sits on rather than a
   second bankroll. vector-effect keeps the stroke 1.5px through the non-uniform viewBox scale. */
.wu-line { stroke: var(--wu-line); stroke-width: 1.5; stroke-dasharray: 4 3;
  vector-effect: non-scaling-stroke; fill: none; }
.wu-pt { position: absolute; width: 6px; height: 6px; margin: -3px 0 0 -3px; border-radius: 50%;
  background: var(--wu-line); cursor: pointer; }
.wu-pt strong { color: var(--wu-line); }
.chart-pt .tip-body, .chart-reset-pt .tip-body, .market-dot .tip-body, .wu-pt .tip-body {
  display: none; position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%);
  margin-bottom: 8px; z-index: 30; white-space: nowrap; width: auto;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px;
  padding: 7px 10px; box-shadow: 0 16px 44px rgba(0,0,0,.55);
  font-size: 12.5px; color: var(--text); line-height: 1.4; cursor: default;
}
.chart-pt:hover .tip-body, .chart-pt:focus .tip-body,
.chart-reset-pt:hover .tip-body, .chart-reset-pt:focus .tip-body,
.wu-pt:hover .tip-body, .wu-pt:focus .tip-body,
.market-dot:hover .tip-body, .market-dot:focus .tip-body { display: block; }
.chart-xaxis { position: relative; height: 44px; margin-top: 10px; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; overflow: visible; }
@media (max-width: 640px) {
  .chart-xaxis { height: 40px; font-size: 10px; }
}

/* ── performance page: monthly summary / reset divider ── */
.sub-head { font-size: 13px; color: var(--muted); margin: 4px 0 8px; text-transform: uppercase; letter-spacing: .04em; }
.month-reset-row td { padding: 2px 0 10px; border: none; }
.month-reset-line { border: none; border-top: 1px dashed var(--line); margin: 0 0 4px; }

/* ── account settings page ── */
.acct-form { display: flex; flex-direction: column; gap: 14px; max-width: 360px; }
.acct-field { display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--muted); font-weight: 500; }
.acct-field label { color: var(--text); font-weight: 600; }
.acct-field input, .acct-field select {
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  border-radius: 8px; padding: 10px 12px; font-size: 14px; font-weight: 500;
  transition: border-color .15s, background .15s; min-height: 40px;
}
.acct-field input:focus, .acct-field select:focus {
  outline: none; border-color: var(--accent); background: rgba(59,130,246,.04);
}
.acct-field input:focus-visible, .acct-field select:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.acct-list { display: flex; flex-direction: column; }
.acct-list-item {
  padding: 10px 0; border-bottom: 1px solid var(--line); font-size: 14px;
}
.acct-list-item:last-child { border-bottom: none; }
.acct-list-link { color: var(--text); text-decoration: none; }
.acct-list-link:hover { color: var(--accent, #6ea8fe); }

/* ── scan progress ── */
.row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.log {
  background: #0a0d0b; border: 1px solid var(--line); border-radius: 8px; padding: 12px;
  max-height: 280px; overflow: auto; font: 12px/1.5 ui-monospace, Menlo, monospace; color: #c8d4cc; white-space: pre-wrap;
}
.spinner {
  width: 16px; height: 16px; border: 2px solid var(--line); border-top-color: var(--accent);
  border-radius: 50%; display: inline-block; animation: spin .8s linear infinite;
}
.skeleton {
  background: linear-gradient(90deg, var(--panel-2) 0%, var(--panel) 50%, var(--panel-2) 100%);
  background-size: 200% 100%; animation: skeleton-load 1.5s infinite; border-radius: 6px;
}
@keyframes skeleton-load { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
/* Reduced-motion: freeze the loading indicators (the bird loader, marquees and live dot already
   opt out below). A static ring / bar / panel still reads as "loading" without the pulsing. */
@media (prefers-reduced-motion: reduce) {
  .spinner  { animation: none; }
  .skel-bar { animation: none; opacity: .6; }
  .skeleton { animation: none; background-position: 0 0; }
}

/* ── walking-osprey loader (shared, see templates/_bird_loader.html) ──
   The bird is _osprey.html's side view, the same asset the page mascots use. Everything below
   is motion only; nothing here draws. */
.bird-loading { margin: 2px 0 12px; text-align: center; }
.bird-track { position: relative; height: 62px; overflow: hidden; }
.bird-walk { position: absolute; bottom: 0; left: 0; will-change: left; animation: bird-cross 7s linear infinite; }
@keyframes bird-cross { from { left: -88px; } to { left: 100%; } }
.bird-walk .osprey-mascot { display: block; transform-origin: 50% 100%; animation: bird-bob .46s ease-in-out infinite; }
@keyframes bird-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-2.5px); } }
/* Pivot each leg at its HIP, in viewBox units. transform-box:fill-box would put the origin at
   the top-centre of the leg's own bounding box, which is inside the bend rather than at the
   joint, and the bird paddles instead of striding. The two hips are at different heights
   because the torso is tilted, so they cannot share one origin. */
.bird-walk .osprey-leg { transform-box: view-box; }
.bird-walk .osprey-leg-a { transform-origin: 72px 68px; animation: bird-stride-a .46s ease-in-out infinite; }
.bird-walk .osprey-leg-b { transform-origin: 56px 70px; animation: bird-stride-b .46s ease-in-out infinite; }
@keyframes bird-stride-a { 0%, 100% { transform: rotate(17deg); } 50% { transform: rotate(-17deg); } }
@keyframes bird-stride-b { 0%, 100% { transform: rotate(-17deg); } 50% { transform: rotate(17deg); } }
.bird-cap { display: inline-block; margin-top: 2px; }
/* honour reduced-motion: the bird stands still, no walk / stride / bob */
@media (prefers-reduced-motion: reduce) {
  .bird-walk, .bird-walk .osprey-mascot, .bird-walk .osprey-leg { animation: none; }
  .bird-walk { left: 50%; transform: translateX(-50%); }
}

/* ── soaring osprey — decorative, for empty states / heroes (uses the logo asset) ── */
.osprey-soar { display: block; margin: 0 auto 10px; width: 72px; height: auto;
  transform-origin: 50% 50%; animation: osprey-soar 4.5s ease-in-out infinite; }
@keyframes osprey-soar {
  0%, 100% { transform: translateY(0) rotate(-3deg); }
  50%      { transform: translateY(-12px) rotate(3deg); }
}
/* Size modifiers are namespaced on purpose: a bare `.lg` is already the league-code chip above
   (background: var(--panel-2)), so `class="osprey-soar lg"` painted a grey block behind the
   transparent logo. Never reuse a bare two-letter utility name for a component modifier. */
.osprey-soar.soar-sm { width: 44px; }
.osprey-soar.soar-lg { width: 104px; }
@media (prefers-reduced-motion: reduce) { .osprey-soar { animation: none; } }

/* ── reusable dismiss (×) button — rounded square, top-right of a card ── */
.card-close {
  position: absolute; top: 12px; right: 12px; z-index: 2;
  appearance: none; border: 1px solid var(--line); background: var(--panel-2);
  color: var(--muted); cursor: pointer; border-radius: 8px; width: 32px; height: 32px;
  font-size: 20px; line-height: 1; display: flex; align-items: center; justify-content: center;
  transition: background .15s, color .15s, border-color .15s;
}
.card-close:hover { background: var(--neg); color: #fff; border-color: var(--neg); }
.card-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── yesterday's winners carousel (anon) ── */
.wins-card { border-color: rgba(52,211,153,.35); position: relative; }
/* ◀ ▶ sit beside the × dismiss button and match its 32px box, so the three controls read as
   one cluster in the card's top-right instead of the arrows floating in the header. × is 32px
   wide at right:12px, so the pair starts at 12 + 32 + 8 = 52px. */
.carousel-nav { position: absolute; top: 12px; right: 52px; z-index: 2; display: flex; gap: 6px; }
.carousel-nav .btn {
  width: 32px; height: 32px; min-height: 0; padding: 0;
  display: flex; align-items: center; justify-content: center; border-radius: 8px;
}
/* overflow-x:auto (not hidden) so a touch swipe works at all, plus mandatory snapping so a
   page always lands on a card edge — the old scrollBy(clientWidth) could settle mid-card and
   leave a sliver of the next one showing. The scrollbar itself is hidden; the arrows are the
   affordance on desktop and the swipe is the affordance on mobile. */
.carousel { overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; -ms-overflow-style: none; }
.carousel::-webkit-scrollbar { display: none; }
.win-card { scroll-snap-align: start; }
/* Keep the header clear of the × + arrows cluster */
.wins-card .card-head { padding-right: 130px; }
@media (max-width: 640px) {
  /* Shorter overall: the header no longer stacks into its own column (the arrows moved out of
     it, so there is nothing left to wrap), and the card padding tightens. */
  .wins-card .card-head { flex-direction: row; align-items: center; padding-right: 130px; }
  .wins-card { padding-top: 14px; padding-bottom: 14px; }
  .wins-card .card-head h2 { font-size: 16px; margin: 0; }
  .wins-card .win-card { padding: 12px 14px; gap: 7px; }
  .wins-card .head-right { width: auto; flex-wrap: wrap; gap: 10px; row-gap: 8px; align-items: center; }
  .wins-card .balance-box { margin-left: 0; }
  .wins-card .carousel-nav { margin-left: auto; }
}
.carousel-track { display: flex; gap: 14px; }
.win-card {
  position: relative; flex: 0 0 calc(50% - 7px); min-width: 0;
  background: var(--panel-2); border: 1px solid rgba(52,211,153,.3); border-radius: 12px;
  padding: 16px 18px; display: flex; flex-direction: column; gap: 10px;
  transition: all .15s;
}
.win-card:hover { border-color: rgba(52,211,153,.6); }
@media (max-width: 640px) {
  .win-card { flex: 0 0 100%; }
}
.win-check {
  position: absolute; top: 12px; right: 14px; width: 26px; height: 26px; border-radius: 50%;
  background: rgba(52,211,153,.18); color: var(--ok); font-weight: 800;
  display: flex; align-items: center; justify-content: center;
}
.win-teams { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding-right: 30px; }
.win-team { display: inline-flex; align-items: center; gap: 7px; min-width: 0; }
.win-logo { width: 28px; height: 28px; object-fit: contain; border-radius: 5px; background: #fff2; }
.win-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.win-vs { color: var(--muted); }
.win-bet { font-size: 14px; }
.win-market { color: var(--muted); margin-right: 6px; }
.win-odds { color: var(--ok); font-weight: 700; }
@media (max-width: 560px) { .win-card { flex: 0 0 100%; } }

/* ── touch targets (WCAG 2.5.5) ─────────────────────────────────────────────────────────────
   On a touch pointer, form controls grow to a 44px tap height, and the compact 32px icon buttons
   get a transparent 44px hit area via ::after WITHOUT changing how they look (keeps the tight
   corner clusters). Sort headers already use ::after for the active-arrow, so they grow padding
   instead. Scoped to `pointer: coarse` so desktop mouse users keep the compact look. */
@media (pointer: coarse) {
  .matrix-table input[type="number"],
  .acct-field input, .acct-field select,
  .settle-form input[type="number"],
  .cur-form select, .market-select,
  .login-card input[type="text"],
  .login-card input[type="email"],
  .login-card input[type="password"] { min-height: 44px; }

  /* `.card-close` is deliberately NOT in this list. It is already `position: absolute` (pinned to
     its card's top-right), which is just as good a containing block for the ::after below — and
     resetting it to `relative` dropped it back into normal flow, so on every touch device the ×
     rendered at the card's top-LEFT, on top of the "Yesterday's results" heading, instead of the
     corner. Only add an element here when it is statically positioned. */
  .carousel-nav .btn, .crown-link { position: relative; }
  .card-close::after, .carousel-nav .btn::after, .crown-link::after {
    content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
    min-width: 44px; min-height: 44px;
  }

  .sort-th, .tips-filter-btn { min-height: 44px; padding-top: 12px; padding-bottom: 12px; }

  /* The lock chip is the subscribe CTA on every locked row, and it was the smallest tap target
     on the page at roughly 29x18. The invisible ::after gives it a real hit box without changing
     how it looks or reflowing the column. */
  .lock-chip, .info-btn { position: relative; }
  .lock-chip::after, .info-btn::after {
    content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
    min-width: 44px; min-height: 44px;
  }

  /* The mobile board header IS the sort and filter control: the desktop thead is display:none,
     so these are the only way to reorder or filter on a phone. They had padding:0, which left the
     hit box as the bare text glyph. Padding on the parent row does not grow a child's target. */
  .board-mhead .mh-sort, .board-mhead .mh-filter {
    min-height: 44px; display: inline-flex; align-items: center;
  }
  /* Height was already 44 here; WIDTH was not. The three outcome columns are 22px wide because
     that is what "1", "X" and "2" need, which left them 22x44 and two pixels under the WCAG
     2.5.8 minimum on the narrowest axis. Widening the flex basis would push the match name, so
     the button overflows its own cell instead: the cells are centred and the header row has no
     other hit targets between them, so the extra pixels land on dead space. */
  .board-mhead .mh-p.mh-sort { min-width: 24px; margin-inline: -1px; justify-content: center; }

  /* Day picker: 24px arrows and 30px day cells, both under the floor. */
  .cal-nav { min-width: 40px; min-height: 40px; }
  .cal-day { min-height: 40px; }

  /* Account markets grid: the whole label is clickable but its height was just the line box. */
  .mkt-toggle { padding: 10px 4px; }
  .mkt-toggle input { width: 20px; height: 20px; }
}

/* The auto-place matrix is a real table with number inputs, so it can't shrink below its columns.
   Let it scroll inside itself on a narrow phone instead of pushing the page sideways. */
@media (max-width: 560px) {
  .matrix-table { display: block; max-width: 100%; overflow-x: auto; }
}

/* ── teaser (anon) ── */
.teaser {
  margin-top: 16px; padding: 16px 18px; border: 1px dashed rgba(59,130,246,.5);
  border-radius: 12px; background: rgba(59,130,246,.06);
}
.teaser p { margin: 0 0 12px; }
.teaser-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.js-buy { cursor: pointer; }

/* Account banners (welcome / trial countdown / trial-ended) shown at the top of the board. */
.acct-banner { display: flex; align-items: center; justify-content: space-between; gap: 14px;
  flex-wrap: wrap; padding: 12px 16px; border-radius: 10px; margin-bottom: 14px; font-size: 14px;
  border: 1px solid var(--line); background: var(--panel); }
.acct-banner.welcome { background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.35); }
.acct-banner.trial { background: rgba(59,130,246,.1); border-color: rgba(59,130,246,.35); }
.acct-banner.ended { background: rgba(234,179,8,.1); border-color: rgba(234,179,8,.4); }
.acct-banner-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.acct-banner .btn { white-space: nowrap; }
.acct-banner-x { text-decoration: none; color: var(--muted); font-size: 20px; line-height: 1;
  padding: 0 4px; }
.acct-banner-x:hover { color: var(--text); }

/* ── buy-a-prediction modal (anon) ── */
body.modal-open { overflow: hidden; }
.modal-overlay {
  position: fixed; inset: 0; z-index: 50; background: rgba(4,7,12,.72);
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
.modal-overlay[hidden] { display: none; }
.modal {
  position: relative; width: 560px; max-width: calc(100% - 40px); background: var(--panel);
  border: 1px solid var(--line); border-radius: 16px; padding: 28px;
  box-shadow: 0 30px 70px rgba(0,0,0,.55);
}
@media (max-width: 600px) {
  .modal { padding: 20px; max-width: calc(100% - 24px); }
}
.modal h2 { margin: 0 0 6px; }
.modal-close {
  position: absolute; top: 14px; right: 16px; background: none; border: none; color: var(--muted);
  font-size: 26px; line-height: 1; cursor: pointer;
}
.modal-close:hover { color: var(--text); }
.price-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 18px 0; }
.price-card {
  position: relative; border: 1px solid var(--line); border-radius: 12px; padding: 18px 12px;
  text-align: center; background: var(--panel-2);
}
.price-card.best { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.price-card.chosen { border-color: var(--line); box-shadow: 0 0 0 1px rgba(139,148,163,.4) inset; }
.price-buy { appearance: none; font: inherit; color: inherit; width: 100%; cursor: pointer;
  transition: filter .15s, border-color .15s; }
.price-buy:hover { filter: brightness(1.15); border-color: var(--accent); }
.price-badge {
  position: absolute; top: -10px; left: 50%; transform: translateX(-50%);
  background: var(--accent); color: #fff; font-size: 10px; font-weight: 700;
  padding: 2px 8px; border-radius: 999px; white-space: nowrap;
}
.price-badge.chosen { background: var(--panel); color: var(--muted); border: 1px solid var(--line); }
.price-amt { font-size: 26px; font-weight: 800; }
.price-unit { color: var(--muted); font-size: 13px; margin-top: 2px; }
.price-note { color: var(--ok); font-size: 12px; margin-top: 6px; }
.modal-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 8px; }
@media (max-width: 480px) { .price-grid { grid-template-columns: 1fr; } }
@keyframes spin { to { transform: rotate(360deg); } }

/* ── mobile: transform match table into cards (CSS-only, single DOM) ── */
@media (max-width: 768px) {
  /* Keep overflow visible on the whole board ancestry so the sticky mobile header isn't captured
     (html{overflow-x:hidden} already clips any stray horizontal width at the top level). The
     earlier capture bug was an inline `main{overflow-y:auto}` in dashboard.html, now removed. */
  main, main.reading { padding: 0 var(--page-pad-x-sm); margin: 14px auto; gap: 12px; overflow: visible; max-width: 100%; }
  .card { padding: 14px 14px; border-radius: 12px; max-width: 100%; overflow: visible; }
  .balance-box { margin-left: 0; }
  .datenav { gap: 4px; flex-wrap: wrap; }
  .cur-date { min-width: 0; font-size: 13px; }
  .card h1, .card h2 { font-size: 15px; font-weight: 600; }
  .stat-grid { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; }
  .modal { padding: 20px; }
  .price-amt { font-size: 22px; }

  /* Match-head control bar must wrap, not force the card wide */
  .card-head.match-head { flex-wrap: wrap; }
  .match-nav { flex-wrap: wrap; gap: 6px; row-gap: 6px; }
  /* Keep the day label inline BETWEEN the ◀ / ▶ buttons (its DOM position), not pulled out
     onto its own line. The nav wraps if it must. */
  .match-nav h2 { order: 0; flex: 0 0 auto; font-size: 14px; margin: 0 2px; white-space: nowrap; }
  .league-search { width: 100% !important; min-width: 0 !important; max-width: 100%; box-sizing: border-box; }

  /* Board table → card rows. overflow stays visible so the sticky mobile header below sticks
     to the VIEWPORT (any clip/hidden ancestor would capture it instead and it'd scroll away). */
  .board-scroll { display: block; overflow: visible; max-width: 100%; }
  table.board, table.board tbody { display: block; width: 100%; max-width: 100%; min-width: 0; }
  table.board thead { display: none; }
  /* Mobile column header — mirrors the match-row flex layout so labels line up over the cards.
     `.board-scroll` prefix outranks the base `.board-mhead{display:none}` that follows in source. */
  .board-scroll .board-mhead {
    display: flex; align-items: center; gap: 5px;
    position: sticky; top: var(--board-head-top, 56px); z-index: 8;
    padding: 8px 8px 8px 6px; margin-bottom: 6px;
    background: var(--panel); border-bottom: 1px solid var(--line);
    font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
  }
  /* Header cells mirror the match-row flex bases below (must stay in sync to line up). The
     text-align is set on every cell because the sort cells are <button>s (UA-centered by
     default), so left/right must be stated to keep them lined up over the columns. */
  .board-mhead .mh-date { flex: 0 0 auto; min-width: 22px; text-align: left; }
  .board-mhead .mh-match { flex: 1 1 auto; min-width: 66px; text-align: left; }
  .board-mhead .mh-p { flex: 0 0 22px; text-align: center; }
  /* Tips column stays: it lines up over the compact TIP badge on each betted row (and is the
     tap-to-filter control). Narrow, matching the badge column width so FT stays aligned. */
  .board-mhead .mh-tips { flex: 0 0 auto; min-width: 40px; max-width: 48px; text-align: right; }
  .board-mhead .mh-ft { flex: 0 0 auto; min-width: 30px; text-align: right; }
  /* Locked tip: icon only (drop the "paid" label), tight box, and lifted above neighbours. */
  .lock-label { display: none; }
  /* Alternating bands on mobile apply to the whole card (tr), not per cell — so neutralise the
     desktop per-cell rule here and band the row background instead. */
  table.board tr.match-row.row-a td:not(.prob-hi):not(.prob-mid),
  table.board tr.match-row.row-b td:not(.prob-hi):not(.prob-mid) { background: transparent; }
  :root[data-theme="light"] table.board tr.match-row.row-a td:not(.prob-hi):not(.prob-mid),
  :root[data-theme="light"] table.board tr.match-row.row-b td:not(.prob-hi):not(.prob-mid) { background: transparent; }
  /* BOTH bands must be named. row-b was, row-a was not, so the light band fell through to the
     card's own `background: var(--panel)` below and rendered white in light theme while the
     dark band looked correct. */
  table.board tr.match-row.row-a { background: var(--board-row-a); }
  table.board tr.match-row.row-b { background: var(--board-row-b); }
  .c-tips .lock-chip { gap: 0; padding: 2px 4px; position: relative; z-index: 2; background: var(--panel); }
  table.board tr.league-group-row { display: block; padding: 10px 4px 4px; }
  /* Value/tips/search filters hide empty league headers via [hidden]; the display:block above
     would otherwise override the UA hidden rule and leave the strip behind (web already hides it). */
  table.board tr.league-group-row[hidden] { display: none; }
  table.board tr.league-group-row td { display: block; padding: 0; background: transparent; }
  table.board tr.match-row {
    display: flex; align-items: center; gap: 5px; width: 100%; box-sizing: border-box;
    background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
    padding: 10px 8px 10px 6px; margin-bottom: 6px; cursor: pointer;
  }
  /* Respect JS filters (value/live/tips/search) which hide rows via the hidden attribute */
  table.board tr.match-row[hidden] { display: none; }
  table.board tr.match-row.expanded { background: var(--panel-2); }
  /* Hide all cells, then show only the ones that matter */
  table.board tr.match-row td { display: none; padding: 0; border-radius: 0; background: transparent; }
  table.board tr.match-row td.c-date { display: flex; flex-direction: column; align-items: flex-start; justify-content: center; flex: 0 0 auto; min-width: 22px; font-size: 10px; font-weight: 700; color: var(--muted); }
  table.board tr.match-row td.c-match { display: block; flex: 1 1 auto; min-width: 66px; white-space: normal; overflow: hidden; }
  /* 1X2 Chance percentages — compact cells pulled tight together, keep magnitude colouring */
  table.board tr.match-row td.c-p {
    display: flex; align-items: center; justify-content: center; flex: 0 0 22px;
    font-size: 10.5px; font-weight: 600; text-align: center; border-radius: 4px; padding: 2px 0; height: 28px;
  }
  table.board tr.match-row td.c-p.prob-mid { background: rgba(52,211,153,.14); }
  table.board tr.match-row td.c-p.prob-hi { background: rgba(22,128,90,.85); }
  table.board tr.match-row td.c-p .pct { display: none; }
  /* FT score */
  table.board tr.match-row td.c-ft { display: flex; align-items: center; justify-content: flex-end; flex: 0 0 auto; min-width: 30px; font-size: 12px; font-weight: 700; white-space: nowrap; }
  /* Tip column — more room now the 1X2 are tighter, so tip labels aren't clipped */
  /* Collapsed: a match with a bet shows only a compact orange TIP badge in a narrow column (same
     orange as the lock), so the row keeps time + teams + chances on one line and nothing is cropped.
     The full bet shows when the row is tapped open (see .expanded rules below). */
  table.board tr.match-row td.c-tips { display: flex; align-items: center; justify-content: flex-end; flex: 0 0 auto; min-width: 40px; max-width: 48px; gap: 4px; overflow: visible; white-space: nowrap; }
  table.board tr.match-row:not(.expanded) td.c-tips .tip { display: none; }
  /* Not on a settled row: once there is a final score the c-ft cell carries the result block,
     which says both that a tip existed and how it did. The badge would repeat it and steal the
     width the score needs. */
  table.board tr.match-row:not(.expanded):not(.is-settled) td.c-tips:has(.tip)::after {
    content: "TIP"; color: #f59e0b; font-weight: 800; font-size: 10px; letter-spacing: .04em;
    border: 1px solid rgba(245,158,11,.45); border-radius: 4px; padding: 1px 5px; line-height: 1.5;
  }
  table.board tr.match-row td.c-match .m-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
  /* A name too long for its slot gets a slow, continuous slide instead of a frozen ellipsis, so
     the full name becomes readable over time. An inline script at the end of the board partial
     (dashboard.html) decides which names actually overflow (scrollWidth vs clientWidth — CSS
     alone can't measure that): only those get wrapped in an inner .m-name-inner span and this
     class. `.m-name` itself must stay the fixed clip window — animating the SAME element that
     clips it would just slide the whole (already-clipped) box sideways as one rigid unit and
     reveal nothing, so it's the inner span that gets translated inside the outer, unmoving crop.
     Ellipsis would also freeze a "…" over text sliding underneath it, so swap to a hard clip. */
  table.board tr.match-row td.c-match .m-name.m-name-marquee { text-overflow: clip; }
  table.board tr.match-row td.c-match .m-name.m-name-marquee .m-name-inner {
    display: inline-block;
    animation: m-name-marquee var(--marquee-dur, 7s) ease-in-out infinite;
  }
  /* Tapping a row open (ⓘ) is a request for full context — stop truncating/sliding the names in
     an expanded row so both are fully readable immediately, wrapping onto a 2nd line if needed.
     Declared after the marquee rules above so it wins the specificity tie when a name is both
     overflowing and expanded. */
  table.board tr.match-row.expanded td.c-match .m-name {
    white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: break-word;
  }
  table.board tr.match-row.expanded td.c-match .m-name .m-name-inner {
    display: inline; animation: none; transform: none;
  }
  /* Tapping a row open shows what the phone layout had to drop: the Odds and Stake columns,
     which exist in the DOM but are hidden by the `td { display: none }` rule above.
     They sit between Tips and FT in source order, so `order` moves them past FT, and the
     zero-height full-width ::after breaks the flex line so they land on their own row.
     Wrapping is enabled only while expanded, so a collapsed card can never wrap by accident. */
  table.board tr.match-row.expanded { flex-wrap: wrap; }
  table.board tr.match-row.expanded::after { content: ""; flex: 0 0 100%; height: 0; order: 9; }
  table.board tr.match-row.expanded td.c-o {
    display: flex; align-items: center; gap: 5px; order: 10; margin-top: 8px;
    font-size: 11.5px; font-weight: 700; color: var(--text);
    padding: 3px 8px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel);
  }
  table.board tr.match-row.expanded td.c-o + td.c-o { margin-left: 5px; }
  /* no <thead> on mobile, so each odds cell carries its own 1 / X / 2 label (data-k) */
  table.board tr.match-row.expanded td.c-o::before {
    content: attr(data-k); color: var(--muted); font-size: 10px; font-weight: 800;
  }
  /* Stake only exists on a matched bet; :has keeps the label off an empty cell. */
  table.board tr.match-row.expanded td.c-stake:has(.ln) {
    display: flex; align-items: center; gap: 5px; order: 11; margin: 8px 0 0 auto;
    font-size: 11.5px; font-weight: 700; color: var(--text);
  }
  table.board tr.match-row.expanded td.c-stake:has(.ln)::before {
    content: "Stake"; color: var(--muted); font-size: 10px; font-weight: 800;
  }
  table.board tr.match-row.expanded td.c-stake .ln { display: inline; }
  table.board tr.match-row td.c-match .m-team { min-width: 0; }
  /* Teams stack vertically inside their cell */
  .m-teams { flex-direction: column; align-items: flex-start; gap: 3px; }
  .m-vs { display: none; }
  /* `align-items: flex-start` above means a row (.m-team) hugs its own content width instead of
     the card's — a long name was rendering at full natural width and bleeding across the row
     into the Chance/Tips cells rather than actually being clipped by the ellipsis rule below it
     (which needs a genuinely narrower box to have anything to do). `width: 100%` clamps it to
     the cell; `.m-name`'s `min-width: 0` lets it shrink inside that flex row instead of the
     default `min-width: auto`, which pins a nowrap span to its full text width regardless. */
  .m-team { display: flex; align-items: center; gap: 6px; width: 100%; }
  .m-name { font-size: 13px; font-weight: 600; min-width: 0; }
  .m-teams .info-btn, .m-teams .crown-link { display: none; }
  /* Tips: show abbreviated */
  .tip-full { display: none; }
  .tip-abbr { display: inline; font-weight: 700; }
  .c-tips .tip { justify-content: flex-end; }
  /* Tap to open shows the full bet info: the row wraps, the bet drops onto its own full-width line
     with its full text, and the 1X2 chance percentages (hidden when collapsed) print. */
  table.board tr.match-row.expanded { flex-wrap: wrap; }
  table.board tr.match-row.expanded td.c-p { flex: 0 0 auto; min-width: 34px; padding: 2px 4px; }
  table.board tr.match-row.expanded td.c-p .pct { display: inline; font-size: 10px; }
  table.board tr.match-row.expanded td.c-tips { flex: 1 0 100%; min-width: 0; max-width: none; justify-content: flex-start; text-align: left; margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--line); }
  table.board tr.match-row.expanded td.c-tips .tip { display: flex; width: 100%; flex-wrap: wrap; justify-content: flex-start; overflow: visible; font-size: 12px; }
  table.board tr.match-row.expanded td.c-tips .tip-abbr { display: none; }
  table.board tr.match-row.expanded td.c-tips .tip-full { display: inline; }

  /* Expanded analyst info row spans full width — but stay hidden until toggled */
  table.board tr.info-row[hidden] { display: none; }
  table.board tr.info-row { display: block; }
  table.board tr.info-row td { display: block; padding: 12px; background: var(--panel-2); border-radius: 8px; margin: 0 0 6px; }
  table.board tr.info-row .info-panel { max-width: 100%; }

  /* Performance page tables: fit to width, hide low-priority columns */
  .table-scroll { overflow-x: visible; display: block; max-width: 100%; }
  table.bets { font-size: 12px; width: 100%; min-width: 0; }
  table.bets th, table.bets td { padding: 8px 5px; }
  table.bets .hide-mobile { display: none; }
}

/* Team-name marquee (mobile card overflow) — slow slide left and back, holding at both ends so
   the start and full tail both get read-time. Not scoped to a viewport: the .m-name-marquee
   class above is only ever added on mobile, so this is inert on desktop regardless. */
@keyframes m-name-marquee {
  0%, 12% { transform: translateX(0); }
  50% { transform: translateX(var(--marquee-shift, 0)); }
  88%, 100% { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
  table.board tr.match-row td.c-match .m-name.m-name-marquee { text-overflow: ellipsis; }
  table.board tr.match-row td.c-match .m-name.m-name-marquee .m-name-inner { animation: none; }
}

@media (max-width: 480px) {
  .card { padding: 12px 12px; }
  .card h1, .card h2 { font-size: 14px; }
  table.board { font-size: 12px; }
  table.board td, table.board th { padding: 6px 4px; }
  /* Must match the 768px rule's specificity (0,2,3) or it silently loses and the smallest,
     most load-bearing text on the board stays at 10px. */
  table.board tr.match-row td.c-date { width: 40px; font-size: 11px; }
  .c-match { min-width: 140px; }
  .c-tips { min-width: 80px; font-size: 11px; }
  .c-stake { font-size: 11px; }
  .c-ft { font-size: 11px; }
  .m-name { font-size: 12px; }
}

/* ── zulubet-style main board ─────────────────────────────────────────────── */
/* Match rows are the content: dark (--bg) cards, rounded, floating on the card's own
   --panel surface with a hair of vertical breathing room. The league strip is the surface
   itself (--panel), borderless, so it recedes into the background — the crest hangs a few
   px left of the rows as a barely-there tab. `separate` (not `collapse`) is what buys us
   the per-row rounding and the row gap. */
.board-mhead { display: none; }  /* desktop uses the real <thead>; shown only on mobile */
table.board { width: 100%; max-width: 100%; margin: 0 auto; border-collapse: separate; border-spacing: 0 3px; font-size: 13.5px; }
table.board th { text-align: left; color: var(--muted); font-weight: 600; font-size: 11px;
  text-transform: uppercase; letter-spacing: .04em; padding: 4px 8px 3px; }
/* All header labels top-align on one line — the single-tier headers (Date/Match/Tips/Stake/FT)
   level with the Chance/Odds group labels — so the 1 X 2 sub-headers tuck underneath. */
table.board thead th { vertical-align: top; }
table.board td { padding: 9px 8px; border: 0; vertical-align: middle; background: var(--board-row-a); }
table.board tr.match-row td:first-child { border-radius: 6px 0 0 6px; }
table.board tr.match-row td:last-child { border-radius: 0 6px 6px 0; }
/* League title = a subtle band (--panel-2: #d0dbd4 light / #1d222b dark) that separates each
   competition; the match rows below sit on the lighter --bg so title and first row alternate. */
table.board tr.league-group-row td { padding: 10px 12px 8px 2px; background: var(--lg-band);
  border: 0; border-radius: 6px 6px 0 0; }
/* Indent the match rows' first column so each league title hangs a little to their left —
   makes the start of a new league easier to spot. Header matches so columns stay aligned. */
table.board thead th.c-date, table.board tr.match-row td.c-date { padding-left: 18px; }

/* Live: a pulsing red dot in the Date cell, PLUS the elapsed minute.
   The dot alone used to be the whole treatment, with the minute surviving only in a title
   attribute. On a phone there is no hover, so a live match rendered as an unlabelled red dot and
   the one fact a reader wants (how long it has been running) was unreachable. The badge is now
   shown wherever the dot is; it is the same markup the live refresh already swaps in. */
.live-tag { display: inline-block; font-size: 10px; font-weight: 800; color: #ef4444;
  line-height: 1.2; letter-spacing: .01em; }
.live-time-m { display: inline-flex; align-items: center; gap: 4px;
  color: #ef4444; font-weight: 800; font-size: 11px; white-space: nowrap; }
.live-dot-m { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #ef4444;
  animation: live-pulse 1.4s ease-in-out infinite; flex: 0 0 auto; }
@media (prefers-reduced-motion: reduce) { .live-dot-m { animation: none; } }
@media (max-width: 768px) {
  /* Zero the desktop 18px time indent on mobile so the kick-off time starts flush left under the
     TIME header (this rule lives after the desktop padding-left:18px so it wins). */
  table.board tr.match-row td.c-date { padding-left: 0; }
  table.board tr.match-row.is-live td.c-date { padding-left: 0; }
  /* The Live filter button reads as an oversized pill on a phone; make it match its sibling nav. */
  .live-btn { font-size: 12px; padding: 5px 8px; }
  .live-btn .live-dot { width: 6px; height: 6px; }
}

/* Live filter button — a toggle: pressed inverts foreground and background. */
.live-btn[aria-pressed="true"] { background: var(--text); color: var(--bg); border-color: var(--text); }
.live-btn[disabled] { opacity: .45; cursor: not-allowed; }
/* Country reads as a quiet subtitle after the league name (not a badge). */
.lg-country { display: inline-block; margin-left: 8px; color: var(--muted); font-size: 11px;
  font-weight: 600; letter-spacing: .02em; vertical-align: middle; }

/* Date / time column */
.c-date { white-space: nowrap; color: var(--muted); font-weight: 700; font-size: 12px;
  font-variant-numeric: tabular-nums; width: 46px; }
.ft-tag { display: inline-block; padding: 0 5px; border-radius: 4px; background: rgba(139,148,163,.18);
  color: var(--muted); font-size: 10.5px; font-weight: 800; }
.tomorrow-tag {
  display: inline-block; font-size: 11px; color: var(--muted); background: var(--panel-2);
  padding: 2px 6px; border-radius: 3px; margin-left: 6px; font-weight: 600;
}
/* Kickoff date subtitle under the time (empty for today's matches → collapses). */
.ko-date { display: block; font-size: 10px; font-weight: 600; color: var(--muted); line-height: 1.1; }
.ko-date:empty { display: none; }

/* Match column — home and away side by side, one line. `width:1%` makes it shrink to its
   content instead of soaking up the table's spare width, which was opening a ~330px void
   between the team names and the Chance columns. The slack goes to Tips (see .c-tips). */
.c-match { min-width: 210px; max-width: 300px; }
.m-teams { display: flex; align-items: center; gap: 0; }
.m-team { display: inline-flex; align-items: center; gap: 6px; vertical-align: middle; min-width: 0; }
.m-vs { color: var(--muted); margin: 0 7px; flex-shrink: 0; }
.m-logo { width: 16px; height: 16px; object-fit: contain; flex-shrink: 0; }
/* Long club names wrap onto a second line instead of stretching the match column (and with
   it the whole board) past the card edge. Short names still sit on one line. */
.m-name { font-weight: 600; overflow-wrap: break-word; }

/* grouped two-tier header (Chance / Odds over 1 X 2). `border-spacing: 0 3px` gaps the match
   rows, and the same gap falls BETWEEN the two header rows — while stuck, scrolling content
   used to show through that 3px seam under Chance/Odds. The sub-row (1 X 2) cells extend their
   own --panel background UP 3px to close the seam flush; no box-shadow edge to show as a line. */
table.board thead .grp { text-align: center; border-bottom: 0; padding: 4px 8px 0;
  background: var(--panel); }
table.board thead .sub th { padding-top: 0; padding-bottom: 2px; font-size: 10px; text-align: center;
  /* Cover the WHOLE seam. When the two header rows are stuck, the offsetTop-based top2 leaves a
     3-4px gap that scrolling rows used to shimmer through under Chance/Odds; -3px only covered
     part of it. -7px overlaps into the group row (same --panel colour) so nothing shows. */
  box-shadow: 0 -7px 0 0 var(--panel); }

/* Sticky header on all screen sizes */
table.board thead th { position: sticky; z-index: 10; background: var(--panel); top: 0; }

/* Sticky header. `.table-scroll` uses overflow-x, and a non-visible overflow on one axis
   computes the other to `auto` — which would make the header stick to a container that
   never scrolls vertically, i.e. not stick at all. So on screens wide enough to need no
   horizontal scroll we drop the scroll container entirely and stick to the viewport,
   offset by the sticky topbar's height. */
@media (min-width: 1180px) {
  .board-scroll { overflow: visible; }
  table.board thead th { top: var(--board-head-top, 70px); }
  /* Offsets are measured at runtime (app.js) — the topbar is flex-wrap and its height
     changes with viewport/auth state, and the two header rows have different heights, so
     any hard-coded value leaves the sub-row overlapping or floating. */
  table.board thead tr:first-child th { top: var(--board-head-top, 70px); }
  table.board thead tr.sub th { top: var(--board-head-top2, 100px); }
}
/* Sortable headers — the whole label is the hit target. The Tips header shares the look but
   is a filter, not a sort, so it takes the same base chrome without the sort arrow. */
.sort-th, .tips-filter-btn {
  appearance: none; background: none; border: 0; padding: 6px 4px; margin: 0; font: inherit;
  color: inherit; text-transform: inherit; letter-spacing: inherit; cursor: pointer;
  display: inline-flex; align-items: center; gap: 4px; border-radius: 3px; transition: all .15s;
}
.sort-th:hover, .tips-filter-btn:hover { color: var(--text); background: rgba(59,130,246,.08); }
.sort-th:focus-visible, .tips-filter-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.sort-th[data-active] { color: var(--accent); font-weight: 600; }
.sort-th[data-active]::after { content: " ↓"; font-size: 9px; }
.sort-th[data-active][data-dir="asc"]::after { content: " ↑"; }
/* Tips is a filter: its active state is the ✓/✗ in the label plus an accent tint. */
.tips-filter-btn[aria-pressed="true"] { color: var(--accent); font-weight: 600; }
/* 3-way Value filter: "with" inverts (like Live), "without" uses the neg accent so the
   two active states read differently at a glance. The Live button styles its dot separately. */
.toggle-btn[data-state="with"]    { background: var(--text); color: var(--bg); border-color: var(--text); }
.toggle-btn[data-state="without"] { background: var(--neg); color: var(--bg); border-color: var(--neg); }
.toggle-btn[disabled] { opacity: .45; cursor: not-allowed; }

/* 1 X 2 probability columns — the whole cell fills green by magnitude rather than wearing a
   border: a strong chance reads dark, a fair one light, an unlikely one stays uncoloured. */
.c-p { text-align: center; width: 40px; }
.prob { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--text); }
.prob .pct { font-size: 9px; color: var(--muted); margin-left: 1px; }
table.board td.prob-mid { background: rgba(52,211,153,.14); }   /* fair chance — pale */
table.board td.prob-hi  { background: rgba(22,128,90,.85); }    /* strong chance — deep */
.prob-hi .pct { color: #cdebdd; }
.prob-top { font-weight: 800; }
/* Thin history: the number is still our honest estimate, it just has almost nothing behind it.
   Dimmed rather than hidden or recoloured — a warning colour would read as "danger", and the
   point is only that this cell is less certain than its neighbours. The tooltip carries the
   count and the bookmaker's own number. */
.prob-thin { color: var(--prob-thin); }
.prob-thin .thin-dot { margin-left: 2px; font-weight: 800; opacity: .75; }

/* Tips — one line per tip; selection + tip odds + result mark. */
.c-tips { min-width: 150px; width: 1%; white-space: nowrap; }
/* Trailing spacer: the ONLY elastic column. Every real column therefore sizes to its own
   content and packs left, and the table's spare width collects harmlessly on the right
   instead of tearing a gap open in the middle of a row. */
.c-pad { width: 100%; padding: 0; }
.tip { display: flex; align-items: center; gap: 6px; line-height: 1.7; white-space: nowrap; }
.tip-sel { font-weight: 700; }
.tip-mkt { color: var(--muted); font-size: 11px; }
.tip-odd { color: var(--text); font-variant-numeric: tabular-nums; font-size: 12px; }
/* Bet outcome next to a tip: a rounded bar, not a tick or a cross.
   A green checkmark sitting beside a red scoreline read as a contradiction - the tick means
   "our bet won" while the red means "the home side lost", and at a glance the two symbols argue
   with each other. A plain colour bar carries the same one bit without pretending to be a
   verdict on the match. Colour is never the only cue: every bar keeps a title/aria-label. */
.tip-mark { display: inline-block; width: 3px; height: 13px; border-radius: 999px;
  vertical-align: -2px; margin-left: 5px; flex: none; }
.tip-mark.ok { background: var(--pos); }
.tip-mark.no { background: var(--neg); }
.tip-mark.void { background: var(--muted); opacity: .55; }
/* Coarse pointers get a slightly taller bar so it stays legible at arm's length. */
@media (pointer: coarse) { .tip-mark { height: 15px; width: 3.5px; } }
/* Tip text variants: full on desktop, abbreviated on mobile */
.tip-full { display: inline; }
.tip-abbr { display: none; }
/* Stake sits right next to Tips (no wide gap) */
.c-stake { text-align: left; padding-left: 6px; white-space: nowrap; }
.c-stake .stake, .c-stake .stake-fixed { font-weight: 700; }
/* Odds outcome columns */
.c-o { text-align: center; width: 46px; font-variant-numeric: tabular-nums; }
.ln { line-height: 1.7; white-space: nowrap; }

/* FT result + success block */
.c-ft { white-space: nowrap; }
/* Glyph, not a bar: ✓ when the top 1X2 chance matched the result, ✗ when it missed. A visible mark
   (not colour alone) so the win/loss reads without relying on colour and works on touch. */
/* Did our top chance match the result? A rounded bar, not a tick or a cross.
   Sitting immediately after the full-time score, a green checkmark next to a red result read as
   a contradiction: the tick meant "our pick matched" while the red meant "the home side lost",
   and the two symbols argued with each other in the same breath. A colour bar carries the one
   bit it is actually carrying, and stops competing with the scoreline for meaning. */
.ft-ind { display: inline-block; margin-left: 7px; vertical-align: -2px;
  width: 3px; height: 13px; border-radius: 999px; }
/* NOT the money palette. This bar says whether the model's top 1X2 chance matched the result,
   and it is drawn on every settled match; the money block beside it is drawn only on a match that
   carried a bet. In --pos/--neg they were indistinguishable, and there are five times more bars
   than bets: 582 against 106 on 2026-08-22, pointing opposite ways on a day that lost money while
   60% of its bets won. Green and red on this site mean profit and loss. This is not that, so it
   is blue for a hit and grey for a miss, and the board carries a key. */
.ft-ind-win { background: var(--info); }
.ft-ind-lose { background: var(--muted); opacity: .5; }
@media (pointer: coarse) { .ft-ind { height: 15px; width: 3.5px; } }
.ft-score { font-weight: 800; font-variant-numeric: tabular-nums; }
.board-key { margin: 8px 2px 0; color: var(--muted); font-size: .78rem; line-height: 1.7; }
.board-key .ft-ind { margin: 0 3px 0 0; vertical-align: -1px; }
/* No tip on a match → a faint grey ×; a cancelled/abandoned match → a cancel glyph at FT. */
.no-tip { color: var(--muted); opacity: .55; font-weight: 700; }
.ft-cancel { color: var(--muted); font-size: 14px; font-weight: 700; }
.lg-fav { margin-right: 5px; font-size: 13px; vertical-align: middle; }
.res-block { display: inline-block; margin-left: 6px; min-width: 10px; padding: 1px 6px;
  border-radius: 4px; font-size: 11px; font-weight: 700; vertical-align: middle; }
.res-win { background: rgba(52,211,153,.18); }
.res-lose { background: rgba(248,113,113,.18); }
.res-flat { background: transparent; }
.res-block .profit-fixed.pos { color: var(--pos); }
.res-block .profit-fixed.neg { color: var(--neg); }
.tag-cs3 { display: inline-block; padding: 0 5px; border-radius: 4px; background: rgba(59,130,246,.16);
  color: var(--accent); font-size: 10px; font-weight: 700; }

/* ── board: Live filter button ────────────────────────────────────────────────
   Shown by app.js only when the board actually contains a live match, so it is not a
   permanently dead control (most of the day nothing is being played). The dot blinks slowly
   to read as "something is happening right now" without being a distracting strobe. */
.live-btn .live-dot {
  display: inline-block; width: 8px; height: 8px; margin-right: 6px; border-radius: 50%;
  background: #ef4444; vertical-align: middle;
  animation: live-pulse 2s ease-in-out infinite;
}
@keyframes live-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) {
  .live-btn .live-dot { animation: none; }
}

/* Mobile board header: the Tips cell is a real button (the desktop <thead> filter is hidden on
   mobile), styled to look like the surrounding header cells rather than a chunky button. */
.board-mhead .mh-filter {
  appearance: none; background: none; border: 0; padding: 0; font: inherit; color: inherit;
  cursor: pointer; text-align: right;
}
.board-mhead .mh-filter[data-state="with"], .board-mhead .mh-filter[data-state="without"] {
  color: var(--accent); font-weight: 700;
}
/* Mobile sort headers: same look as the plain header cells, tappable, with an active-column tint
   and a compact direction arrow (mirrored from the desktop <thead> by app.js). No text-align here
   so each cell's own alignment (set above) wins. */
.board-mhead .mh-sort {
  appearance: none; background: none; border: 0; padding: 0; font: inherit; color: inherit;
  text-transform: inherit; letter-spacing: inherit; cursor: pointer; white-space: nowrap;
}
.board-mhead .mh-sort[data-active] { color: var(--accent); font-weight: 700; }
.board-mhead .mh-sort[data-active]::after { content: "↓"; font-size: 8px; margin-left: 1px; }
.board-mhead .mh-sort[data-active][data-dir="asc"]::after { content: "↑"; }


/* Gate-conflict verdict. The panel used to list contradictions with no size and no
   recommendation, so it read as trivia rather than as something to act on. */
.mr-verdict { border-radius: 10px; padding: 12px 14px; margin: 12px 0 16px;
  border: 1px solid var(--line); background: var(--panel-2); }
.mr-verdict.bad { border-left: 4px solid var(--neg); }
.mr-verdict.ok { border-left: 4px solid var(--pos); }
/* Neither red nor green on purpose: the measurement did not come out either way, and colouring
   an inconclusive result is how it gets read as a finding. */
.mr-verdict.flat { border-left: 4px solid var(--muted); }
.mr-gate-note { margin-top: 12px; padding-top: 10px; border-top: 1px dashed var(--line); }
.mr-verdict-head { font-weight: 800; font-size: 14px; margin-bottom: 6px; }
.mr-verdict p { font-size: 13px; margin: 6px 0; }
.mr-verdict-do { border-top: 1px dashed var(--line); padding-top: 8px; }
.mr-verdict code { font-size: 12px; padding: 1px 4px; border-radius: 4px;
  background: var(--panel); border: 1px solid var(--line); }

/* Results page lede. Sits between the H1 and the record strip, so it is the first thing read
   and sets up why the strip below it shows losing windows as well as winning ones. */
.perf-lede { margin: 0 0 16px; color: var(--muted); font-size: .95rem; max-width: 62ch;
  line-height: 1.55; }

/* A figure that carries its own explanation on hover. The dotted underline is the only signal
   a title attribute gets, and without it nobody discovers the tooltip. */
.has-tip { border-bottom: 1px dotted var(--muted); cursor: help; }

/* The record strip on the results page is not a link: it already IS the results page. Same
   look, no hover lift and no pointer. */
.track-strip.is-static { cursor: default; }
.track-strip.is-static:hover { transform: none; }

/* The heatmap's own control: how much red pure chance would produce. Neutral by default
   because "indistinguishable from noise" is the normal state, not an alarm. */
.mr-noise { margin: 10px 0 14px; padding: 10px 14px; border-radius: 8px; font-size: 13px;
  color: var(--muted); background: var(--panel-2); border-left: 3px solid var(--muted); }
.mr-noise b { color: var(--text); }
.mr-noise.beats { border-left-color: var(--pos); }

/* Admin: markets hidden from readers while still being placed. Bordered as a group because it
   is editorial, not one more per-market gate, and mixing the two invited misreading. */
.mr-suppress { border: 1px solid var(--line); border-radius: 10px; padding: 10px 14px 14px;
  margin: 14px 0; }
.mr-suppress legend { font-size: 12px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted); padding: 0 6px; }
.mr-supp-grid { display: flex; flex-wrap: wrap; gap: 8px 20px; margin-top: 8px; }
.mr-supp-now { margin-top: 10px; }

/* Cells the live gate can never admit. Hatched rather than hidden: the settled result is still
   real and worth reading, but nothing there is bettable under the current settings. */
.hcell.shut { position: relative; opacity: .40; }
.hcell.shut::after { content: ""; position: absolute; inset: 0; border-radius: inherit;
  background: repeating-linear-gradient(45deg, transparent 0 3px, rgba(127,127,127,.55) 3px 4px);
  pointer-events: none; }

/* An accepted warning is folded down, not deleted: the decision stays visible and reversible,
   and it reappears in full the moment its own numbers change. */
.mr-conflict.is-acked { opacity: .55; }
.mr-ack { float: right; display: flex; align-items: center; gap: 8px; margin-left: 12px; }
.mr-ack-on { font-size: 11.5px; color: var(--muted); white-space: nowrap; }

/* The markets form owns only its submit: the checkboxes sit in the fieldset above it and reach
   it by form=, so the two switch groups read as one block instead of two nested forms. */
.mr-mktform { margin-top: 10px; }

/* A flat bound that clips the bands drawn above it. Flagged at the field rather than in a
   paragraph below, because the whole failure is that the two disagree without showing it. */
.mp-legacy.is-clash input { border-color: var(--warn, #f59e0b); }
.mp-fix { font: inherit; font-size: 11px; font-weight: 700; cursor: pointer; margin-left: 4px;
  padding: 2px 7px; border-radius: 999px; white-space: nowrap;
  color: var(--warn, #f59e0b); background: rgba(245,158,11,.12);
  border: 1px solid rgba(245,158,11,.35); }
.mp-fix:hover { background: rgba(245,158,11,.22); }

/* Derived, not typed: price lives in the band table and these two only report its span. Styled
   as output rather than input so nobody tries to type in them and wonders why it does nothing. */
.mp-derived input { background: transparent; border-style: dashed; color: var(--muted);
  cursor: default; }
.mp-derived-note { font-style: italic; }

/* The post-mortem's finding, with the switch that acts on it. Bordered so it reads as the
   actionable part of a page that is otherwise a report. */
.pm-act { border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; margin: 18px 0; }
.pm-act-head { display: flex; align-items: center; justify-content: space-between; gap: 14px;
  flex-wrap: wrap; margin-bottom: 6px; }

/* The weekly search's verdict, folded in from the retired optimizer page. */
.mr-optline { font-size: 13px; margin: 0 0 10px; padding: 9px 12px; border-radius: 8px;
  background: var(--panel-2); border-left: 3px solid var(--muted); }

/* The only warning on the command centre about money moving right now: a market staking while
   the code ships it off for an evidenced reason. Louder than the gate-shape notices. */
.mr-override { border: 1px solid rgba(226,106,106,.4); background: rgba(226,106,106,.08);
  border-radius: 10px; padding: 12px 16px; margin: 10px 0 14px; font-size: 13px; }
.mr-override ul { margin: 8px 0 6px; padding-left: 20px; }
.mr-override li { margin-bottom: 5px; }

/* ── favourite-club combobox ──
   The suggestion's second line (country · competition, plus Women / Reserves) is the reason this
   is not a <datalist>: without it the list is as ambiguous as the plain text box it replaced. */
.club-picker { position: relative; }
/* Chips: one per chosen club, each carrying its own hidden favorite_club value. They sit ABOVE
   the input rather than inside it — an inline token field has to re-implement caret and focus
   behaviour, and gets it subtly wrong on mobile keyboards. */
.club-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.club-chips:not(:empty) { margin-bottom: 7px; }
/* A chip is the ACTIVE state: filled accent with a tick, so a chosen club reads as chosen at
   a glance rather than as another label near the box. */
.club-chip { display: inline-flex; align-items: center; gap: 5px; font-size: 13px;
  background: rgba(var(--accent-rgb), .2); border: 1px solid rgba(var(--accent-rgb), .55);
  color: var(--text); border-radius: 999px; padding: 3px 4px 3px 10px; line-height: 1.5;
  font-weight: 600; }
.club-chip::before { content: "\2713"; color: var(--accent); font-weight: 700; font-size: 11px; }
.club-chip-x { background: none; border: 0; color: var(--muted); cursor: pointer;
  font-size: 17px; line-height: 1; padding: 0 6px; border-radius: 999px; }
.club-chip-x:hover, .club-chip-x:focus-visible { color: #f87171; outline: none; }
@media (pointer: coarse) { .club-chip { font-size: 14px; padding: 5px 5px 5px 12px; }
  .club-chip-x { font-size: 20px; padding: 0 8px; } }
/* A suggestion you already have. Kept in the list rather than hidden, because searching for a
   club you had picked showed an empty dropdown, which reads as "it did not save". */
.club-list li.chosen .club-name { color: var(--accent); }
.club-tick { color: var(--accent); font-weight: 700; margin-left: 6px; }
.club-list li.chosen:hover .club-tick::after { content: "  remove"; font-weight: 500;
  font-size: 11px; color: var(--muted); }
/* The picker owns the field's full width. Inside .acct-field (a flex column) the wrapper
   shrank to its content and squeezed the input to about a third of the row. */
.club-picker { width: 100%; }
.club-picker input { width: 100%; box-sizing: border-box; }
/* Field subtitle: what the setting DOES, above the control rather than under it, where it was
   read after the decision instead of before it. */
.field-sub { font-size: 12px; color: var(--muted); font-weight: 400; line-height: 1.45;
  margin: -2px 0 2px; }
/* Inside a tickbox the label is a span, so the subtitle needs to be told to take its own line;
   everywhere else it follows a block-level <label> and already does. Without this the opt-in
   read as one run-on sentence starting "Important update emails Not a newsletter". */
.login-card .checkbox-label .field-sub { display: block; margin: 3px 0 0; }
.login-card .checkbox-label > span > .field-sub + * { margin-top: 0; }

.club-list {
  position: absolute; z-index: 60; top: calc(100% + 4px); left: 0; right: 0;
  margin: 0; padding: 4px; list-style: none; max-height: 260px; overflow-y: auto;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 12px 32px rgba(0,0,0,.45);
}
.club-list li {
  display: flex; flex-direction: column; gap: 1px; padding: 8px 10px;
  border-radius: 7px; cursor: pointer;
}
.club-list li.on { background: rgba(var(--accent-rgb), .14); }
.club-name { font-size: 13.5px; font-weight: 600; color: var(--text); }
.club-meta { font-size: 11.5px; color: var(--muted); }
@media (pointer: coarse) { .club-list li { padding: 11px 10px; } }

/* ── danger zone (account page) ──
   Destructive actions get their own visually separated block rather than sitting in the flow of
   ordinary settings, so nothing here can be clicked by momentum. */
.danger-zone { border: 1px solid rgba(211,47,47,.45); padding: 0; overflow: hidden; }
.dz-banner {
  display: flex; align-items: center; gap: 9px;
  background: rgba(211,47,47,.12); color: #e0603a;
  padding: 11px 18px; font-size: 12px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .08em;
  border-bottom: 1px solid rgba(211,47,47,.35);
}
.dz-warn { flex: 0 0 auto; }
.dz-row { display: flex; gap: 26px; flex-wrap: wrap; align-items: flex-start;
  justify-content: space-between; padding: 18px; }
.dz-what { flex: 1 1 320px; min-width: 0; }
.dz-what h3 { margin: 0 0 6px; font-size: 15px; font-weight: 700; color: var(--text); }
.dz-what p { margin: 0 0 10px; color: var(--muted); font-size: 13px; line-height: 1.55; }
.dz-list { margin: 0; padding-left: 18px; color: var(--muted); font-size: 13px; line-height: 1.7; }
.dz-form { flex: 0 0 auto; display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
.dz-check { font-size: 12.5px; color: var(--muted); }
.dz-btn { background: #d32f2f; color: #fff; border: 1px solid #d32f2f; font-weight: 600; }
.dz-btn:hover { background: #b71c1c; border-color: #b71c1c; color: #fff; }

.dz-dialog {
  border: 1px solid var(--line); border-radius: 14px; background: var(--panel);
  color: var(--text); padding: 22px; max-width: 440px; width: calc(100% - 32px);
  box-shadow: 0 24px 60px rgba(0,0,0,.5);
}
.dz-dialog::backdrop { background: rgba(0,0,0,.55); }
.dz-dialog h2 { margin: 0 0 10px; font-size: 18px; font-weight: 700; }
.dz-dialog p { margin: 0 0 10px; color: var(--muted); font-size: 13.5px; line-height: 1.6; }
.dz-dialog-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; }
@media (max-width: 560px) {
  .dz-row { flex-direction: column; }
  .dz-form { width: 100%; }
  .dz-dialog-actions .btn { flex: 1 1 100%; }
}

/* ── API quota strip (command centre) ──
   The reset sits with the number on purpose: a percentage on its own does not say whether it is
   a normal evening or a problem, and that depends entirely on how long it has to last. */
.quota-strip { display: flex; gap: 12px; flex-wrap: wrap; margin: 4px 0 16px; }
.quota-card {
  flex: 1 1 220px; min-width: 0; padding: 12px 14px; border-radius: 12px;
  background: var(--panel-2); border: 1px solid var(--line);
}
.quota-card.warn { border-color: rgba(251,191,36,.5); background: rgba(251,191,36,.07); }
.quota-card.crit { border-color: rgba(224,96,58,.55); background: rgba(224,96,58,.09); }
.q-name { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted); }
.q-big { font-size: 25px; font-weight: 800; font-variant-numeric: tabular-nums; margin-top: 2px; }
.q-unit { font-size: 12px; font-weight: 600; color: var(--muted); }
.q-big.q-none { font-size: 16px; color: var(--muted); font-weight: 600; }
.quota-card.warn .q-big { color: #fbbf24; }
.quota-card.crit .q-big { color: #e0603a; }
.q-sub { font-size: 12px; color: var(--muted); margin-top: 1px; }
.q-reset { font-size: 12px; margin-top: 8px; color: var(--text); }
.q-age { font-size: 11px; color: var(--muted); margin-top: 3px; }

/* Account header actions. Log out and the command centre are both actions, so both are buttons
   rather than one being a link in a list of facts about the account. */
.acct-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 14px; }

/* The one-paragraph explainer above the gate conflicts. Every conflict names the same setting,
   so the setting is explained once at the top rather than assumed five times. */
.mr-explain { font-size: 12.5px; line-height: 1.65; color: var(--text);
  background: var(--panel-2); border-radius: 8px; padding: 10px 13px; margin: 10px 0 4px; }
.mr-explain code { font-size: 12px; }

/* ── Opening offer ────────────────────────────────────────────────────────────
   Shared by the pricing page, the account page, signup and the board strip. Amber, the
   price-tag colour: green already means "trial active" and red means something is wrong,
   so neither was free to reuse for a sale. Hidden entirely when store.promo_live() is off,
   rather than greyed, because an expired offer on screen is worse than no offer. */
.offer-banner { display: flex; align-items: center; justify-content: center; gap: 10px;
  flex-wrap: wrap; background: rgba(245,158,11,.12); border: 1px solid rgba(245,158,11,.38);
  border-radius: 10px; padding: 12px 18px; margin: 0 auto 8px; max-width: 620px;
  font-size: 14px; text-align: center; line-height: 1.55; }
.offer-tag { flex: none; background: var(--warn); color: #1f1300; font-size: 11px;
  font-weight: 700; letter-spacing: .3px; padding: 3px 9px; border-radius: 999px;
  text-transform: uppercase; }
.offer-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 700;
  background: rgba(245,158,11,.18); border: 1px solid rgba(245,158,11,.45);
  border-radius: 6px; padding: 1px 7px; letter-spacing: .4px; }
.offer-inline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: 13px; color: var(--text); margin: 0 0 10px; line-height: 1.5; }
/* The struck-through old price on the yearly card. Muted and smaller so the number people
   actually pay stays the dominant one. */
.pricing-price .was { color: var(--muted); font-size: .62em; font-weight: 600;
  margin-right: 6px; text-decoration-thickness: 2px; }
/* Board strip: full width of the page frame, one line, sits under the header. */
.offer-strip { display: flex; align-items: center; justify-content: center; gap: 10px;
  flex-wrap: wrap; background: rgba(245,158,11,.1); border: 1px solid rgba(245,158,11,.32);
  border-radius: 10px; padding: 9px 14px; margin: 0 0 14px; font-size: 13px; line-height: 1.5; }
.offer-strip a { color: var(--warn); font-weight: 700; text-decoration: none; white-space: nowrap; }
.offer-strip a:hover { text-decoration: underline; }
@media (max-width: 560px) { .offer-strip { font-size: 12px; padding: 8px 11px; } }

/* ── Follow a team (board stars) ──────────────────────────────────────────────
   One star per SIDE of a fixture. Dim until hovered or followed, because two of these on
   every one of ~500 rows would otherwise be the loudest thing on a dense board. Followed
   stars stay amber at full strength: that is the state worth seeing at a glance. */
.fav-star { flex: none; background: none; border: 0; cursor: pointer; padding: 0 0 0 5px;
  font-size: 12px; line-height: 1; color: var(--line); transition: color .12s, transform .12s;
  -webkit-tap-highlight-color: transparent; }
.match-row:hover .fav-star { color: var(--muted); }
.fav-star:hover, .fav-star:focus-visible { color: var(--warn); transform: scale(1.25); outline: none; }
.fav-star.on, .match-row:hover .fav-star.on { color: var(--warn); }
.fav-star.busy { opacity: .45; pointer-events: none; }
/* The pop when a star is switched on. Short and once: this is a confirmation, not decoration. */
@keyframes fav-pop { 0% { transform: scale(1); } 45% { transform: scale(1.55); } 100% { transform: scale(1); } }
.fav-star.pop { animation: fav-pop .28s ease-out; }
@media (hover: none) {
  /* No hover on touch, so the control has to be visible and thumb-sized without it. */
  .fav-star { color: var(--muted); font-size: 13px; padding: 2px 2px 2px 7px; }
}

/* Toast for the star's refusals (not signed in, list full). Errors only: a successful star
   is its own feedback and does not need a message. */
.fav-toast { position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
  background: var(--panel); color: var(--text); border: 1px solid var(--line);
  border-radius: 10px; padding: 10px 16px; font-size: 13px; z-index: 60;
  box-shadow: 0 8px 26px rgba(0,0,0,.34); opacity: 0; transition: opacity .18s; max-width: 88vw; }
.fav-toast.show { opacity: 1; }
.fav-toast a { color: var(--accent); font-weight: 700; }


/* ── FAQ ──────────────────────────────────────────────────────────────────────
   The questions have their own page now (/faq) and the pricing page links to it, so the
   rules are shared. Copied VERBATIM: moving them line by line split .faq-fineprint across
   its own declarations and left it without a closing brace, which silently swallowed the
   rules after it. */
.faq-section { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 22px 24px; }
.faq-item { margin-bottom: 14px; }
/* An <h2>, so eight questions are navigable by heading instead of being eight anonymous divs.
   `font-size: inherit` and the margin reset keep it drawn exactly as the div was: the semantics
   changed, the design did not. */
.faq-q { font-size: inherit; font-weight: 700; margin: 0 0 4px; }
/* The things people find out too late. Last on the page and visually quieter than the FAQ,
   but not hidden: this is the honest place for them, not inside a cancel answer. */
.faq-fineprint { border: 1px solid var(--line); border-radius: 12px; padding: 16px 20px;
  margin: 22px auto 0; max-width: 620px; font-size: 13px; color: var(--muted);
  background: var(--panel); }
.faq-fineprint b { color: var(--text); display: block; margin-bottom: 7px; }
.faq-fineprint ul { margin: 0; padding-left: 18px; }
.faq-fineprint li { margin-bottom: 5px; line-height: 1.55; }
.faq-fineprint li:last-child { margin-bottom: 0; }
.faq-a { font-size: 14px; color: var(--muted); }
.faq-more { text-align: center; font-size: 14px; color: var(--muted); margin: 22px 0 0; }

/* ── Site footer ──────────────────────────────────────────────────────────────
   Moved here from dashboard.html's inline <style>, which is why the footer only ever rendered
   on the board: the markup AND its rules both lived on one page.

   Copied VERBATIM. The first attempt moved it line by line, which silently split every
   multi-line rule and — worse — lifted the three rules inside the 760px media query out of it,
   so the stacked, centred mobile footer applied at every width. A rule set is a block; moving
   one is a copy, not a filter.

   `footer` needs its page shell to sit at the bottom: body is a flex column with min-height
   100vh and main takes flex: 1. That lives in the page templates. */
/* Full-bleed bar whose content aligns to the same centred frame as the header (identical
   padding formula), so header and footer line up edge to edge. The top margin matches the
   board's own row gap, so the table-to-footer gap equals the yesterday-results-to-table gap. */
footer { background: var(--panel); border-top: 1px solid var(--line); font-size: 12px;
  flex-shrink: 0; margin-top: var(--page-gap);
  padding: 16px max(var(--page-pad-x), calc((100% - var(--content-max)) / 2 + var(--page-pad-x))); }
.footer-content { display: flex; justify-content: space-between; align-items: center; gap: 32px; }
.footer-rg { max-width: 1200px; margin: 14px auto 0; padding: 12px 24px 2px; border-top: 1px solid var(--line);
  font-size: 12px; line-height: 1.6; color: var(--muted); text-align: center; }
.footer-rg .rg-badge { display: inline-block; border: 1px solid currentColor; border-radius: 4px;
  padding: 0 5px; font-weight: 800; font-size: 11px; }
.footer-rg a { color: var(--muted); text-decoration: underline; }
.footer-rg .rg-sep { opacity: .5; }
.footer-left { flex: 1 1 0; text-align: left; }
.footer-right { flex: 1 1 0; }
.footer-copyright { color: var(--muted); margin-bottom: 12px; font-weight: 500; }
.footer-links { display: flex; gap: 20px; flex-wrap: wrap; justify-content: flex-start; }
.footer-right .footer-links { justify-content: flex-end; }
.footer-links a { color: var(--accent); text-decoration: none; }
.footer-links a:hover { text-decoration: underline; }
/* Socials — centre column of the footer; @oddsprey on IG / TikTok / WhatsApp */
.footer-social { flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: 10px; }
.footer-social-label { color: var(--muted); font-weight: 600; font-size: 12px; letter-spacing: .02em; }
.social-links { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.social-link { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border: 1px solid var(--line);
  border-radius: 999px; color: var(--text); text-decoration: none; font-size: 12px; font-weight: 600;
  background: var(--panel-2); transition: border-color .15s, color .15s, transform .1s; }
.social-link:hover { border-color: var(--accent); color: var(--accent); transform: translateY(-1px); }
.social-link svg { flex-shrink: 0; }
@media (max-width: 760px) {
  .footer-content { flex-direction: column; text-align: center; gap: 20px; }
  .footer-left, .footer-right { flex: none; text-align: center; }
  .footer-links, .footer-right .footer-links { justify-content: center; }
}

/* The board shell's server-rendered fixture list. It exists for the first paint and for
   crawlers, and is replaced wholesale by the real board, so it is styled to read as a plain
   list rather than to imitate a table it is not. */
.board-preview { list-style: none; margin: 0; padding: 0; }
.board-preview li { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 9px 14px; border-bottom: 1px solid var(--line); font-size: .9rem; }
.board-preview li:last-child { border-bottom: 0; }
.bp-time { color: var(--muted); font-variant-numeric: tabular-nums; min-width: 42px; }
.bp-teams { flex: 1 1 240px; }
.bp-comp { color: var(--muted); font-size: .82rem; }
.bp-odds { color: var(--muted); font-variant-numeric: tabular-nums; font-size: .82rem; }
@media (max-width: 560px) { .bp-comp { flex-basis: 100%; } }

/* Welcome card: confirms the clubs picked at signup and demonstrates what following one does.
   The demo rows are deliberately styled to read as a SAMPLE rather than as live fixtures, so
   nobody tries to click one. */
/* Appearance control (_theme_choice.html). A segmented pair, because there are two named
   states and neither of them is "off". Every colour is a token, so the control itself is
   readable in the theme it is not currently selecting. */
.theme-choice { display: inline-flex; gap: 0; border: 1px solid var(--line); border-radius: 10px;
  overflow: hidden; background: var(--panel-2); }
.theme-opt { appearance: none; border: 0; background: transparent; color: var(--muted);
  font: inherit; font-size: .88rem; font-weight: 600; padding: 8px 16px; cursor: pointer;
  display: inline-flex; align-items: center; gap: 7px; min-height: 40px; }
.theme-opt + .theme-opt { border-left: 1px solid var(--line); }
.theme-opt:hover { color: var(--text); }
.theme-opt[aria-pressed="true"] { background: var(--panel); color: var(--text); }
.theme-opt:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.theme-choice-note { margin: 8px 0 0; font-size: .82rem; }

.onboard-card { position: relative; padding: 20px 22px; margin-bottom: 14px; }
.onboard-card h2 { margin: 0 0 6px; font-size: 1.1rem; }
.onboard-card > p { margin: 0 0 14px; }
.onboard-demo { border: 1px dashed var(--line); border-radius: 12px; padding: 10px 12px;
  margin-bottom: 16px; background: var(--panel-2); }
.onboard-demo-lbl { color: var(--muted); font-size: .72rem; text-transform: uppercase;
  letter-spacing: .06em; margin-bottom: 8px; }
.ob-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; font-size: .9rem;
  border-top: 1px solid var(--line); }
.ob-row:first-of-type { border-top: 0; }
.ob-pinned { color: var(--text); }
.ob-row:not(.ob-pinned) { color: var(--muted); }
.ob-star { color: var(--accent); }
.ob-star.ob-off { color: var(--line); }
.ob-time { font-variant-numeric: tabular-nums; color: var(--muted); }
.ob-teams { flex: 1; }
.ob-tag { font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--accent); background: rgba(var(--accent-rgb), .12); padding: 3px 8px;
  border-radius: 999px; }
.onboard-form label { display: block; }
.onboard-theme { margin-top: 16px; }
.onboard-theme-lbl { display: block; font-weight: 600; }

.onboard-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
@media (max-width: 560px) { .ob-tag { display: none; } }

/* Latest settled calls, in the Results shell. Server-rendered, so it is the first thing a
   crawler and a slow connection see on the page that claims a public track record. */
.perf-recent { margin-bottom: 14px; }
.perf-recent h2 { margin: 0 0 12px; font-size: 1.05rem; }
.perf-recent-t { width: 100%; border-collapse: collapse; font-size: .88rem; }
.perf-recent-t th { text-align: left; color: var(--muted); font-weight: 600; font-size: .72rem;
  text-transform: uppercase; letter-spacing: .05em; padding: 0 10px 8px 0; }
.perf-recent-t td { padding: 8px 10px 8px 0; border-top: 1px solid var(--line);
  font-variant-numeric: tabular-nums; }
.pr-date, .pr-odds { color: var(--muted); white-space: nowrap; }
.pr-match { font-weight: 600; }
.pr-tip { color: var(--muted); }
.pr-res { font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  padding: 3px 9px; border-radius: 999px; }
.pr-res.won { background: rgba(52,211,153,.16); color: #34d399; }
.pr-res.lost { background: rgba(224,96,58,.16); color: #e0603a; }
@media (max-width: 640px) { .perf-recent-t .pr-tip { display: none; } }

/* ── Reading pages: the guide, About and Methodology ─────────────────────────────────────────
   These lived inside betting_guide.html's own <style>, which meant a second prose page had to
   copy them. That is how ten pages ended up with ten hand-written <head> blocks. Width, padding
   and centring still come from the shared `main.reading` frame; this is only the card and the
   typography inside it. The guide's infographic rules (.gfx) stay with the guide, since nothing
   else draws one. */
.guide-section { background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
  padding: 24px; margin-bottom: 20px; }
.guide-section h2 { margin-top: 0; color: var(--accent); }
.guide-section h3 { margin-top: 16px; margin-bottom: 8px; }
.guide-section p { margin: 8px 0; line-height: 1.6; }
.guide-section ul { margin: 8px 0; padding-left: 24px; }
.guide-section li { margin: 6px 0; }

/* A definition list with two columns, not data anyone sorts or scans numerically. */
.guide-table { width: 100%; border-collapse: collapse; margin: 14px 0 4px; font-size: .95rem; }
.guide-table th { text-align: left; color: var(--muted); font-weight: 600; font-size: .78rem;
  text-transform: uppercase; letter-spacing: .04em; padding: 0 12px 8px 0; }
.guide-table td { padding: 10px 12px 10px 0; border-top: 1px solid var(--line);
  vertical-align: top; line-height: 1.55; }
.guide-table td:first-child { white-space: nowrap; }

/* Reading osprey sitting ON the intro section's bottom edge, half above it and half below. Out
   of flow so it never reflows the copy; the section keeps right padding so the text never runs
   under it, and the gap below is widened so it does not collide with the next section. */
.guide-intro { position: relative; padding-right: 196px; margin-bottom: 58px; }
.guide-osprey { position: absolute; right: 58px; bottom: 0; width: 138px;
  transform: translateY(42%); pointer-events: none; z-index: 2; }
.guide-osprey .osprey-mascot { width: 100%; height: auto; }
@media (max-width: 860px) {
  /* the bird eats too much of the line at this width — pull it back to the corner */
  .guide-intro { padding-right: 140px; }
  .guide-osprey { right: 16px; width: 112px; }
}
@media (max-width: 620px) {
  .guide-section { padding: 16px; }
}

/* ── Continue with Google ────────────────────────────────────────────────────────────────────
   On the login page and in the header's sign-in dialog. Rendered only when the credentials are
   configured, so these rules match nothing on an unconfigured deploy. */
.oauth-row { margin-top: 14px; }
/* Leading a form rather than following a submit button: the rule below the buttons separates
   them from the fields, so it needs the space under it, not over it. */
.oauth-row.or-below { margin-top: 4px; }
.oauth-row.or-below .oauth-or { margin: 14px 0 4px; }
.oauth-consent { margin: 10px 0 8px; }
.oauth-or { display: flex; align-items: center; gap: 10px; color: var(--muted);
  font-size: .8rem; margin: 4px 0 12px; }
.oauth-or::before, .oauth-or::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.oauth-row .oauth-btn + .oauth-btn { margin-top: 10px; }
.oauth-btn { display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; background: var(--panel); border: 1px solid var(--line); color: var(--text);
  /* 44px, the same touch target every other control on a phone gets. */
  min-height: 44px; text-decoration: none; font-weight: 600; }
.oauth-btn:hover { border-color: var(--accent); }
.oauth-btn svg { flex: none; }

/* ── Admin surface strip ─────────────────────────────────────────────────────────────────────
   The admin pages are one system split across several URLs, and before this every one of them
   was reachable only from the command centre or by typing the address. The strip is included by
   each of them so any surface leads to every other, and the page you are on is drawn as text
   rather than a link that goes nowhere. */
.adm-nav { display: flex; flex-wrap: wrap; gap: 6px 20px; align-items: flex-end; margin: 0 0 14px; }
.adm-group { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
/* Specificity note: `.adm-nav span` below is (0,1,1) and would win over a bare
   `.adm-group-label` (0,1,0), drawing the heading as another bordered pill. Scoped to
   `.adm-nav .adm-group-label` (0,2,0) so the heading reads as a heading. */
.adm-nav .adm-group-label { font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); border: 0; border-radius: 0; padding: 0 2px 0 0; font-weight: 600; }
.adm-nav a, .adm-nav span { font-size: 12px; padding: 4px 11px; border: 1px solid var(--line);
  border-radius: 999px; color: var(--text); text-decoration: none; }
.adm-nav a:hover { border-color: var(--accent); color: var(--accent); }
.adm-nav span.here { border-color: var(--accent); color: var(--accent); font-weight: 700; }
.adm-nav a.out { border-style: dashed; color: var(--muted); }

/* ── .mr-table ───────────────────────────────────────────────────────────────────────────────
   The admin diagnosis tables. The class was on eleven tables across the command centre and the
   governance page and matched no rule anywhere, so every one of them rendered as a browser
   default table: no rules, no alignment on the numbers, headers the same weight as the data.
   Defined here rather than in either page because both use it, and a copy in one template would
   drift the moment the other changed. */
.mr-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.mr-table th { text-align: left; font-weight: 600; color: var(--muted); font-size: 11.5px;
  letter-spacing: .02em; padding: 6px 12px 6px 0; border-bottom: 1px solid var(--line);
  white-space: nowrap; }
.mr-table td { padding: 8px 12px 8px 0; border-bottom: 1px solid var(--line);
  vertical-align: top; }
.mr-table tr:last-child td { border-bottom: 0; }
.mr-table td.num, .mr-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
