/* STYLES — TA-533 fourth extraction pilot. Every rule dashboard/index.html previously carried in its
   own inline <style> block, moved verbatim (byte-for-byte apart from the extraction itself — no rule
   reordered, renamed, or rewritten). Pre-extraction audit (2026-09-27) confirmed this is safe to move:
   zero url(), @import, or @font-face in the block; the only external font reference is var(--font-num)
   pointing at "Oswald", loaded via separate <link rel="stylesheet" href="https://fonts.googleapis.com/...">
   tags in <head> that are untouched by this move (a Google Fonts URL, not a relative one — nothing here
   depends on this file's own location); no data: URIs; and no test parses this CSS directly out of
   dashboard/index.html (checked test/ for style/.css references before extracting).

   Loaded via <link rel="stylesheet" href="/styles.<hash>.css"> in <head>, BEFORE #app and any rendered
   content — same position the inline block held, so first-paint styling is unchanged.

   ⚠ ROOT-RELATIVE HREF IS REQUIRED, NOT COSMETIC — same reasoning as every extracted JS asset.
   dashboard/index.html is also served at rewritten URLs like /i/<token> (netlify.toml's "/i/*" ->
   /index.html rewrite). A bare "styles.HASH.css" reference resolves against THAT path to
   /i/styles.HASH.css, which the same wildcard rewrite would also match, serving index.html's HTML back
   as the "stylesheet" and breaking every invite link before the app even paints. Always reference this
   file as "/styles.<hash>.css" — test/dashboard-script-url-resolution.test.mjs enforces this generically
   for every local asset (script src AND link href), and test/dashboard-asset-versioning.test.mjs enforces
   it specifically for this file's own <link> tag.

   VERSIONED FILENAME, ON PURPOSE (see scripts/hash-dashboard-asset.mjs's own header for the full
   reasoning). This file's name embeds a short hash of its own content. After editing the CONTENT below,
   run:
     node scripts/hash-dashboard-asset.mjs dashboard/styles.<current-hash>.css
   then `git mv` to the filename it prints and update the matching <link href="..."> in
   dashboard/index.html, in the same commit. test/dashboard-asset-versioning.test.mjs enforces that the
   filename, the file's own content hash, and index.html's reference never drift apart. */
  :root {
    /* ⚠ DECLARED, BECAUSE THE BROWSER PAINTS CONTROLS WE DO NOT. This app is deliberately dark-only —
       every token below is a dark value and there is no light palette. Without this line the UA still
       believes it is in light mode, so anything IT paints comes out light: the timeframe <select>'s
       OPEN dropdown list (the closed control is ours and looks right, the popup is not), scrollbars,
       focus rings, form controls. On the Results page that is the one control the page has. */
    color-scheme: dark;
    --bg:#0B1220; --panel:#141b24; --panel2:#1c2632; --line:#26323f; --txt:#e6edf3; --mut:#8b98a5;
    --accent:#10B981;                                                 /* canonical brand green */
    --accent-bright:#34D399;                                          /* beacon tint (replaces #00d166); live/alive moments ONLY */
    --accent-ink:#06210f;                                             /* text/icon ON an accent fill */
    --accent-glow:color-mix(in srgb, var(--accent) 50%, transparent);
    --accent-dim:color-mix(in srgb, var(--accent) 14%, transparent);
    --accent-faint:color-mix(in srgb, var(--accent) 6%, transparent);
    --warn:#d29922; --bad:#f85149; --blue:#58a6ff;
    --prime:#e8b53d; --prime-ink:#241a02; --prime-glow:color-mix(in srgb, var(--prime) 50%, transparent);  /* gold PRIME — top conviction tier (D2); NOT --warn */
    --font-num:"Oswald", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;  /* SP-6 · big-number display face (Oswald max weight is 700) */
    --tier-strong:#3FA491; --tier-strong-dim:color-mix(in srgb, #3FA491 14%, transparent);  /* SP-314 · Strong-tier signal, deliberately NOT the brand accent */
    /* TA-785/TA-778 — Handicapper's independent lean, new token approved by Design Lead (TA-778 comment
       thread). Violet/indigo is the one hue family nothing else on this card speaks for — never reuse
       --prime/--tier-strong/--accent/--blue/--warn/--bad, each of those already carries a different real
       meaning elsewhere on the same card (see that comment's full audit). Built the exact same way
       --tier-strong/--tier-strong-dim already are. */
    --handicap:#A78BFA; --handicap-dim:color-mix(in srgb, #A78BFA 14%, transparent);
  }
  * { box-sizing:border-box; }
  /* Backstop, not the fix: every genuinely wide element in this app already scrolls INSIDE itself
     (.tabs, .subtabs, .tscroll, .sv-scroll all use their own overflow-x:auto) — nothing here is meant
     to widen the page. This just makes a future regression like TA's mobile-header overflow fix
     (2026-09-22, see .fresh) fail visibly-contained instead of pushing the whole document sideways. */
  /* ⚠ min-height:100vh IS LOAD-BEARING, NOT COSMETIC (found 2026-10-06, the Scoreboard-tab bell-cutoff
     bug). overflow-x:hidden alone on html/body forces the UA to resolve overflow-y as `auto` too (CSS2.1
     overflow spec: one axis non-visible forces the other off `visible`) — and with neither axis given an
     explicit height, html/body then size to their own CONTENT, not the viewport. Every other tab always
     happened to have enough content to exceed one viewport, so this never surfaced. The Scoreboard tab
     (e.g. a light slate — one CFB game, no NFL) can render shorter than the viewport, and body/html then
     become a short `overflow:auto` box that CLIPS any taller position:absolute descendant painted inside
     it — including #bellMenu (a child of the always-tall-enough header), which measured a correct
     753px-tall box via getBoundingClientRect() while visually cutting off around body's own ~400px
     content height. min-height:100vh keeps body/html at least viewport-tall regardless of how short the
     active tab's content is, so that clip boundary can never sit above the viewport's own bottom edge. */
  html { overflow-x:hidden; min-height:100vh; }
  body { margin:0; background:var(--bg); color:var(--txt); font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; overflow-x:hidden; min-height:100vh; }
  a { color:var(--blue); }
  /* The invite card is a brand-new player's entire first impression, and the global blue anchor
     was the only blue thing on an otherwise green-and-dark screen. --blue is a real system token,
     so this was not an invented colour, but the app's own in-content action link (see "Ask the
     Sharp") is accent green at 700 — that is the treatment for "here is the other thing you can
     do on this screen", which is exactly what these are. Scoped to the card so the global default
     is untouched everywhere else, and written once rather than inline on each anchor. */
  .invite-card a { color:var(--accent); font-weight:700; }
  /* THE canonical small uppercase label (SP-225). New surfaces use this; the dozen older
     component-scoped variants get retired by attrition when their components are next touched,
     rather than in one sweep that would shift type across the whole product at once. */
  .eyebrow { font-size:12px; letter-spacing:.06em; text-transform:uppercase; color:var(--mut); }
  /* Same label, promoted. Used where the card itself is accent-bordered and the heading leads it. */
  .eyebrow.on { color:var(--accent); }
  /* min-width:0 on both flex children: without it, a flex item's default min-width:auto lets its own
     unbreakable content (e.g. the freshness label) force ITSELF wider than the space header actually has,
     which pushes the whole row — and therefore the document — past the viewport. See TA mobile-header
     overflow fix (2026-09-22): a longer freshness string exposed this same-day the .fresh media query below
     was added. Keep both guards; either alone still lets the other child blow out the row. */
  header { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:11px 14px; border-bottom:1px solid var(--line); background:var(--panel); position:sticky; top:0; z-index:5; min-width:0; }
  @media (min-width:600px){ header { padding:14px 18px; } }
  .brand { font-weight:800; letter-spacing:.3px; font-size:18px; min-width:0; flex:0 1 auto; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .brand span { color:var(--accent); }
  /* Requirement: logo may scale down on mobile so it never competes with the top-right controls for space.
     ⚠ The wordmark is an <img style="height:51px;width:auto"> INLINE style, swapped in at the edge for the
     new-brand host (netlify/edge-functions/lib/rebrand-html.mjs) — a class-level rule cannot beat an inline
     one without !important, which is the one legitimate use of it here (there is no other way to override
     an inline style).
     ⚠ max-height (an absolute px value), not height:auto alone, is what keeps this SAFE at normal widths.
     First attempt used `height:auto !important` with only `max-width:100%` — at any width with no shrink
     pressure (including ordinary desktop), that resolves the image's flex-basis contribution from its raw
     SOURCE FILE size (700x203 here), not the intended 51px-tall rendering, because max-width is a
     percentage and percentages don't count during max-content sizing — measured live, it rendered the
     wordmark at up to 184x53 in cases that should have stayed 51px tall, and would have rendered it at its
     full ~700px source width on desktop, wide enough to overlap the nav. max-height:51px is an ABSOLUTE
     constraint, so it DOES count during max-content sizing: .brand's natural contribution becomes the
     51px-tall rendering (~176px wide) everywhere there's room for it — i.e. everywhere the header already
     looked exactly as it does today — and only actually shrinks below that once .brand (the sole
     flex-shrink:1, min-width:0 child of header — .topbar's controls are pinned to flex-shrink:0 above) is
     squeezed by real space pressure, with max-width:100% taking over as the binding constraint at that
     point and height following it down (both auto, aspect ratio preserved throughout).
     This can only ever make the header SHORTER than its already-sticky-positioned .tabs-wrap top:74px/80px
     expects (last computed for the full 51px logo, see that comment) — a short header only opens a small
     gap above the tab strip on the very narrowest phones, never an overlap, so those two numbers do not
     need to change. (Recomputing them for real would mean also touching the JS TABS_WRAP_TOP constant,
     which a prior PR deliberately deferred to TA-259 rather than do inside an unrelated change — same
     boundary applies here.) */
  .brand img { max-height:51px; max-width:100%; width:auto !important; height:auto !important; }
  .muted { color:var(--mut); }
  button { cursor:pointer; border:1px solid var(--line); background:var(--panel2); color:var(--txt); padding:8px 14px; border-radius:8px; font-size:14px; }
  button.primary { background:var(--accent); color:var(--accent-ink); border-color:var(--accent); font-weight:700; }
  button:disabled { opacity:.5; cursor:default; }
  input, select { background:var(--panel2); border:1px solid var(--line); color:var(--txt); padding:9px 11px; border-radius:8px; font-size:14px; width:100%; }
  .wrap { max-width:1000px; margin:0 auto; padding:18px 18px 104px; } /* extra bottom cushion so footnotes scroll clear of the fixed "Ask the Sharp" button */
  .center { min-height:70vh; display:flex; align-items:center; justify-content:center; }
  .card { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:22px; }
  /* Sticky wrapper holds the scroll-affordance fades (SP-3). The inner .tabs is the horizontal scroller.
     top must track the header's real rendered height (image height + its own vertical padding + its
     1px border), which now moves with the brand logo's size — 74px/80px below reflect the 51px logo
     (11px/14px header padding at the same 600px breakpoint the header itself uses). Recompute both
     numbers (here AND the JS TABS_WRAP_TOP below) any time the header logo's height changes again. */
  .tabs-wrap { position:sticky; top:74px; z-index:4; background:var(--panel); }
  @media (min-width:600px){ .tabs-wrap { top:80px; } }
  /* edge chevrons cue that more tabs are off-screen (a same-color fade is invisible on the dark panel).
     ⚠ SP-4 moved these from .tabs-wrap onto an inner .tabs-fade. They are absolutely positioned
     top:0/bottom:1px, so while they were anchored to .tabs-wrap, adding the contextual .subtabs row
     inside that wrapper stretched both gradients down over BOTH rows — describing a scroll that only
     the top row does. .tabs-fade hugs the scrolling strip alone; .tabs-wrap stays the sticky box. */
  .tabs-fade { position:relative; }
  .tabs-fade::before, .tabs-fade::after { position:absolute; top:0; bottom:1px; width:50px; pointer-events:none; z-index:2; opacity:0; transition:opacity .18s ease; display:flex; align-items:center; color:var(--accent); font-size:28px; font-weight:800; line-height:1; }
  .tabs-fade::before { content:'‹'; left:0;  justify-content:flex-start; padding-left:9px;  background:linear-gradient(to right, var(--panel) 42%, rgba(20,27,36,0)); }
  .tabs-fade::after  { content:'›'; right:0; justify-content:flex-end;   padding-right:9px; background:linear-gradient(to left,  var(--panel) 42%, rgba(20,27,36,0)); }
  .tabs-fade.fade-l::before { opacity:1; }   /* more tabs to the left  */
  .tabs-fade.fade-r::after  { opacity:1; }   /* more tabs to the right */
  .tabs { display:flex; gap:6px; flex-wrap:nowrap; overflow-x:auto; -webkit-overflow-scrolling:touch; scrollbar-width:none; padding:12px 18px 0; border-bottom:1px solid var(--line); background:var(--panel); }
  .tabs::-webkit-scrollbar { display:none; }
  .tab { flex:0 0 auto; white-space:nowrap; padding:9px 14px; border-radius:8px 8px 0 0; border:1px solid transparent; border-bottom:none; color:var(--mut); background:none; }
  .tab.active { color:var(--txt); background:rgba(63,185,80,.16); border-color:rgba(63,185,80,.6); }
  /* ── SP-4 grouped nav ─────────────────────────────────────────────────────────────────────────
     A group button is a .tab that opens a menu. The menu itself is NOT a child of .tabs: .tabs is an
     overflow-x scroller (and carries a mask below 720px), so anything positioned inside it is clipped
     at the strip's edge. The single #navMenu element is appended to <body> and positioned from the
     button's rect, which has no clipping ancestor to escape. */
  .navgroup { flex:0 0 auto; display:inline-flex; }
  .tab-group { display:inline-flex; align-items:center; gap:5px; }
  .tab-group .caret { display:inline-flex; opacity:.7; transition:transform .15s ease; }
  .tab-group[aria-expanded="true"] .caret { transform:rotate(180deg); opacity:1; }
  .nav-menu { position:fixed; z-index:50; background:var(--panel); border:1px solid var(--line); border-radius:12px;
    box-shadow:0 8px 28px rgba(0,0,0,.5); padding:6px; min-width:196px; max-width:min(280px,92vw); }
  /* ⚠ THESE RULES MUST SURVIVE BOTH ELEMENT TYPES. Every entry was a <button> until Weekly Picks arrived
     as an <a> — a real link, because it leaves the app for the public card. The rule set was written for
     buttons and reset nothing a browser applies to anchors, so Weekly Picks rendered underlined and read
     as the odd one out rather than as a peer. `text-decoration` and `font` are declared explicitly for
     that reason; a button does not inherit the page font either, which is why it is stated rather than
     assumed. The menu should look like one list whatever each row happens to be made of. */
  .nav-menu .nav-item { display:block; width:100%; text-align:left; background:none; border:1px solid transparent;
    color:var(--txt); padding:10px 12px; border-radius:8px; font-size:14px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    text-decoration:none; font-family:inherit; font-weight:inherit; cursor:pointer; box-sizing:border-box; }
  .nav-menu .nav-item:hover { background:var(--panel2); }
  .nav-menu .nav-item.on { color:var(--accent); font-weight:700; background:var(--accent-dim); }
  /* Contextual second row — the siblings of whatever you are looking at, so moving between two boards in
     the same group never costs a trip back through the menu. Lives inside .tabs-wrap so the computed
     --nav-h sticky offset picks it up automatically. */
  .subtabs { display:flex; gap:6px; flex-wrap:nowrap; overflow-x:auto; -webkit-overflow-scrolling:touch; scrollbar-width:none;
    padding:8px 18px; border-bottom:1px solid var(--line); background:var(--panel2); }
  .subtabs::-webkit-scrollbar { display:none; }
  .subtab { flex:0 0 auto; white-space:nowrap; background:none; border:1px solid var(--line); color:var(--mut);
    padding:5px 11px; border-radius:999px; font-size:12.5px; font-weight:600; }
  .subtab:hover { color:var(--txt); }
  .subtab.active { color:var(--accent-ink); background:var(--accent); border-color:var(--accent); font-weight:700; }
  .play { background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:12px 14px; margin:8px 0; }
  .play .top { display:flex; justify-content:space-between; align-items:baseline; gap:10px; }
  .play .pick { font-weight:800; font-size:17px; }
  .play .conv { color:var(--accent); font-weight:700; white-space:nowrap; }
  .play .why { color:var(--mut); margin-top:6px; font-size:13.5px; }
  /* TA-725 refinement pass — Games card title is league + full two-team matchup ("NFL · Seattle Seahawks
     @ Denver Broncos"), materially longer than every other `.play .pick` use (a single play name, e.g.
     "UNLV Rebels +1.5"). Reusing `.pick`'s 17px/800 sizing wrapped that content 4-5 lines on mobile,
     which both read as oversized gutters (lots of narrow wrapped lines inside the same real padding every
     other card uses) and produced wildly uneven card heights row to row. SCOPED to `.game-card` only —
     `.play .pick` itself is untouched, so Game Lines/Prop/Teaser cards are byte-for-byte unaffected. */
  .game-card .top { align-items:flex-start; }
  .game-card-league { margin-bottom:4px; }
  /* TA-725 refinement pass, round 2 (UX review, 2026-10-14) — min-height alone only reserves 2 lines for
     a SHORT title; it never capped a LONG one, so "Western Kentucky Hilltoppers @ Sam Houston State
     Bearkats"-length matchups (common — this slate is mostly NCAA FB, where mascot-heavy full names are
     the norm, not the exception) still ran to 4-5 lines and card heights stayed uneven across a grid row,
     the exact complaint this was supposed to fix. -webkit-line-clamp caps every title at the SAME 2
     lines, truncating with an ellipsis only on the rare matchup that needs it — paired with min-height so
     a short 1-line title still reserves the same 2-line block. The full matchup is never lost: it's still
     the page title one tap away in Game Detail. */
  .game-card-title { font-weight:700; font-size:15px; line-height:1.32; min-height:2.64em; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
  .badge { font-size:11px; padding:2px 8px; border-radius:999px; border:1px solid var(--line); color:var(--mut); display:inline-block; white-space:nowrap; } /* inline-block+nowrap so the pill never splits its border across a line-wrap (mobile clip fix) */
  /* BL-61 confidence grade */
  .conf-row { display:flex; align-items:baseline; gap:8px; margin-top:4px; flex-wrap:wrap; }
  /* D2 tier ladder (shared voice — matches .p-tierpill + .potd-prime): PRIME=gold hero · STRONG=dim green ·
     SOLID=teal outline (one step down from Strong's fill, same --tier-strong token — a real qualified
     tier, not lumped with "not qualified") · LEAN/WATCH=neutral (TA-734, 2026-10-16: found live that
     Solid rendered byte-identical to Lean/no-edge, risking a real third-tier pick reading as "nothing
     here" to a fast scanner. Deliberately NOT var(--blue) — that's reserved by .conf-Standard below for a
     genuinely different concept and its own comment explicitly depends on Solid staying out of that
     color). */
  .conf-chip { font-size:11px; font-weight:800; padding:2px 9px; border-radius:999px; border:1px solid var(--line); white-space:nowrap; letter-spacing:1px; color:var(--mut); text-transform:uppercase; }
  /* TA-841 — .opin-chip is Our Opinion's own badge shape (docs/specs/handicapper/CONVICTION-BADGE-SHAPES.md
     §2), composed with the EXISTING .conf-Prime/.conf-Strong/.conf-Solid/.conf-Lean tier-color classes
     verbatim (never redefined) — e.g. <span class="opin-chip conf-Prime">PRIME</span>. SAME base metrics
     as .conf-chip (font-size/weight/padding/letter-spacing/case/nowrap) so the two sit flush at equal
     height/baseline when adjacent. MUST be declared BEFORE .conf-Prime/.conf-Strong/.conf-Solid/.conf-Lean
     (it is, right here) so those tier classes still win on background/color/border/box-shadow when
     composed with it — only border-radius ever differs between an Opinion badge and an Analytics badge
     at the same tier.
     REAL VISUAL FINDING, ROUND 1 (2026-10-22, real Chrome render at true 11px size, zoomed screenshot,
     lowest-contrast Lean tier specifically): the Design Lead's proposed subtle corner-radius "tag"
     silhouette (border-radius: 2px 999px 999px 2px) was NOT reliably perceptible at a glance. A single
     cut/rounded corner (a "diamond-B" clip-path tried next) was then shown to David directly in a live
     swatch comparison — his verdict: a single-corner treatment "looks off or like it's been cut off," a
     rendering-glitch read, not an intentional shape.
     REAL VISUAL FINDING, ROUND 2 (2026-10-22, David's own live review of 5 real rendered candidates — Pill
     reference, full-rectangle, compact-boxy, elongated-hexagon, true-octagon): a FULL RECTANGLE with sharp
     square corners on all four sides read as the clearest, most obviously-intentional alternative to the
     pill at every tier tested, including Lean (the lowest-contrast case) — no ambiguity, no clipped-glyph
     risk, and the simplest possible CSS (one property, zero clip-path edge cases, nothing to break across
     browsers or future tier-color changes). Shipped as border-radius: 0 — still passes the spec's §0
     color-first test (a gold rectangle next to a gold pill still reads as "two golds" at a glance), since
     only the corner treatment changed, not fill/icon/size. See the TA-841 build handoff for the actual
     screenshots and the full swatch comparison (6 candidates total) this decision was made from. */
  .opin-chip {
    font-size: 11px;
    font-weight: 800;
    padding: 2px 10px;
    border-radius: 0;
    border: 1px solid var(--line);
    white-space: nowrap;
    letter-spacing: 1px;
    color: var(--mut);
    text-transform: uppercase;
  }
  .conf-Prime { background:var(--prime); color:var(--prime-ink); border:0; box-shadow:0 4px 14px -4px var(--prime-glow); }
  .conf-Strong { background:var(--tier-strong-dim); color:var(--tier-strong); border:1px solid color-mix(in srgb,var(--tier-strong) 45%,transparent); }
  .conf-Solid { background:transparent; color:var(--tier-strong); border:1px solid color-mix(in srgb,var(--tier-strong) 35%,transparent); }
  .conf-Lean { background:transparent; color:var(--mut); border:1px solid var(--line); }
  /* HALO FRAME (tonight's build) — docs/specs/handicapper/CONVICTION-BADGE-SHAPES.md's own shape split
     (.conf-chip pill = Our Analytics, .opin-chip rectangle = Our Opinion) renders completely unchanged —
     this is a pure OUTER wrapper around the pair, never a redefinition of .conf-chip/.opin-chip or any
     .conf-Prime/.conf-Strong/.conf-Solid/.conf-Lean color rule. It composes around whatever markup the
     caller already builds (JS decides WHEN to apply it — see _haloFrameTier() in dashboard/index.html);
     this stylesheet only owns what it looks like once applied. inline-flex + a little padding gives the
     glow real room to breathe around BOTH chips without touching either chip's own border-radius/shape;
     the wrapper's own radius is generous (999px) purely so the glow silhouette doesn't itself read as a
     hard-edged box — it never clips or reshapes its children. */
  .halo-frame { display:inline-flex; align-items:center; gap:6px; padding:3px 6px; border-radius:999px; }
  .halo-Prime { box-shadow:0 0 0 1px color-mix(in srgb, var(--prime) 55%, transparent), 0 0 16px 2px var(--prime-glow); }
  .halo-Strong { box-shadow:0 0 0 1px color-mix(in srgb, var(--tier-strong) 50%, transparent), 0 0 14px 2px color-mix(in srgb, var(--tier-strong) 45%, transparent); }
  /* TA-731 Game Intelligence — Read/Opinion/Recommendation. Reuses .conf-chip/.conf-Prime etc for the
     Qualified tier badge (same tier vocabulary, same colors, never redefined); these are the NEW
     section/state classes specific to this card. */
  .gi-card { padding:16px 18px; }
  .gi-sec { margin-bottom:14px; }
  .gi-sec:last-child { margin-bottom:0; }
  .gi-sec-h { font-size:11px; font-weight:800; letter-spacing:1.5px; color:var(--mut); margin-bottom:6px; }
  .gi-basis { font-size:10.5px; font-weight:700; letter-spacing:.5px; text-transform:uppercase; color:var(--accent); margin-left:4px; }
  .gi-rec { padding:10px 12px; border-radius:10px; margin-bottom:8px; border:1px solid var(--line); font-size:13.5px; }
  .gi-rec:last-child { margin-bottom:0; }
  .gi-rec-qualified { border-color:var(--accent); background:color-mix(in srgb, var(--accent) 7%, transparent); }
  .gi-rec-lean { border-style:dashed; }
  .gi-rec-none { color:var(--mut); font-style:italic; }
  .gi-rec-cross { color:var(--mut); font-size:12.5px; background:transparent; border-style:dashed; }
  /* TA-813 — THE OPINION is now Handicapper's own independent voice (see renderGameIntelligence's own
     header comment), not Game Intelligence's market-math content. A quiet left-border accent in the
     SAME --handicap token already used by the AGREES/SPLIT chip/1H/TOTAL badges/research dot — never
     a new color, never a tier color — is enough to mark it as a visually distinct voice from the
     neutral THE ANALYSIS section above it, without inventing a new treatment. */
  .gi-sec-handicap { border-left:3px solid var(--handicap); padding-left:10px; margin-left:-10px; }
  /* Standard Angle (Confidence Signal Lab, 2026-10-05) — a validated recommendation CLASS (the production path itself
     has earned a real closing-line-value track record) where we do NOT yet have evidence to differentiate
     individual plays by conviction the way Prime/Strong/Solid/Lean do. Deliberately its OWN visual
     treatment, not borrowed from any existing tier — borrowing Solid's or Lean's color would wrongly imply
     this sits on the SAME graded ladder as a line-shop play that cleared gradePlay()'s actual scoring.
     Blue reads as "real and trustworthy," distinct from the muted/neutral Solid-Lean-Watch family and far
     from Prime's gold hero treatment, matching the explicit "do not invent football conviction this signal
     doesn't earn" guardrail this class was built under. */
  .conf-Standard { background:color-mix(in srgb,var(--blue) 14%,transparent); color:var(--blue); border:1px solid color-mix(in srgb,var(--blue) 45%,transparent); }
  .conf-Watch { background:transparent; color:var(--mut); border:1px solid var(--line); }
  .conf-Pass { color:var(--bad); border-color:var(--bad); } /* juice kills the edge — not a bet (one true brand red) */
  .play.pass { opacity:.62; } /* demote a juice-killed play: dim it, no green, no bet button */
  /* ═══ TA-785/TA-778 — Agreement-Connector Chip Pair ═══════════════════════════════════════════════
     Handicapper's independent lean, shown next to the existing tier chip wherever it already renders
     (Best Angles, Games Grid, Game Detail). Renders nothing at all when there's no cached read for a
     pick — this whole block, byte-identical to not existing, per the same honest-omission convention
     already used elsewhere in this app (an omitted bet control, an omitted untracked book row).
     Chip shell IDENTICAL to .conf-chip (same 11px/800-weight/999px-pill shape), border/bg swapped to
     the new --handicap token — never a smaller "bonus" treatment (David's "common and ordinary"
     correction, TA-778). */
  .handicap-chip { background:var(--handicap-dim); color:var(--handicap); border:1px solid color-mix(in srgb, var(--handicap) 45%, transparent); }
  .ac-pair { display:inline-flex; align-items:center; gap:6px; cursor:pointer; }
  /* Connector — SHAPE (not color) carries agree/diverge, both render in var(--mut), never --warn/--bad:
     a divergence is a feature, not an alarm (the whole reason this exists is the SMU case, where the
     divergence was the valuable signal). Reuses the dashed/solid motifs already in this file
     (.gi-rec-lean/.gi-rec-cross's dashed border, .exec-label .line's thin currentColor divider) rather
     than inventing a new visual idiom. */
  .ac-connector { display:inline-block; width:14px; height:0; border-top:1.5px solid var(--mut); opacity:.55; }
  .ac-connector.diverge { border-top-style:dashed; width:18px; } /* a literal visible gap via the dash pattern itself — reads as "open" even at a glance on a small card */
  /* TA-815 — this used to hold a single short badge word ("AGREES"/"SPLIT"), where the all-caps
     micro-label treatment read fine. It now carries a real phrase with a team name ("Opinion: Virginia
     Cavaliers — different side"), and all-caps on a phrase that long reads as shouting and is genuinely
     harder to scan than sentence case — so this drops the uppercase/letter-spacing treatment and reads
     as normal text instead, same muted color and weight as before. */
  .ac-lbl { font-size:11px; color:var(--mut); font-weight:700; }
  .ac-chevron { color:var(--mut); font-weight:700; opacity:.65; } /* small affordance (same &rsaquo; glyph "View game insights ›" already uses on this card) so a member knows this is tappable without guessing */
  .play.pass .conv { color:var(--mut) !important; }
  /* Odds vs Fair — the book's number beside the sharp fair number (BL-99 #1) */
  .oddsfair { display:flex; align-items:center; gap:9px; flex-wrap:wrap; margin-top:9px; }
  .oddsfair .of-cell { display:inline-flex; align-items:baseline; gap:5px; background:rgba(255,255,255,.03); border:1px solid var(--line); border-radius:8px; padding:3px 9px; }
  .oddsfair .of-lbl { font-size:10px; font-weight:700; letter-spacing:.5px; text-transform:uppercase; color:var(--mut); }
  .oddsfair .of-num { font-size:15px; font-weight:800; color:var(--txt); }
  .oddsfair .of-fair .of-num { color:var(--accent); }   /* the fair number pops — "what the book got wrong" */
  .oddsfair .of-px { font-size:12px; color:var(--mut); font-weight:600; }
  .oddsfair .of-arrow { font-size:11px; color:var(--mut); }
  .oddsfair .of-gap { font-size:12px; font-weight:800; color:var(--accent); white-space:nowrap; }
  .oddsfair .of-gap.pass { color:var(--mut); }
  .oddsfair .of-tail { flex-basis:100%; font-size:11px; color:var(--mut); font-weight:500; }
  .home-play .of-mini { color:var(--accent); font-weight:700; font-size:12px; }
  .home-play .of-mini::before { content:'· '; color:var(--mut); font-weight:400; }
  /* day-lens: filter plays by kickoff day (Home + Game Lines line-shop) */
  .daylens { display:flex; gap:6px; flex-wrap:wrap; margin:2px 2px 10px; }
  .daylens button { font-size:12px; padding:3px 11px; border-radius:999px; border:1px solid var(--line); background:transparent; color:var(--mut); cursor:pointer; }
  .daylens button.on { background:var(--panel2); color:var(--txt); border:1px solid var(--line); box-shadow:inset 0 -2px 0 var(--accent); font-weight:700; } /* D1: neutral raised pill w/ green underline — frees green so it doesn't collide with the PRIME tier badge */
  /* SP-229, Design Lead: the results LEAGUE LENS sits a few pixels under the nav tabs, and an outline-plus-
     underline pill next to a filled tab is two selection idioms on one screen. This matches .tab.active
     exactly. Scoped to .lenspill so every other day-lens in the app keeps D1's treatment untouched.
     ⚠ Note for the Design Lead: D1's comment above says the neutral pill exists specifically to keep green
     away from the PRIME gold badge. This ruling supersedes it HERE only; if the two should agree, that is a
     call about D1, not about this page. */
  .daylens.lenspill button.on { background:rgba(63,185,80,.16); border-color:rgba(63,185,80,.6); color:var(--txt); box-shadow:none; font-weight:700; }
  /* The league pills were 22px tall — half the 44px minimum for a touch target, on the one control that
     governs every number on the page. Padding only; nothing else about them moves. */
  .daylens.lenspill button { min-height:44px; padding:0 16px; font-size:13px; }
  /* TA-341 REOPENED — Design Lead fix: shoppingModeBar (formerly bare .daylens.shoplens) was visually
     IDENTICAL to the plain day-filter pills sitting directly above it — no label, no distinct look, so the
     control read as a continuation of the muted description sentence rather than something that changes
     what's shown (David missed it on four separate looks). This ONE row now carries BOTH the shopping-mode
     toggle AND the Betting State control together — a labeled pill group, a divider, then the state picker
     — never two visually separate bolted-on widgets. `.compact` (Play of the Day) drops the text labels
     and shrinks the control size; the board's full row keeps both labels. */
  .shopmode-row { display:flex; align-items:center; gap:10px; flex-wrap:wrap; background:var(--panel2); border:1px solid var(--line); border-radius:10px; padding:9px 12px; margin:8px 2px 12px; }
  .shopmode-lbl { font-size:11px; letter-spacing:.05em; text-transform:uppercase; color:var(--mut); font-weight:700; white-space:nowrap; }
  /* TA (2026-10-11, Game Lines filter-UX polish) — a label and its control used to be SEPARATE direct
     children of .shopmode-row, so flex-wrap could (and on real boards, did) split them onto different
     lines — "Betting in" stranded at the end of one row with the actual <select> alone on the next.
     .shopmode-group keeps a label+control pair together as ONE unit, same idea as .shopmode-pills
     already applied to the 3 mode buttons.
     ⚠ flex-wrap MUST be wrap, not nowrap — caught live at a real 390px mobile width (2026-10-11): nowrap
     forced the label and the (also-wrapping) pills/select to share one unbroken row, squeezing the pills
     container down to a sliver too narrow for even one pill, so all 3 stacked vertically in a cramped
     column beside a label vertically CENTERED against that whole stack — worse than the bug this was
     fixing. With wrap, a cramped viewport instead drops the label onto its own line directly above the
     control, the same clean pattern .shopmode-row itself already uses one level up. */
  .shopmode-group { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
  .shopmode-pills { display:flex; gap:6px; flex-wrap:wrap; }
  .shopmode-pills button { font-family:inherit; font-size:13px; font-weight:600; padding:7px 14px; min-height:36px; border-radius:999px; border:1px solid var(--line); background:transparent; color:var(--mut); cursor:pointer; }
  .shopmode-pills button.on { background:rgba(63,185,80,.18); border-color:rgba(63,185,80,.65); color:var(--txt); font-weight:800; box-shadow:none; }
  .shopmode-pills button:hover:not(.on) { color:var(--txt); border-color:color-mix(in srgb, var(--line) 60%, var(--txt) 15%); }
  .shopmode-divider { width:1px; height:22px; background:var(--line); margin:0 2px; }
  /* flex:0 0 auto — without it this <select> was stretching to fill an entire wrapped row by itself
     (measured live: a 2-character state code in a full-width pill, found during the 2026-10-11 polish
     pass). It only ever needs to size to its own content, like every other control in this row. */
  .statectrl { font-family:inherit; font-size:13px; font-weight:600; padding:7px 12px; min-height:36px; border-radius:999px; border:1px solid var(--line); background:transparent; color:var(--txt); cursor:pointer; display:inline-flex; align-items:center; gap:6px; flex:0 0 auto; }
  /* The <select>'s own closed-state box inherits the dark theme fine via the rule above, but its native
     dropdown POPUP list does not automatically follow — Chromium falls back to a white system dropdown
     unless every <option> gets an explicit background/color (found live, David, 2026-09-18: the AR/AK/AZ...
     state list rendered near-illegible light-gray-on-white). */
  .statectrl option { background:var(--panel2); color:var(--txt); }
  .shopmode-row.compact { padding:7px 10px; margin:8px 0 10px; background:rgba(255,255,255,.03); }
  .shopmode-row.compact .shopmode-lbl { font-size:10.5px; }
  .shopmode-row.compact .shopmode-pills button { font-size:12px; padding:5px 11px; min-height:32px; }
  .shopmode-row.compact .statectrl { font-size:12px; padding:5px 10px; min-height:32px; }

  /* TA (2026-10-11, Game Lines filter-UX polish) — progressive disclosure for the tier/"at my books"
     filters (appendTierFilterControl). These are set-and-forget far more often than Shopping As or
     Betting In, so they're now a native <details>, default OPEN (nothing is hidden on first load — see
     objective #7's own "without hiding the primary... or making frequent tasks harder") inside the SAME
     .shopmode-row, closeable by anyone who's set it once and doesn't want to see it every time. The
     closed-state summary still names which tiers are showing (.smf-hint) so collapsing never hides
     information, only the control surface. Reuses .shopmode-lbl's exact label typography — same visual
     language as "Shopping as"/"Betting in", never a new one. */
  .shopmode-filters { flex:1 1 auto; min-width:0; }
  .shopmode-filters > summary { cursor:pointer; list-style:none; display:flex; align-items:center; gap:6px;
    font-size:11px; letter-spacing:.05em; text-transform:uppercase; color:var(--mut); font-weight:700; user-select:none; }
  .shopmode-filters > summary::-webkit-details-marker { display:none; }
  .shopmode-filters > summary::before { content:'▸'; display:inline-block; font-size:9px; color:var(--mut); transition:transform .15s ease; }
  .shopmode-filters[open] > summary::before { transform:rotate(90deg); }
  .smf-hint { text-transform:none; letter-spacing:0; font-weight:600; color:var(--txt); }
  .shopmode-filters-body { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:8px; padding-top:8px; border-top:1px solid var(--line); }

  /* ═══ SP-258 — THE RESULTS PAGE ═════════════════════════════════════════════════════════════════════
     ⚠ THE PAGE PRINCIPLE THAT GOVERNS EVERY RULE BELOW: on Results, GREEN MEANS "A WIN." Nothing else on
     this page may be green. Four things wanted it — the brand accent, the Strong tier chip, the active
     control, and the winning bar. The bars are the hero, so they keep it and the other three give it up.
     That is why the active control below has a FILL but no HUE and no underline (the underline was the
     green leak), and why .ladder re-skins .conf-Strong off the shipped dim green.
     Everything here is scoped to .rpage / .ladder so no other surface in the app moves. */

  /* THE QUIET STEP — and it goes UP in size, down in contrast. Going smaller is the failure: 10px grey is
     "small print someone hopes you skip", and these lines answer one of the four questions the page exists
     to answer. 12.5px, generous leading, muted ink, measured line length. ⚠ Nothing below 12.5px carries a
     sentence on this page: the season frame, the coverage sentence, the non-reportable bar labels and the
     counting card all speak in this one voice rather than in four near-misses. */
  .rpage { --fs-quiet:12.5px; }
  .rquiet { font-size:var(--fs-quiet); line-height:1.65; color:var(--mut); max-width:62ch; }
  .rquiet b, .rfig { color:var(--txt); font-weight:700; font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1; }
  /* ⚠ 62ch IS A READING MEASURE, NOT A COMPOSITION. Left alone inside a bordered card it produced a
     418px ribbon in a 964px box at 1000px — 57% of the card empty, which reads as a layout that gave up.
     A text column on the page BACKGROUND is normal and keeps its measure; inside a card the container has
     to do something with the leftover space, so the cap comes off and the card's own width (or its
     columns, below) sets the measure instead. */
  .rpage .card .rquiet { max-width:none; }

  /* ── THE CONTROL ROW. Two DIFFERENT idioms, because they are two different questions. ──────────────
     ⚠ SIX PILLS MEASURE 569px AGAINST A 354px BUDGET AT 390px. Two pill groups is not a taste question;
     it does not fit. League is a fixed three-way choice -> a segmented track. Timeframe has a VARYING
     option count (one surviving week emits two options, not three) -> a native menu button, which
     absorbs that with zero reflow. Measured fit at 390px: 186 + 138 + 10 = 334px, 20px spare.
     ⚠ The sticky offset is COMPUTED into --nav-h (see renderApp) and never hardcoded: .tabs-wrap's own
     top:53px is already ~6px wrong at >=600px, where the header's padding grows. */
  /* ⚠ THE TWO CONTROLS MUST READ AS TWO THINGS. At desktop the space between them says so on its own; on
     a phone they sit close enough to read as one control group, which is the misreading the whole
     two-idiom decision exists to prevent. A hard minimum gap keeps them apart at every width, and the
     grounds already differ (transparent track vs filled menu button). */
  .rctl   { display:flex; align-items:center; justify-content:space-between; gap:18px;
            margin:0 -18px; padding:9px 18px; background:var(--bg);
            border-bottom:1px solid var(--line); position:sticky; top:var(--nav-h,101px); z-index:3; }
  .rseg        { display:inline-flex; background:var(--panel); border:1px solid var(--line);
                 border-radius:999px; padding:3px; gap:2px; }
  .rseg button { min-height:38px; padding:0 14px; font-size:13px; font-weight:600;
                 border-radius:999px; border:1px solid transparent;
                 background:transparent; color:var(--mut); white-space:nowrap; }
  /* ACTIVE STATE: KEEP THE FILL, DROP THE HUE. Four cues carry it — fill, border, ink, weight. No
     underline: that was the green leak, and green is spoken for by the winning bars. */
  .rseg button.on { background:var(--panel2); border-color:var(--line); color:var(--txt);
                    font-weight:700; box-shadow:0 1px 0 rgba(0,0,0,.35); }
  .rtf { position:relative; display:inline-flex; align-items:center; }
  /* ⚠ width:auto must override the global `input,select{width:100%}` — without it the menu eats the row. */
  .rtf select  { appearance:none; -webkit-appearance:none; min-height:44px; padding:0 30px 0 14px; font-size:13px;
                 font-weight:700; border-radius:10px; background:var(--panel2);
                 border:1px solid var(--line); color:var(--txt); width:auto; }
  .rtf::after { content:'⌄'; position:absolute; right:12px; top:46%; transform:translateY(-50%);
                pointer-events:none; color:var(--mut); font-size:14px; line-height:1; }
  /* ⚠ NEVER WRAP, SCROLL, OR DROP A CONTROL — SO THE BREAKPOINT HAS TO BE MEASURED, NOT ESTIMATED.
     The first cut shortened the league values below 359px on an estimate of 186px for the track. It
     renders at 204px, and the row needs seg + gap + menu = 359px against a budget of (width − 36). That
     is 324px at 360 and 339px at 375 — so the row overflowed by 22px and 7px at two of the three most
     common phone widths, and only just fitted at 390 by squashing the track. Measured, with headroom:
       ≤376px  short values + tighter padding → 278px, budget 324 at 360 · 46px spare
       377px+  full values, trimmed padding   → 341px, budget 354 at 390 · 13px spare
     The Design Lead's ruling — `College · NFL · Both` at 390 — is preserved exactly; what changed is
     that it now fits with room instead of by compression. */
  .rseg .lg-short { display:none; }
  @media (max-width:420px){ .rseg button { padding:0 11px; } }
  /* Must come AFTER the block above: same specificity, and this one wins where both apply. */
  @media (max-width:376px){
    .rseg .lg-full { display:none; }
    .rseg .lg-short { display:inline; }
    .rseg button { padding:0 10px; }
    .rtf select { padding:0 26px 0 11px; }
    .rctl { gap:14px; }
  }

  /* ── THE HERO. Three genuinely different facts. ⚠ NOT ROI beside units: at one unit per play
     `units / (W+L)` IS the units figure, so printing both was the same number twice. */
  .rhero { display:flex; gap:10px; margin:14px 0 0; }
  .rhero > div { flex:1 1 0; min-width:0; }
  .rhero .k { font-size:12px; letter-spacing:.06em; text-transform:uppercase; color:var(--mut); }
  .rhero .v { font-family:var(--font-num); font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
              font-size:30px; font-weight:700; line-height:1.15; margin-top:4px; }
  /* ⚠ THREE FACTS, THREE ROWS, BELOW 600px. Wrapping produced a ragged 2+1 with ~50% dead space beside a
     stranded third figure, and the captions are wildly uneven ("430-399-5" against a three-line sentence),
     so the first row also came out tall and lopsided. Stacked full-width they share one left edge and one
     rhythm. Desktop keeps the clean 3-across. */
  @media (max-width:599px){
    .rhero { display:block; }
    .rhero > div { margin-top:16px; }
    .rhero > div:first-child { margin-top:0; }
  }

  /* ── THE LADDER. One arm per cell diverging from a shared centre: length is proportional to |ROI|,
     left+red for a loss, right+green for a win. ⚠ COUNTS WOULD NOT WORK — Prime's 17 bets would be a stub
     beside Solid's 47 and the staircase would be invisible. ROI makes +25.1 -> +7.7 -> -13.2 literally a
     descending staircase. Sample size lives in text beneath each panel. */
  .lpanel { margin-top:12px; padding:16px 18px; }
  /* The panel heading names the bet type on the left, and the two figure columns on the right — it never
     said what those green and red numbers were, on the section that is the whole product. The header grid
     matches the row grid exactly, so "Return" and "Bets" sit over the figures they name. */
  .lhead { display:grid; grid-template-columns:1fr var(--rail-r); column-gap:var(--gap); align-items:baseline; margin:0 0 10px; }
  .lhead h4 { margin:0; font-size:15px; font-weight:700; }
  .lhead .lroi span { font-size:11px; letter-spacing:.05em; text-transform:uppercase; color:var(--mut);
                      font-family:inherit; font-weight:600; }
  /* ⚠ FIXED 58px BOTH SIDES so the centre line lands at exactly 50% and every panel's axis aligns with
     every other panel's. The shared axis has to hold between panels or the whole thing reads as broken. */
  /* ⚠ THE RAILS ARE UNEQUAL NOW, AND THE AXIS IS COMPUTED RATHER THAN ASSUMED. The right rail carries
     BOTH the return and the sample size, so it is wider than the tier rail — and the axis can no longer
     sit at a hardcoded 50%. What the Design Lead's ruling actually requires is that the centre line lands
     at the SAME x in all three panels, not that the rails match each other; identical rails in every
     panel still guarantees that. The position is derived from the rail widths so the two can never drift. */
  /* TA-602 — RIGHT RAIL WIDENED AGAIN, 140px -> 165px (mobile 128px -> 145px), to comfortably hold the
     now-FIXED 9ch + 5.5ch `.lroi` columns (see its own comment) plus their 8px gap with real breathing
     room either side, rather than the 140px that was a tight fit sized before those columns were fixed.
     The axis position below is computed FROM --rail-r, so widening it here is the whole fix; nothing
     else needs to move. */
  .lpanel { --rail-l:58px; --rail-r:165px; --gap:10px; }
  .lbars { position:relative; }
  .lbars::before { content:''; position:absolute; margin-left:-.5px; top:-2px; bottom:-2px; width:1px;
                   left:calc(var(--rail-l) + var(--gap) + (100% - var(--rail-l) - var(--rail-r) - var(--gap) * 2) / 2);
                   background:color-mix(in srgb,var(--txt) 32%,transparent); }
  .lrow { display:grid; grid-template-columns:var(--rail-l) 1fr var(--rail-r); column-gap:var(--gap);
          align-items:center; min-height:26px; }
  @media (max-width:376px){ .lpanel { --rail-l:50px; --rail-r:145px; } }
  .lrow .lrail { text-align:left; }
  .ltrack { position:relative; min-height:26px; display:flex; align-items:center; }
  /* Equal weight, absolutely: same height, same opacity, same radius for red and green. A loss is never
     shrunk, greyed or de-emphasised. */
  /* ⚠ A USABLE MINIMUM. At 390px the half-track is 91px, so a −4.8% arm computed to about 11px and a
     panel of small returns read as ragged stubs rather than as a comparison. The minimum keeps every bar
     legible as a bar — which side of the axis it sits on is the primary read, and that stays exactly
     true. Below roughly 2.5% the LENGTH stops being proportional; the exact figure is printed beside it
     at full weight, so nothing is hidden by the floor and nothing is exaggerated by it either. */
  .lbar { position:absolute; top:50%; transform:translateY(-50%); height:10px; border-radius:2px; min-width:7px; }
  .lbar.win  { left:50%;  background:var(--accent); }
  .lbar.loss { right:50%; background:var(--bad); }
  /* ⚠ OFF-SCALE: a cell too small to set the scale, whose return runs PAST the end of it.
     The first attempt drew it at FULL track length in a lighter wash, and the Design Lead measured what
     that actually produced: Prime +83.3% (3 bets) and Solid +39.3% (25 bets) rendered as bars of
     IDENTICAL LENGTH. A reader scanning length concluded the two were equal when one figure is more than
     double the other. Length is the primary read; a fill opacity is not a length.
     Worse, the wash broke this section's own rule three comments above — "a loss is never shrunk, greyed
     or de-emphasised" — because a muted red bar reads weaker than a full-weight one right below it.
     So: the bar STOPS SHORT of the track end, and a full-strength overflow cue sits past the gap. Length
     now says "further than this chart goes"; nothing is dimmed; red and green stay equal weight. */
  .lbar.off-scale::after { content:""; position:absolute; top:-2px; bottom:-2px; width:3px; border-radius:1px; }
  .lbar.off-scale.win::after  { right:-7px; background:var(--accent); }
  .lbar.off-scale.loss::after { left:-7px;  background:var(--bad); }
  /* Thin and empty rows keep the FULL row height and the axis tick. The label starts 9px right of the axis. */
  /* ⚠ THE LABEL RUNS INTO THE ROI COLUMN AND WRAPS RATHER THAN CLIPPING. The ROI column is COMPLETELY
     EMPTY on both non-reportable states, so the 58px rail plus its 10px gap is free space — and even
     then `No Strong game lines in NFL yet` does not fit on one line at 390px. Clipping a server-authored
     sentence to `No Strong game lin…` would be worse than either state it describes, so the ROW GROWS.
     That does not break the shared axis: the axis is drawn down the whole stack at a fixed x, and a
     taller row moves nothing sideways. It cannot collide with a bar either — a row that has a bar never
     has a label, which is why the bar can stay absolutely positioned and vertically centred. */
  .lnote { margin-left:calc(50% + 9px); margin-right:-68px; padding:5px 0;
           font-size:var(--fs-quiet); line-height:1.35; color:var(--mut); }
  .lnote b { color:var(--txt); font-weight:700; font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1; }
  /* ⚠ THE SAMPLE SIZE CARRIES EQUAL VISUAL WEIGHT TO THE RETURN — same face, same size, same weight,
     differing only in colour. This is what replaced the 10-bet floor: with no suppression, "+93%" alone
     would be a headline and "2 bets" a footnote, and the reader would take the headline. Side by side at
     one weight they read as a single fact, which is the honest shape of it. It is also why `n` left the
     panel footnote — a count three rows away from the figure it qualifies is not a qualification. */
  /* ⚠ BOTH COLUMNS ARE TRULY FIXED TRACKS NOW, NOT `1fr`/`auto`. Each `.lrow` is its OWN independent grid
     (there is no shared parent grid across the stacked rows), so `1fr` and `minmax(X,auto)` both resize
     PER ROW off that row's own content — the previous `1fr minmax(2.6ch,auto)` looked fixed for a 1- or
     2-digit bet count, where the drift was sub-pixel, but TA-602 put a full W-L-T record ("170-133-4" vs
     "27-22") in the wide column and the same latent flaw became an 8px+ rag every row — David, 2026-10-04:
     "these need to be symmetrical and spaced so they have a consistent vertical alignment." Literal `ch`
     widths are the actual fix: every row's two columns start and end at the SAME x regardless of that
     row's own content length, which is what right-aligned tabular-nums needs to read as a real ledger
     rather than ragged text. 9ch fits the longest realistic record (a full-season "274-201-4"-shaped
     figure); 5.5ch fits "100.0%" with a hair of room. The header below uses the SAME track so the labels
     sit over their columns. */
  .lroi { display:grid; grid-template-columns:9ch 5.5ch; column-gap:8px; align-items:baseline;
          font-family:var(--font-num); font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
          font-weight:700; font-size:16px; text-align:right; }   /* Oswald caps at 700, not 800 */
  @media (max-width:376px){ .lroi { grid-template-columns:8ch 5ch; column-gap:6px; font-size:15px; } }
  .lroi .lroi-n { color:var(--mut); }
  /* The per-panel subtotal. Quieter than a rung and separated by a rule, because it is a SUM of the three
     above it, not a fourth grade. Same figure columns so it lines up with what it totals. */
  .ltotal { display:grid; grid-template-columns:var(--rail-l) 1fr var(--rail-r); column-gap:var(--gap);
            align-items:center; border-top:1px solid var(--line); margin-top:4px; padding-top:8px; }
  .ltotal .ltot-label { font-size:11px; letter-spacing:.04em; text-transform:uppercase; color:var(--mut); }
  .ltotal .lroi { font-size:14px; }
  .ltotal .lnote { font-size:13px; color:var(--mut); }
  /* ⚠ THE TIER LADDER LIVES IN THE LEFT RAIL, NEVER ON THE BARS — bars are result-coded, the rail is
     tier-coded, one meaning per element. A luminance ladder: gold fill -> white outline -> grey outline.
     Strong is re-skinned off the shipped dim-green .conf-Strong because green is spoken for here. */
  .ladder .conf-chip { letter-spacing:.5px; padding:2px 7px; }
  .ladder .conf-Strong { background:transparent; color:var(--txt);
                         border:1px solid color-mix(in srgb,var(--txt) 40%,transparent); }
  /* TA-602 — David, 2026-10-04: "the tier button colors should all be the same, one of the prime looks
     different." The half-opacity step this used to be (the ONLY cue separating an empty cell from a thin
     one) read as a visual bug, not a signal — a reader comparing panels sees Prime gold in one and a
     washed-out Prime in another and reasonably reads that as inconsistency, not as "this one is empty".
     Emptiness is still said in words right next to it (`b.emptyLabel`/"None in range"), so the chip does
     not need to carry the same fact twice. Chips are now full-strength in every row, empty or not. */

  /* ── THE "EVERYTHING WE TRACK" RECONCILIATION — discrete labelled figures, not running prose.
     ⚠ It was six lines of 12.5px grey carrying SEVEN bolded figures packed into a sentence, in the
     second-most prominent position on the page: the densest block on it, and the one a stranger would
     point at and call our spreadsheet. It is a reconciliation. It gets the shape of one. */
  .rall { margin-top:16px; }
  .rall-figs { display:flex; flex-wrap:wrap; gap:10px 26px; margin:8px 0 9px; }
  /* ⚠ MEASURED AT 360 AND 375, NOT ONLY AT 390 — this strip was claimed as "one row" on the strength of a
     390px measurement and still wrapped 3+1 at the two widths below it, orphaning VOIDED on its own line.
     That is the same measure-one-width mistake that put the control-row breakpoint 30px out. The four
     items need ~246px of content; at a 26px gutter that is 324px, which is exactly the budget at 360, so
     it tipped over. A 16px gutter brings it to 294px — 30px of headroom at the narrowest width. */
  @media (max-width:420px){ .rall-figs { gap:10px 16px; } }
  .rall-figs > div { display:flex; flex-direction:column; gap:1px; }
  .rall-figs .l { font-size:11px; letter-spacing:.05em; text-transform:uppercase; color:var(--mut); }
  .rall-figs .f { font-family:var(--font-num); font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
                  font-size:17px; font-weight:700; color:var(--txt); }
  /* ── THE COUNTING CARD. An aside by GROUND, not by shrinking. */
  .rcount { margin-top:12px; background:var(--panel2); }
  .rcount .cline { margin-top:7px; }
  .rcount .cline:first-of-type { margin-top:0; }
  /* Two columns once the card is wide enough to leave a ribbon otherwise — five lines fall 3 + 2, and each
     column is ~60ch, so the reading measure is kept by the column rather than by an arbitrary cap. */
  @media (min-width:720px){
    .rcount .clines { column-count:2; column-gap:34px; }
    .rcount .cline { break-inside:avoid; margin-top:0; margin-bottom:10px; }
    /* The closing card is one sentence, so it caps its OWN width rather than splitting into columns. */
    .rclose { max-width:620px; }
  }
  /* ── THE LAST TEN. A loss renders at exactly the same weight as a win. */
  /* ⚠ THE PAGE BODY MUST NEVER SCROLL HORIZONTALLY. A pick is free text up to ~80 characters
     (`Florida State Seminoles +6.5 …`) and in a 4-column table at 390px that pushed the document to
     431px wide. It wraps instead — a table this narrow has nowhere to scroll to. */
  .rten { width:100%; border-collapse:collapse; font-size:13.5px; }
  .rten td { padding:9px 8px 9px 0; border-top:1px solid var(--line); vertical-align:top; }
  .rten tr:first-child td { border-top:0; }
  /* `width:1%` + nowrap is shrink-to-fit under auto layout: the three short columns take exactly what
     their content needs and the pick column absorbs everything left, wrapping mid-word if it has to.
     ⚠ NOT `table-layout:fixed` — that splits the width evenly regardless, which collapsed the date to
     nothing and set `Lost` one letter per line. */
  /* The date is a group header spanning the row, not a column repeated ten times down the rail. */
  .rten .rt-day td { padding:14px 0 5px; border-top:1px solid var(--line);
                     font-size:11px; letter-spacing:.06em; text-transform:uppercase; color:var(--mut); }
  .rten tr:first-child.rt-day td { padding-top:0; border-top:0; }
  .rten .rt-day + tr td { border-top:0; }
  .rten .rt-pick { overflow-wrap:anywhere; }
  .rten .rt-res  { width:1%; white-space:nowrap; }
  .rten .rt-u { width:1%; text-align:right; white-space:nowrap; font-family:var(--font-num);
                font-variant-numeric:tabular-nums; font-weight:700; }
  /* The one link on the page. A link, not a button — it goes somewhere, it does not do anything. */
  .rmeth { color:var(--txt); font-weight:700; text-decoration:underline; text-underline-offset:3px;
           text-decoration-color:color-mix(in srgb,var(--txt) 35%,transparent); }
  /* backtest / model-validation: minimized by default, one clear divider, self-serve when you want it */
  .backtest { margin-top:16px; border-top:2px dashed var(--line); padding-top:10px; }
  .backtest > summary { cursor:pointer; list-style:none; color:var(--mut); font-size:12.5px; font-weight:700; padding:6px 2px; user-select:none; }
  .backtest > summary::-webkit-details-marker { display:none; }
  .backtest > summary::before { content:'▸ '; }
  .backtest[open] > summary::before { content:'▾ '; }
  /* date-on-every-card: the kickoff day, unmissable at the point of the bet — prevents cross-week mistakes */
  .datechip { display:inline-block; font-size:11px; font-weight:800; letter-spacing:.02em; padding:2px 9px; border-radius:999px;
    background:rgba(88,166,255,.13); color:var(--blue); border:1px solid rgba(88,166,255,.5); vertical-align:middle; white-space:nowrap; }
  .datechip.nodate { background:rgba(210,153,34,.14); color:var(--warn); border-color:var(--warn); } /* no date = verify, don't assume */
  .conf-why { color:var(--txt); font-size:12.5px; line-height:1.45; min-width:0; flex:1; }
  .conf-details { margin-top:6px; }
  .conf-details summary { cursor:pointer; color:var(--mut); font-size:11.5px; list-style:none; }
  .conf-details summary::-webkit-details-marker { display:none; }
  .conf-details summary::before { content:'▸ rationale'; }
  .conf-details[open] summary::before { content:'▾ rationale'; }
  .conf-factors { margin-top:6px; display:flex; flex-direction:column; gap:5px; padding-left:2px; }
  .conf-f { font-size:12px; color:var(--mut); line-height:1.4; }
  .conf-note { font-size:10.5px; color:var(--mut); margin-top:6px; font-style:italic; }
  .noise { color:var(--warn); }
  table { width:100%; border-collapse:collapse; font-size:13.5px; }
  th, td { text-align:left; padding:8px 10px; border-bottom:1px solid var(--line); }
  th { color:var(--mut); font-weight:600; }
  /* ⚠ SP-3, RE-OPENED BY SP-229 — A WIDE TABLE SCROLLS INSIDE ITS CARD. THE BODY NEVER SCROLLS SIDEWAYS.
     Measured at 390x844 before this rule: a 482px grid table inside a 354px card with no overflow-x
     ancestor anywhere up to .wrap, so documentElement.scrollWidth was 523 against a 390 clientWidth and
     scrolling right dragged the header, the tab strip and the section title off-screen with it. A cell
     shredded to "No / pro / Co / yet" does not read as careful, it reads as broken — which defeats the one
     thing this page is for. The scroll belongs to the table, not the document. */
  .tscroll { overflow-x:auto; -webkit-overflow-scrolling:touch; }
  .tscroll::-webkit-scrollbar { height:6px; }
  .tscroll::-webkit-scrollbar-thumb { background:var(--line); border-radius:999px; }
  /* ⚠ A SCROLLER WITH NO AFFORDANCE READS AS A LAYOUT BUG. At 390px the grid is 566px inside a 308px
     window — 46% hidden, the whole Teasers column off-screen — and the reader saw "No Prime…", "-2.4%",
     "21-19 · -0" sliced mid-character at the card edge. Worse by proximity: the nav strip 300px above
     scrolls sideways and DOES have a green chevron, so the reader has just been taught "chevron = swipe"
     and then meets a table with none. This is the SAME fade the tab strip uses (SP-3), deliberately —
     reusing the learned signal rather than inventing a second one. */
  .tscroll-wrap { position:relative; }
  .tscroll-wrap::before, .tscroll-wrap::after { content:''; position:absolute; top:0; bottom:0; width:38px; pointer-events:none; z-index:2; opacity:0; transition:opacity .18s ease; display:flex; align-items:center; color:var(--accent); font-size:26px; font-weight:800; line-height:1; }
  .tscroll-wrap::before { content:'‹'; left:0;  justify-content:flex-start; padding-left:5px; background:linear-gradient(to right, var(--panel) 40%, rgba(20,27,36,0)); }
  .tscroll-wrap::after  { content:'›'; right:0; justify-content:flex-end;   padding-right:5px; background:linear-gradient(to left,  var(--panel) 40%, rgba(20,27,36,0)); }
  .tscroll-wrap.fade-l::before { opacity:1; }
  .tscroll-wrap.fade-r::after  { opacity:1; }
  /* Fixed layout + explicit widths: a one-word "Conviction" header was holding desktop width and crushing
     the data columns that carry the actual claim. Narrow label, even thirds for the three bet types. */
  .gridtbl { table-layout:fixed; }
  .gridtbl th:first-child, .gridtbl td:first-child { width:22%; }
  /* Same state must render in the same place. Empty cells were top-aligned in one row and mid-cell in the
     next, on one screen, because the rows differ in height. */
  .gridtbl th, .gridtbl td { vertical-align:top; }
  /* The return and the count are ONE line and ONE size — at these sample sizes the count is part of the
     claim, not a caveat, so nothing may shrink one without shrinking the other. */
  .gridtbl .cellhead { font-size:17px; line-height:1.3; }
  .grid { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
  .chk { display:flex; align-items:center; gap:6px; }
  .empty { color:var(--mut); padding:30px; text-align:center; }
  .filters { display:flex; flex-wrap:wrap; gap:16px; align-items:center; margin:6px 0 4px; padding:10px 12px; background:var(--panel); border:1px solid var(--line); border-radius:10px; }
  .fgroup { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
  .flabel { color:var(--mut); font-size:11px; text-transform:uppercase; letter-spacing:.6px; margin-right:2px; }
  .fchip { position:relative; padding:5px 11px; border-radius:999px; font-size:13px; border:1px solid var(--line); background:var(--panel2); color:var(--mut); }
  .fchip.on { background:var(--accent); color:var(--accent-ink); border-color:var(--accent); font-weight:700; }
  /* TA-843-follow-up — "My Research" tab pill only (see LIST_MODE_TABS's own comment in index.html):
     echoes .opin-chip's sharp-rectangle silhouette (border-radius:0) since this tab is entirely
     Opinion-type data. Scoped to this ONE button via .fchip-rect — .fchip itself stays pill-shaped
     everywhere else, it's shared by a dozen unrelated filter bars site-wide. */
  .fchip-rect { border-radius:0; }
  /* 2026-10-21 — "My Research" presence dot (TA-811). --handicap violet, never a tier color — same
     token the AGREES/SPLIT chip and 1H/TOTAL badges already use. A 1.5px ring in the page background
     keeps it legible against BOTH .fchip's dark inactive fill and .fchip.on's solid green active fill. */
  .fchip-research-dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; border-radius:50%; background:var(--handicap); box-shadow:0 0 0 1.5px var(--bg); }
  /* SP-28 DFS lens system */
  .fchip.off { opacity:.45; cursor:not-allowed; }
  .fchip .whenchip { font-size:9.5px; color:var(--warn); margin-left:5px; font-weight:600; }
  /* TA-846 — shared "Filters" dropdown shell (filtersDropdown() in index.html). Reuses .statectrl's own
     pill/border/color language (same control family, same visual weight) rather than inventing a new
     button shape — this is meant to read as "one more control in this row", not a distinct new idiom. */
  .ddwrap { position:relative; flex:0 0 auto; }
  .dd { display:inline-flex; align-items:center; gap:7px; font-family:inherit; font-size:13px; font-weight:600; padding:7px 13px; min-height:36px; border-radius:999px; border:1px solid var(--line); background:transparent; color:var(--txt); cursor:pointer; }
  .dd[aria-expanded="true"] { border-color:var(--accent); color:var(--accent); }
  .ddcount { background:var(--accent-dim); color:var(--accent); font-size:11px; font-weight:800; padding:1px 7px; border-radius:999px; }
  .ddchev { color:var(--mut); font-size:10px; line-height:1; transition:transform .15s ease; }
  .dd[aria-expanded="true"] .ddchev { transform:rotate(180deg); color:var(--accent); }
  .ddpanel { position:absolute; top:calc(100% + 8px); right:0; z-index:12; width:min(320px, calc(100vw - 32px)); background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:10px; box-shadow:0 14px 34px -10px rgba(0,0,0,.55); display:flex; flex-direction:column; gap:10px; }
  .ddpanel[hidden] { display:none; }
  /* The .filters/.fgroup groups this panel wraps already carry their own background/border/padding/
     margin for standalone use — flattened here so nesting them inside the dropdown's own panel chrome
     never stacks a second visible box-within-a-box. */
  .ddpanel .filters { background:transparent; border:0; padding:0; margin:0; gap:12px; }
  /* TA-602 — the Results tab's tier-counted toggle reuses THIS filter-bar idiom (.filters/.fgroup), not
     a checkbox-plus-label — the existing tier chip itself is the toggle, same affordance as .fchip.off
     above, just scoped to .conf-chip so it never touches the dozen other places that class renders. */
  /* David, 2026-10-04: "I just want them to have their normal size look" — `line-height:0` here was a
     stray inline-flex whitespace-trimming habit that doesn't do anything useful once `display:flex` +
     `align-items:center` already handle vertical centering, but it DOES cascade into `.conf-chip`
     (which sets no line-height of its own), collapsing its text's line box to near zero and squashing
     the normally-round 999px pill into a thin distorted oval. Dropped; `.conf-chip` renders exactly as
     it does everywhere else on the page now. */
  .tier-toggle { display:inline-flex; align-items:center; gap:6px; background:none; border:0; padding:0; margin:0; cursor:pointer; }
  .tier-toggle[aria-checked="false"] .conf-chip { opacity:.55; }
  .tier-toggle:focus-visible { outline:2px solid var(--accent); outline-offset:3px; border-radius:4px; }
  /* TA-602 — David, 2026-10-04: "put a little checkbox... I think is easier to view." A real drawn
     checkbox (border-trick checkmark, no font-glyph dependency that could render inconsistently) rather
     than relying on the chip's own colour difference to carry "is this selected" — the chip alone read as
     a plain label, not a control. */
  .tt-box { width:15px; height:15px; border-radius:4px; border:1.5px solid var(--line); background:var(--bg); flex:none; position:relative; box-sizing:border-box; }
  .tier-toggle[aria-checked="true"] .tt-box { background:var(--accent); border-color:var(--accent); }
  .tier-toggle[aria-checked="true"] .tt-box::after { content:''; position:absolute; left:4px; top:1px; width:4px; height:8px; border:solid var(--accent-ink); border-width:0 2px 2px 0; transform:rotate(45deg); }
  .lensdesc { color:var(--mut); font-size:12.5px; margin:10px 2px 2px; line-height:1.5; }
  .lensdesc b { color:var(--txt); }
  .prov { font-size:9.5px; font-weight:800; text-transform:uppercase; letter-spacing:.3px; padding:1px 6px; border-radius:5px; margin-left:7px; background:var(--accent-dim); color:var(--accent); border:1px solid color-mix(in srgb,var(--accent) 35%,transparent); white-space:nowrap; }
  tr.lens-changed td { background:color-mix(in srgb,var(--accent) 7%,transparent); }
  .drop { color:var(--mut); text-decoration:line-through; }
  .chg-arrow { color:var(--accent); font-weight:700; margin:0 5px; }
  .lensdelta { margin-top:11px; padding:10px 12px; border-radius:10px; background:var(--accent-dim); border:1px solid color-mix(in srgb,var(--accent) 30%,transparent); font-size:12.5px; line-height:1.5; }
  .lensdelta b { color:var(--txt); }
  /* TA-894 — DFS lineup card stack, replacing the dense 8-column table. One card per roster slot so the
     `why` narrative (methodology) reads as primary content, not crammed into one cell. */
  .dfs-lineup { display:flex; flex-direction:column; gap:8px; margin-top:10px; }
  .dfs-slot { background:var(--panel2); border:1px solid var(--line); border-radius:10px; padding:10px 12px; }
  .dfs-slot.lens-changed { background:color-mix(in srgb,var(--accent) 7%,transparent); border-color:color-mix(in srgb,var(--accent) 30%,transparent); }
  .dfs-slot.empty { color:var(--mut); padding:8px 12px; display:flex; gap:8px; align-items:center; }
  .dfs-slot-head { display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; }
  .dfs-slot-tag { font-size:10.5px; flex:0 0 auto; }
  .dfs-slot-name { font-weight:700; font-size:14.5px; color:var(--txt); flex:1 1 auto; min-width:0; }
  .dfs-slot-sal { font-family:var(--font-num); font-weight:700; font-size:13.5px; color:var(--txt); white-space:nowrap; margin-left:auto; }
  .dfs-slot-inj { color:var(--warn); font-size:12px; font-weight:600; margin-top:4px; display:flex; align-items:center; gap:4px; }
  .dfs-slot-why { color:var(--txt); font-size:13.5px; line-height:1.55; margin-top:7px; }
  .dfs-slot-meta { display:flex; gap:12px; flex-wrap:wrap; margin-top:7px; padding-top:7px; border-top:1px solid var(--line); color:var(--mut); font-size:11.5px; }
  @media (max-width:480px){ .dfs-slot-name { font-size:13.5px; } .dfs-slot-sal { margin-left:0; flex-basis:100%; order:3; } }
  /* TA-516 SLICE 5 — Best Angles / By Game recommendation card. Mobile-first stack, hero-first hierarchy:
     player+market (quiet), HIGHER/LOWER+line (the loudest thing on the card), then operator/game/STRONG/
     market-implied-% as one small secondary line. Deliberately its OWN component, not a `.play` variant —
     `.play` is shared by several unrelated boards on this page and this card's hierarchy is specific to
     "what does The Angle like, which way, at what number" in a way those other boards don't need. */
  .pk-rec { background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:12px 14px; margin:10px 0; }
  /* TA-701 — By Game now shows every captured prop, not just recommended ones; .pk-recommended's accent
     left-border + .pk-rec-tag give the one visual cue that distinguishes "The Angle recommends this" from
     "ordinary evaluated prop" without repeating the full Best Angles card treatment. */
  .pk-rec.pk-recommended { border-left:3px solid var(--accent); }
  .pk-rec-tag { display:inline-flex; align-items:center; gap:3px; font-size:9.5px; font-weight:800; letter-spacing:.3px; padding:1px 6px; border-radius:5px; background:var(--accent); color:var(--accent-ink); white-space:nowrap; }
  .pk-rec-tag svg { width:9px; height:9px; }
  /* TA-704 (TA-516 Phase 2) — the real, tappable Higher/Lower selection control, replacing the old
     decorative circle icon. Deliberately plain outline buttons (not the accent-filled Recommended
     treatment) so a member's OWN selection reads as distinct from The Angle's own recommendation signal
     everywhere else on the card — selecting a prop is never visually confused with Angle endorsing it. */
  .pk-pick-row { display:flex; gap:8px; }
  .pk-pick-btn { font-size:12.5px; font-weight:700; padding:7px 14px; border-radius:8px; border:1px solid var(--line); background:transparent; color:var(--txt); cursor:pointer; min-height:36px; }
  .pk-pick-btn.on { background:var(--accent); color:var(--accent-ink); border-color:var(--accent); }
  .pk-rec.pk-selected { box-shadow:0 0 0 1px var(--accent) inset; }
  /* TA-704 — the entry tray, fixed so it stays reachable while scrolling a long By Game list. Sits ABOVE
     the Ask The Angle pill (not beside it) -- see `body:has(.pk-entry-tray) .sharp-btn` below -- so the
     two controls are vertically stacked, never overlapping, on any viewport width. */
  .pk-entry-tray { position:fixed; left:12px; right:12px; bottom:max(12px, env(safe-area-inset-bottom)); z-index:19; background:var(--panel); border:1px solid var(--accent); border-radius:12px; padding:10px 12px; box-shadow:0 -4px 20px rgba(0,0,0,.4); max-width:460px; margin:0 auto; max-height:45vh; overflow-y:auto; }
  .pk-entry-tray-head { display:flex; justify-content:space-between; align-items:center; font-size:12.5px; font-weight:700; color:var(--txt); margin-bottom:6px; }
  .pk-entry-tray-head button { background:none; border:none; color:var(--mut); font-size:12px; cursor:pointer; text-decoration:underline; padding:4px; }
  .pk-entry-tray-legs { display:flex; flex-direction:column; gap:4px; }
  .pk-entry-tray-leg { display:flex; justify-content:space-between; align-items:center; gap:8px; font-size:12.5px; padding:5px 2px; border-bottom:1px solid var(--line); }
  .pk-entry-tray-leg:last-child { border-bottom:none; }
  .pk-entry-tray-leg .pk-market { font-weight:500; color:var(--mut); margin-left:3px; }
  .pk-entry-tray-dir { color:var(--accent); margin-left:4px; }
  .pk-entry-tray-leg button { background:none; border:none; color:var(--mut); cursor:pointer; padding:4px; display:flex; }
  .pk-entry-tray-leg button svg { width:14px; height:14px; }
  .pk-entry-tray-warn { font-size:11.5px; color:var(--warn,var(--mut)); margin-top:6px; }
  .pk-entry-tray-hint { font-size:11.5px; color:var(--mut); margin-top:6px; }
  .pk-entry-analyze { width:100%; margin-top:8px; padding:10px; border-radius:8px; font-weight:700; font-size:13px; }
  .pk-entry-analyze:disabled { opacity:.5; cursor:not-allowed; }
  /* TA-704 — shift the Ask The Angle pill up above the entry tray rather than letting either cover the
     other. :has() is already used elsewhere on this page (body:has(.logbet) below) for the same reason:
     pure CSS, no manual class bookkeeping, automatically correct the instant the tray appears/disappears. */
  body:has(.pk-entry-tray) .sharp-btn { bottom:calc(max(12px, env(safe-area-inset-bottom)) + 160px); }
  .pk-rec .pk-head { display:flex; justify-content:space-between; align-items:baseline; gap:8px; }
  .pk-rec .pk-player { font-weight:700; font-size:14px; color:var(--txt); }
  .pk-rec .pk-market { font-weight:500; color:var(--mut); font-size:13px; margin-left:4px; }
  .pk-rec .pk-op { font-size:12px; font-weight:700; color:var(--mut); white-space:nowrap; }
  /* the hero: direction + line. Never color-only — an icon (shape) and the word HIGHER/LOWER (text) both
     carry the meaning, so it reads the same to a colorblind user or in a quick black-and-white screenshot. */
  .pk-rec .pk-hero { display:flex; align-items:center; gap:7px; margin:7px 0 6px; }
  .pk-rec .pk-hero svg { width:20px; height:20px; flex:0 0 auto; color:var(--accent); }
  .pk-rec .pk-dir { font-size:22px; font-weight:800; letter-spacing:-.01em; color:var(--txt); line-height:1.1; }
  .pk-rec .pk-dir b { color:var(--accent); } /* the line itself pops a shade brighter within the hero */
  /* TA-702 #3 — the band-strength tag folded directly into the hero line (e.g. "Lower 249.5 Lean Lower"),
     replacing the old second pill that repeated the direction underneath the first one. */
  .pk-dir .pk-band-tag { font-size:13px; font-weight:700; letter-spacing:.2px; margin-left:4px; vertical-align:middle; }
  .pk-dir .pk-band-tag.pk-band-strong { color:var(--accent); }
  .pk-dir .pk-band-tag.pk-band-lean { color:var(--accent); opacity:.82; }
  /* A neutral card (No Clear Edge / Not Evaluated) communicates "nothing to act on here" — it must never
     borrow the same bright accent treatment a genuine directional call earns, on the icon or the hero
     text, so a member scanning a long By Game list can tell real calls apart from the rest at a glance. */
  .pk-rec.pk-neutral .pk-hero svg { color:var(--mut); }
  .pk-rec.pk-neutral .pk-dir { font-size:15px; font-weight:600; color:var(--mut); letter-spacing:normal; }
  .pk-rec .pk-offers { display:flex; flex-direction:column; gap:4px; margin:7px 0 6px; padding:8px 10px; background:var(--panel2); border-radius:8px; }
  .pk-rec .pk-offer-row { display:flex; justify-content:space-between; align-items:baseline; font-size:13.5px; }
  .pk-rec .pk-offer-row .pk-op-name { color:var(--mut); font-weight:600; }
  .pk-rec .pk-offer-row .pk-op-line { font-weight:800; color:var(--txt); }
  .pk-rec .pk-sub { display:flex; flex-wrap:wrap; align-items:center; gap:5px; font-size:11.5px; color:var(--mut); }
  .pk-rec .pk-sub .pk-strong-sm { font-size:9.5px; font-weight:800; letter-spacing:.3px; padding:1px 6px; border-radius:5px; background:var(--accent-dim); color:var(--accent); border:1px solid color-mix(in srgb,var(--accent) 35%,transparent); }
  .pk-rec .pk-sub .pk-dot { opacity:.6; }
  @media (min-width:640px){
    /* desktop scales the SAME stack — player/op share a row, hero stays the biggest thing on the card,
       it just stops needing to wrap onto its own visual "section" the way a 390px card does. */
    .pk-rec { padding:14px 18px; }
    .pk-rec .pk-dir { font-size:24px; }
  }
  /* TA-516 SLICE 7 — Best Entry. Secondary to Best Angles (.pk-rec above) on purpose: a dashed border
     instead of a solid one, a smaller header, and each leg is one compact row rather than its own
     hero card — this module is "here's the combination", not a second copy of the primary feed. One
     .pk-entry-card per operator with a built entry; multiple cards stack, never collapse into a
     tab-only view, so both operators' entries stay simultaneously scannable while packing. */
  .pk-entry-card { background:var(--panel); border:1px dashed color-mix(in srgb,var(--accent) 45%,var(--line)); border-radius:10px; padding:11px 13px; margin:10px 0; }
  .pk-entry-card .pk-entry-head { display:flex; justify-content:space-between; align-items:baseline; gap:8px; margin-bottom:7px; }
  .pk-entry-card .pk-entry-title { font-weight:700; font-size:13px; color:var(--txt); }
  .pk-entry-card .pk-entry-title .pk-entry-size { color:var(--accent); }
  .pk-entry-card .pk-entry-op { font-size:11.5px; font-weight:700; color:var(--mut); white-space:nowrap; }
  .pk-entry-card .pk-entry-legs { display:flex; flex-direction:column; gap:1px; border-radius:8px; overflow:hidden; }
  .pk-entry-leg { padding:6px 8px; background:var(--panel2); font-size:12.5px; }
  .pk-entry-leg .pk-entry-leg-top { display:flex; align-items:baseline; justify-content:space-between; gap:8px; }
  .pk-entry-leg .pk-entry-leg-who { color:var(--txt); font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
  .pk-entry-leg .pk-entry-leg-who .pk-market { font-weight:500; color:var(--mut); margin-left:4px; }
  .pk-entry-leg .pk-entry-leg-game { color:var(--mut); font-size:11px; margin-top:2px; }
  .pk-entry-leg .pk-entry-leg-call { display:flex; align-items:center; gap:4px; font-weight:800; white-space:nowrap; flex:0 0 auto; color:var(--txt); }
  .pk-entry-leg .pk-entry-leg-call svg { width:13px; height:13px; color:var(--accent); }
  .pk-entry-leg .pk-entry-leg-call b { color:var(--accent); }
  .pk-entry-card .pk-entry-sub { margin-top:7px; font-size:10.5px; color:var(--mut); line-height:1.4; }
  .pk-entry-none { margin:8px 0; font-size:12px; color:var(--mut); padding:0 2px; }
  @media (min-width:640px){
    .pk-entry-card { padding:13px 16px; }
    .pk-entry-leg { font-size:13px; }
  }
  /* TA-702 #4 — bottom/right now respect the iOS safe area (the home-indicator strip sits inside
     env(safe-area-inset-bottom) on a notched iPhone; without this the pill was already partway into it)
     and the pill shrinks to icon-only under 480px, confirmed on a real iPhone to overlap the lower prop
     cards on Pick'em's By Game list at full size. Shrinking is the general fix (the button is page-level
     chrome, not scoped to one tab); the pickem card list below ALSO gets its own bottom clearance — see
     `.pk-rec:last-child` margin — since a long list still deserves a guaranteed gap regardless of exactly
     how big the pill renders on any given device. */
  .sharp-btn { position:fixed; bottom:max(20px, calc(env(safe-area-inset-bottom) + 12px)); right:max(20px, calc(env(safe-area-inset-right) + 12px)); z-index:20; color:var(--accent-ink); border:none; padding:12px 18px; border-radius:999px; font-weight:800; cursor:pointer;
    background:linear-gradient(100deg,var(--accent) 0%,var(--accent-bright) 26%,#a7f3c4 50%,var(--accent-bright) 74%,var(--accent) 100%); background-size:230% 100%;
    animation:sharpWave 4.6s linear infinite, sharpBreathe 3.4s ease-in-out infinite; }
  @media (max-width:480px){
    .sharp-btn { padding:12px; }
    .sharp-btn .sharp-name { display:none; } /* icon-only below 480px — same tap target, smaller footprint */
  }
  /* a light band flows across the bubble + a soft glow breathes — a subtle beacon, alive but not loud */
  @keyframes sharpWave { 0%{ background-position:200% 0; } 100%{ background-position:0% 0; } }
  @keyframes sharpBreathe { 0%,100%{ box-shadow:0 4px 16px rgba(0,0,0,.45), 0 0 0 0 color-mix(in srgb, var(--accent-bright) 0%, transparent); } 50%{ box-shadow:0 5px 20px rgba(0,0,0,.45), 0 0 20px 2px var(--accent-glow); } }
  @media (prefers-reduced-motion: reduce){ .sharp-btn { animation:none; } }
  /* "Sharp" carries the logo persona — off-white wordmark color + tracking, "Ask the" stays black */
  .sharp-btn .sharp-name { color:var(--txt); letter-spacing:.3px; }
  /* SP-264 / SP-260 — THE FAILURE MESSAGE WAS PAINTED OVER BY THIS BUTTON.
     Measured on the live page at 555px: the .betmsg error rendered at x35 y733 (338px wide) while the
     fixed pill occupies x354-518, y706-750 — 18px of horizontal and 16px of vertical overlap, so the
     tail of the sentence was covered. At a real 390px phone it wraps into the same band and worsens.
     A member who cannot save and cannot read WHY is worse off than before the guard existed.
     While a bet form is open the pill has nothing to do — you are filling in a form, not asking a
     question — so it stands down. Same condition _hasOpenBetForm() already uses to protect an open
     form from the 40s live-score re-render.
     :has() is the whole mechanism; where it is unsupported this rule simply does not apply and we are
     no worse off than today. The general case (the pill covering other content everywhere) is SP-260. */
  /* ⚠ !important IS LOAD-BEARING. The pill ships with an inline style="display:none" and is switched
     to display:block inline at sign-in, and an inline style beats a non-!important rule — so the first
     version of this shipped and did NOTHING. Verified inert before this word was added. */
  body:has(.logbet) .sharp-btn, body:has(.betform) .sharp-btn { display:none !important; }

  /* TA-725 refinement, round 4 — the fixed .sharp-btn pill overlapping Game Detail's Market Angle
     rationale text mid-scroll (not just at page-end, which the TA-702 spacer above already covers).
     Measured live: a 1331px window puts .wrap's right edge at x1158 while .sharp-btn's left edge sits
     at x1126 — a real desktop window size (unmaximized laptop), not a narrow edge case. 600-1399px is
     the band where this physically collides (below 600px the mobile layout + a smaller icon-only pill
     below 480px change the geometry; above 1400px .wrap's 1000px max-width naturally clears the pill).
     TA-260 generalization (2026-10-15): the same mechanism bites on every page, not just Game Detail —
     confirmed live via a standalone headless-Chrome measurement pass (tools/ta260-pill-overlap-check.mjs)
     against production at 390/768/1024/1399/1920px: real overlaps found at 768px (Game Lines' "BET →"
     CTA) and 1024px (Game Lines' "BET →" CTA, Prop Bets' "I placed this bet" button), none at 390, 1399,
     or 1920. That's exactly the predicted 600-1399 band — .wrap's own auto side-margins are what clear
     the pill above ~1036px, same as they do for Game Detail. Moved the rule from the Game Detail-only
     `.gd-board` selector to `.wrap` itself so every tab gets the same safe zone — `.gd-board` sits inside
     `.wrap`, so this fully supersedes (not stacks with) the old scoped rule; no page's card width changes
     beyond this single shared container getting the same right-side breathing room Game Detail already
     had. */
  @media (min-width:600px) and (max-width:1399px){ .wrap { padding-right:240px; } }

  .sharp-drawer { position:fixed; top:0; bottom:0; right:0; width:min(440px,100%); background:var(--panel); border-left:1px solid var(--line); z-index:30; display:flex; flex-direction:column; transform:translateX(105%); transition:transform .2s ease; }
  .sharp-drawer.open { transform:none; }
  .sharp-head { padding:14px 16px; border-bottom:1px solid var(--line); display:flex; justify-content:space-between; align-items:center; }
  .sharp-msgs { flex:1; overflow-y:auto; padding:14px; display:flex; flex-direction:column; gap:10px; }
  .sharp-msg { padding:10px 12px; border-radius:10px; font-size:14px; line-height:1.55; white-space:pre-wrap; }
  .sharp-msg.user { background:var(--panel2); align-self:flex-end; max-width:88%; }
  .sharp-msg.sharp { background:var(--bg); border:1px solid var(--line); align-self:flex-start; max-width:96%; }
  .sharp-msg.hint { color:var(--mut); align-self:center; text-align:center; font-size:13px; }
  /* SP-294 — a Sharp reply is rendered markdown, so its bubble lays out as HTML (pre-wrap would double
     every gap now that paragraphs are real elements). Scoped to .sharp-msg.md so nothing else moves. */
  .sharp-msg.md { white-space:normal; }
  .sharp-msg.md .stext { display:inline-block; max-width:100%; vertical-align:bottom; }
  .sharp-msg.md > :first-child, .sharp-msg.md .stext > :first-child { margin-top:0; }
  .sharp-msg.md > :last-child, .sharp-msg.md .stext > :last-child { margin-bottom:0; }
  .sharp-msg.md .md-p { margin:0 0 8px; }
  .sharp-msg.md .md-h { font-weight:700; margin:12px 0 5px; line-height:1.3; }
  .sharp-msg.md .md-h1 { font-size:15.5px; }
  .sharp-msg.md .md-h2 { font-size:14.5px; }
  .sharp-msg.md .md-h3 { font-size:13.5px; color:var(--mut); }
  .sharp-msg.md .md-l { margin:0 0 8px; padding-left:20px; }
  .sharp-msg.md .md-l li { margin:2px 0; }
  .sharp-msg.md code { background:var(--panel2); border-radius:4px; padding:1px 5px; font-size:12.5px; }
  .sharp-msg.md .md-pre { background:var(--panel2); border:1px solid var(--line); border-radius:8px; padding:9px 11px; margin:0 0 8px; overflow-x:auto; }
  .sharp-msg.md .md-pre code { background:none; padding:0; }
  .sharp-msg.md .md-q { margin:0 0 8px; padding:2px 0 2px 10px; border-left:3px solid var(--line); color:var(--mut); }
  .sharp-msg.md .md-hr { border:0; border-top:1px solid var(--line); margin:10px 0; }
  .sharp-msg.md .md-tw { overflow-x:auto; margin:0 0 8px; }   /* a wide table scrolls inside the bubble — the drawer never widens */
  .sharp-msg.md .md-t { border-collapse:collapse; font-size:13px; }
  /* ⚠ CELLS WRAP, AND THE CAP IS WHAT MAKES THAT WORK. (SP-294 UX pass, 2026-09-16.)
     These were `white-space:nowrap`, which is right for a table of short numbers and catastrophic for
     one with a sentence in it. Measured on the live drawer: a "Why" column of one-line explanations
     rendered 1495px wide, taking the table to 1883px inside a 355px bubble — 81% of it hidden behind a
     thin horizontal scrollbar, including the entire column carrying the reasoning.
     `max-width` is the load-bearing part: without a cap, `white-space:normal` alone still lets one
     prose column take almost all the width under auto layout. With it, long cells wrap at a readable
     measure, short ones stay short, and a genuinely wide table still overflows into the scroll the
     .md-tw wrapper provides. `anywhere` is for the unbroken tokens this content is full of — long
     book names and +19.5 (-110) style prices. */
  .sharp-msg.md .md-t th, .sharp-msg.md .md-t td { border:1px solid var(--line); padding:4px 8px; text-align:left; white-space:normal; overflow-wrap:anywhere; max-width:28ch; }
  .sharp-msg.md .md-t th { background:var(--panel2); font-weight:600; }
  .sharp-msg.md a { color:var(--accent); }
  .sharp-chips { display:flex; flex-wrap:wrap; gap:7px; align-self:flex-start; max-width:96%; margin-top:-2px; }
  .sharp-chip { font-size:12.5px; padding:7px 12px; border:1px solid var(--accent); border-radius:999px; background:var(--accent-dim); color:var(--accent); cursor:pointer; font-weight:600; }
  .sharp-chip:hover { background:color-mix(in srgb,var(--accent) 22%,transparent); }
  /* SP-328 follow-up: David's own feedback — the first-open voice question read as just another chat
     bubble ("I had to look a few times before I saw it"). A plain .sharp-msg looks identical to every
     other line in the thread by design; this needs to interrupt the scan instead of joining it, so it
     gets its own bordered, accent-tinted card, closer to a small in-thread pop-up than a message. Kept
     inline (not a real modal) so it still reads top-to-bottom with the greeting above it and never
     blocks the rest of the chat. Same border/radius/gradient-wash treatment as the Home screen's .dcard
     (Survivor deadline rail) — the existing "this needs your attention" component on this product —
     rather than a bespoke glow. Design Lead review, 2026-09-18: the brand book calls restraint the
     point ("do not turn the interface into a sea of green gradients"); reusing .dcard's own flat
     gradient wash instead of an added box-shadow keeps this reading as the same system, not two
     competing "pay attention" treatments. */
  .sharp-voice-ask { display:flex; gap:11px; align-items:flex-start; align-self:stretch; max-width:96%; margin:2px 0 4px;
    background:linear-gradient(180deg,var(--accent-dim),transparent); border:1px solid var(--accent); border-radius:12px; padding:13px 15px;
    transition:background .3s ease, border-color .3s ease, padding .3s ease; }
  .sharp-voice-ask .sva-ic { color:var(--accent); flex:0 0 auto; margin-top:1px; transition:color .3s ease; }
  .sharp-voice-ask .sva-t { font-weight:700; font-size:15px; margin-bottom:10px; transition:font-weight .3s ease, font-size .3s ease, color .3s ease, margin .3s ease; }
  .sharp-voice-ask .sva-btns { display:flex; gap:8px; flex-wrap:wrap; }
  .sharp-voice-ask button { border-radius:999px; font-size:13.5px; padding:7px 14px; }
  /* UX review, 2026-09-18: once answered, this has done its job — full accent-card weight forever would
     keep competing with the four real next-step chips right below it, especially after "Yes" where the
     card's own text is no longer a question. Settle it down to a plain muted line, same visual register
     as `.sharp-msg.hint`, so attention moves on to what to actually do next. */
  .sharp-voice-ask.sva-done { background:none; border-color:transparent; padding:2px 0 0; }
  .sharp-voice-ask.sva-done .sva-ic { color:var(--mut); }
  .sharp-voice-ask.sva-done .sva-t { font-weight:400; font-size:12.5px; color:var(--mut); margin-bottom:0; }
  .typing { display:inline-flex; gap:5px; padding:2px 0; }
  .typing i { width:7px; height:7px; border-radius:50%; background:var(--mut); display:inline-block; animation:bob 1s infinite ease-in-out; }
  .typing i:nth-child(2){ animation-delay:.16s; } .typing i:nth-child(3){ animation-delay:.32s; }
  @keyframes bob { 0%,70%,100%{transform:translateY(0);opacity:.35} 35%{transform:translateY(-6px);opacity:1} }
  .sharp-input { border-top:1px solid var(--line); padding:12px; display:flex; gap:8px; min-width:0; }
  .sharp-input textarea { flex:1; min-width:0; width:100%; resize:none; height:46px; } /* min-width:0 lets the flex textarea shrink to the drawer instead of overflowing off-screen (mobile clip fix) */
  .sharp-input .mic { flex:0 0 auto; width:46px; height:46px; border:1px solid var(--line); border-radius:10px; background:var(--panel2); font-size:18px; cursor:pointer; }
  .sharp-input .mic.rec { border-color:var(--bad); background:rgba(255,77,79,.14); animation:micpulse 1.1s infinite; }
  @keyframes micpulse { 0%,100%{ box-shadow:0 0 0 0 rgba(255,77,79,.5);} 50%{ box-shadow:0 0 0 6px rgba(255,77,79,0);} }
  #sharpImgPreview:empty { display:none; }
  #sharpImgPreview { display:flex; align-items:center; gap:8px; padding:8px 12px 0; }
  #sharpImgPreview img { height:52px; width:52px; object-fit:cover; border-radius:8px; border:1px solid var(--line); }
  #sharpImgPreview button { flex:0 0 auto; border:1px solid var(--line); background:var(--panel2); border-radius:8px; padding:4px 8px; cursor:pointer; }
  @media (max-width:640px){ .grid{grid-template-columns:1fr;} .filters{gap:10px;} }
  /* bell */
  .bell { position:relative; display:inline-block; }
  #bellBtn { position:relative; padding:8px 11px; }
  .bell-count { position:absolute; top:-6px; right:-6px; background:var(--bad); color:#fff; font-size:10px; font-weight:700; min-width:16px; height:16px; line-height:16px; border-radius:999px; padding:0 4px; text-align:center; }
  .bell-menu { position:absolute; right:0; top:44px; width:min(360px,92vw); max-height:70vh; overflow-y:auto; background:var(--panel); border:1px solid var(--line); border-radius:12px; box-shadow:0 8px 28px rgba(0,0,0,.5); z-index:40; padding:6px; text-align:left; }
  /* PHONE — the panel was rendering off the LEFT edge of the viewport, clipping the first character of every
     line ("Injury update" read as "njury update"). Cause: it is anchored `right:0` to the BELL, and the bell
     is not the rightmost control in the header — refresh and the account avatar sit to its right. So a
     92vw-wide panel hung from a button at ~75vw starts at roughly -17vw, off-screen, and `overflow-y:auto`
     gives no horizontal scroll to recover it.
     Anchoring to the viewport instead of the button is the fix. `position:fixed` is safe here: nothing in the
     ancestor chain sets transform, filter, backdrop-filter, perspective, will-change or contain, so it
     resolves against the viewport and not a containing block. (header is position:sticky, which does NOT
     create one.) Checked before writing this, because a fixed element under a transformed ancestor silently
     anchors to the ancestor and would reproduce the same class of bug. */
  @media (max-width:599px){
    .bell-menu { position:fixed; left:10px; right:10px; top:56px; width:auto; max-height:min(70vh,calc(100vh - 72px)); }
  }
  .bell-menu .n { padding:10px 12px; border-radius:8px; border-bottom:1px solid var(--line); }
  .bell-menu .n:last-child { border-bottom:none; }
  .bell-menu .n.unread { background:var(--panel2); }
  .bell-menu .n-body { font-size:13px; color:var(--mut); margin-top:3px; line-height:1.5; overflow-wrap:anywhere; white-space:normal; }
  /* :not(.n-body-multi) — the multi-item card below needs normal block layout for its rows; the
     webkit-box/line-clamp display here fights that (both targeted .n-body.clamp, so a multi-item card
     used to get BOTH rules at once). */
  .bell-menu .n-body.clamp:not(.n-body-multi) { display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
  /* a multi-item bundle (several injury updates in one notification) clamps by ROW COUNT, not text lines.
     Layout lives HERE, not as an inline style on the row (core/.../index.html), specifically so this
     nth-child rule can win: an inline style attribute always beats an external stylesheet rule regardless
     of selector specificity, which is what silently broke the first version of this collapse. */
  .bell-menu .n-body-row { display:flex; gap:8px; padding:8px 0; }
  .bell-menu .n-body-row:not(:last-child) { border-bottom:1px solid var(--line); }
  .bell-menu .n-body-multi.clamp .n-body-row:nth-child(n+3) { display:none; }
  .bell-menu .n-more { display:inline-block; margin-top:4px; font-size:11px; font-weight:600; color:var(--accent); cursor:pointer; user-select:none; }
  .bell-menu .n-time { font-size:11px; color:var(--mut); margin-top:5px; }
  .bell-menu .n-link { cursor:pointer; } .bell-menu .n-link:hover { background:var(--panel2); }
  .bell-menu .n-link .n-time { color:var(--accent); }
  .bell-empty { color:var(--mut); padding:18px; text-align:center; font-size:13px; }
  /* DK/FD highlight — the most-used US books (others still shown for users in any state) */
  .play.mybook { border-left:3px solid var(--blue); }
  /* My Bets live tracker */
  .bet-score { font-family:var(--font-num); font-variant-numeric:tabular-nums; font-weight:700; font-size:15px; }
  .bet-live { color:var(--accent); font-weight:700; font-size:11.5px; letter-spacing:.3px; } /* brand green, matching every "good" signal */
  .bet-live .dot { display:inline-block; width:7px; height:7px; border-radius:50%; background:var(--accent-bright); margin-right:5px; animation:sharpBreathe 1.6s ease-in-out infinite; } /* the louder beacon green ONLY on the pulsing dot */
  .ats { font-size:12px; font-weight:800; padding:2px 9px; border-radius:999px; border:1px solid var(--line); white-space:nowrap; }
  .ats.covering, .ats.won { color:var(--accent); border-color:var(--accent); }
  .ats.losing, .ats.lost { color:var(--bad); border-color:var(--bad); }
  .ats.push { color:var(--mut); }
  .mybets-sum { display:flex; gap:14px; flex-wrap:wrap; margin:2px 2px 10px; color:var(--mut); font-size:13px; }
  .mybets-sum b { color:var(--txt); }
  /* performance scoreboard — rolling record / win% / units / ROI over settled bets, honoring the filters */
  .scoreboard { margin:2px 2px 10px; background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:12px 14px; }
  .scoreboard .sb-scope { font-size:11.5px; text-transform:uppercase; letter-spacing:.04em; color:var(--mut); margin-bottom:7px; }
  .scoreboard .sb-row { display:flex; gap:18px; flex-wrap:wrap; align-items:baseline; }
  .scoreboard .sb-stat { display:flex; flex-direction:column; gap:1px; }
  .scoreboard .sb-val { font-size:19px; font-weight:800; color:var(--txt); line-height:1.1; }
  .scoreboard .sb-val.pos { color:var(--accent); } .scoreboard .sb-val.neg { color:var(--bad); }
  .scoreboard .sb-lbl { font-size:11px; color:var(--mut); text-transform:uppercase; letter-spacing:.03em; }
  .scoreboard .sb-note { font-size:11.5px; color:var(--mut); margin-top:7px; }
  .scoreboard .sb-empty { font-size:13px; color:var(--mut); }
  /* league lens chips (All · CFB · NFL) */
  .lg-lens { display:flex; gap:6px; flex-wrap:wrap; margin:0 2px 10px; }
  .lg-lens button { font-size:12.5px; font-weight:700; padding:6px 13px; border-radius:999px; cursor:pointer;
    background:var(--panel2); border:1px solid var(--line); color:var(--mut); }
  .lg-lens button.on { background:var(--accent-dim); border-color:var(--accent); color:var(--txt); }
  /* Scoreboard tab — the full day's slate (NFL+CFB), live/upcoming/final, independent of any bet */
  .liveboard-controls { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin:0 2px 12px; }
  .liveboard-daynav { display:flex; gap:4px; }
  .liveboard-daynav button, .liveboard-tz button { font-size:12.5px; font-weight:700; padding:6px 11px; border-radius:999px; cursor:pointer;
    background:var(--panel2); border:1px solid var(--line); color:var(--mut); }
  .liveboard-daynav button.on, .liveboard-tz button.on { background:var(--accent-dim); border-color:var(--accent); color:var(--txt); }
  .liveboard-tz { display:flex; gap:4px; margin-left:auto; }
  .liveboard-row { display:flex; align-items:center; gap:12px; padding:11px 14px; background:var(--panel); border:1px solid var(--line); border-radius:12px; margin:0 2px 8px; }
  .liveboard-row.final { opacity:.72; }
  .liveboard-lg { font-size:10.5px; font-weight:800; letter-spacing:.04em; color:var(--mut); border:1px solid var(--line); border-radius:6px; padding:2px 6px; flex-shrink:0; }
  .liveboard-matchup { flex:1 1 auto; min-width:0; }
  .liveboard-teams { font-weight:700; font-size:14px; color:var(--txt); }
  .liveboard-teams .sc { font-family:var(--font-num); font-variant-numeric:tabular-nums; color:var(--accent); font-weight:800; margin:0 1px; } /* score pops distinct from the team name — was indistinguishable from an AP ranking prefix */
  .liveboard-meta { font-size:11.5px; color:var(--mut); margin-top:2px; }
  .liveboard-status { flex-shrink:0; text-align:right; font-size:12.5px; }
  /* close-game highlight — Q4+/within one score (≤7), bubbled above other live games (see .close sort) */
  .liveboard-row.close { border-color:var(--warn); background:rgba(210,153,34,.08); }
  .close-tag { color:var(--warn); font-weight:700; margin-left:7px; white-space:nowrap; }
  .close-tag .ic { vertical-align:-0.1em; }
  /* Scoreboard's sharp/steam note — one plain-English sentence, entitled Game Lines members only
     (see renderScoreboardTab); "Ask The Angle" opens the chat for the full breakdown. */
  .sharp-note { margin-top:3px; font-size:11.5px; color:var(--mut); line-height:1.4; }
  .sharp-ask { color:var(--accent); font-weight:700; cursor:pointer; white-space:nowrap; }
  /* transient nudge after logging a bet — points to where you can watch it */
  .bet-toast { position:fixed; left:50%; bottom:22px; transform:translateX(-50%) translateY(12px); z-index:40; background:var(--panel); border:1px solid var(--accent); border-radius:999px; padding:9px 16px; font-size:13px; color:var(--txt); box-shadow:0 6px 20px rgba(0,0,0,.4); opacity:0; transition:opacity .25s, transform .25s; pointer-events:none; }
  .bet-toast.show { opacity:1; transform:translateX(-50%) translateY(0); pointer-events:auto; }
  .bet-toast a { color:var(--accent); font-weight:700; cursor:pointer; }
  /* TA-489 follow-up — the PWA path's "verified, tap to continue" card after a slow BetSaracen check:
     this replaces an auto-click the member is used to, so it must read as the primary action still
     pending, not a passive toast — a real button, not small print at the bottom of the screen. */
  .yx-continue-card { position:fixed; left:12px; right:12px; bottom:56px; z-index:50; max-width:420px;
    margin:0 auto; background:var(--panel); border:3px solid #34d399; border-radius:14px;
    padding:14px; box-shadow:0 12px 32px rgba(0,0,0,.5), 0 0 0 1px rgba(52,211,153,.35);
    opacity:0; transform:translateY(16px); transition:opacity .25s, transform .25s; pointer-events:none; }
  .yx-continue-card.show { opacity:1; transform:translateY(0); pointer-events:auto; }
  .yx-continue-card .yx-cc-lbl { font-size:13px; color:var(--mut); margin-bottom:10px; text-align:center;
    padding-right:22px; }
  .yx-continue-card a.primary { display:block; text-align:center; padding:13px; border-radius:10px;
    font-size:15.5px; text-decoration:none; }
  .yx-continue-card .yx-cc-close { position:absolute; top:8px; right:8px; width:28px; height:28px;
    display:flex; align-items:center; justify-content:center; background:transparent; border:none;
    color:var(--mut); cursor:pointer; border-radius:999px; }
  .yx-continue-card .yx-cc-close:hover { color:var(--txt); background:var(--panel2); }
  /* inline SVG icons (Lucide) — inherit color via currentColor, sized in em */
  .ic { display:inline-block; vertical-align:-0.14em; flex:0 0 auto; }
  button .ic, .iconbtn .ic { vertical-align:middle; }
  /* tap-to-learn "?" explainer */
  .help { display:inline-flex; align-items:center; justify-content:center; width:15px; height:15px; border-radius:50%; border:1px solid var(--mut); color:var(--mut); font-size:10px; font-weight:700; margin-left:6px; cursor:pointer; vertical-align:middle; }
  .help:hover { color:var(--txt); border-color:var(--txt); }
  .helpbox { position:absolute; z-index:50; background:var(--panel2); border:1px solid var(--line); border-radius:8px; padding:10px 12px; font-size:12.5px; line-height:1.5; color:var(--txt); box-shadow:0 6px 20px rgba(0,0,0,.5); display:none; }
  /* bet-capture */
  .betwrap { margin-top:9px; }
  .betbtn { padding:5px 10px; font-size:12.5px; border-radius:7px; }
  /* TA-341 final UX piece — betbtn-exec: the "✓ I placed this bet" logging control ONLY, scoped so it
     never touches teaser/parlay/survivor .betbtn usages elsewhere. Kept deliberately NEUTRAL (same
     --panel2/--line/--txt as the base `button` rule, never green) — this is a LOG, not the sportsbook
     handoff, and must never be visually confused with a BET/VIEW/OPEN execution CTA above it. */
  .betbtn-exec { width:100%; min-height:44px; font-size:13px; font-weight:700; }
  /* ═══ TA-341 final UX piece — sportsbook EXECUTION MODULE ═══════════════════════════════════════════
     Presentation only: same resolveDeepLink()/sbClickAttrs data every book-row's CTA carries today, just
     laid out as a real "Your Best" / "Best Available" module instead of inline badge text. No card-level
     blue accent — blue lives ONLY inside the module (label + Best Available buttons), per Design Lead
     review of the approved mockup (https://claude.ai/artifact/ELSBADdJiKSbs8e556AevT). */
  .exec { border-top:1px solid var(--line); padding-top:8px; margin-top:8px; display:flex; flex-direction:column; gap:7px; }
  .exec-group { display:flex; flex-direction:column; gap:5px; }
  /* TA-341 REOPENED Priority 1 — Your Angle: visually SEPARATE from the frozen Market Angle tier chip
     (confBlock/.conf-chip/.potd-prime), never merged into it — a distinct labeled row so the two reads
     can never be mistaken for one grade. .na (not executable / juice-killed) mutes the tone; a graded
     read reuses the SAME .conf-chip tier coloring as Market Angle (same tier vocabulary, honestly earned
     fresh) so a matching tier LOOKS the same on purpose, while a differing tier reads at a glance. */
  .your-angle { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:6px 2px 2px; padding:8px 10px; border:1px solid var(--line); border-radius:9px; background:var(--panel2); font-size:12.5px; }
  .your-angle.na { border-color:color-mix(in srgb, var(--mut) 40%, var(--line)); }
  .ya-lbl { font-size:10px; letter-spacing:.06em; text-transform:uppercase; color:var(--mut); font-weight:800; }
  .ya-txt { color:var(--txt); }
  .your-angle.na .ya-txt { color:var(--mut); }
  /* Card UX V2 Phase 2 — Market-vs-Your comparison, split by how much it needs the reader's attention.
     .compact: the two tiers AGREE (cmp==='SAME') — Market Angle's chip above already said the tier once
     and the Your Best row below restates book/point/price with its own chip, so this collapses to one
     quiet confirmation line instead of a second full bordered box (the single biggest repeat offender on
     a matching card). .weaker/.stronger: the two tiers DIFFER (PASS/NOT_EXECUTABLE always count as
     .weaker) — kept at full visual weight and color-coded, because THIS is the comparison David wants
     "immediately understandable" without reading prose first. */
  .your-angle.compact { border:none; background:none; padding:1px 2px 0; margin:4px 2px 0; }
  .your-angle.compact .ya-txt { color:var(--mut); font-size:11.5px; }
  .your-angle.weaker { border-color:color-mix(in srgb, var(--warn) 45%, var(--line)); background:color-mix(in srgb, var(--warn) 7%, var(--panel2)); }
  .your-angle.stronger { border-color:color-mix(in srgb, var(--accent) 45%, var(--line)); background:color-mix(in srgb, var(--accent) 7%, var(--panel2)); }
  /* flex+wrap (not nowrap) — the explanatory sentence now nests INSIDE this span as one continuous
     phrase (see renderYourAngle's own header note), so this must wrap like a normal paragraph on a
     narrow card, never force a single unbroken line that could overflow at 390px. */
  .ya-cmp { display:flex; flex-wrap:wrap; align-items:baseline; gap:5px; font-size:12px; font-weight:700; color:var(--txt); }
  .ya-cmp b { font-weight:800; }
  .ya-cmp .ya-txt { font-weight:400; }
  /* the standalone "MARKET ANGLE" label used to be its own full margin-bearing line above confBlock's
     chip; this inline variant sits tight over the chip instead of claiming a whole line of its own. */
  .mkt-lbl { font-size:10px; letter-spacing:.06em; text-transform:uppercase; color:var(--mut); font-weight:800; margin:6px 2px 0; }
  .qa-note { font-size:10.5px; color:var(--mut); margin:4px 2px 0; }
  .exec-label { display:flex; align-items:center; gap:7px; font-size:10.5px; font-weight:800; letter-spacing:.09em; text-transform:uppercase; }
  .exec-label.mine { color:var(--accent); }
  .exec-label.avail { color:var(--blue); }
  .exec-label .line { flex:1; height:1px; background:currentColor; opacity:.25; }
  .book-row { display:flex; align-items:center; gap:10px; background:var(--panel2); border:1px solid var(--line); border-radius:9px; padding:6px 6px 6px 12px; min-height:44px; flex-wrap:wrap; }
  /* TA-341 REOPENED Priority 2 — the RECOMMENDED row among 2+ of the member's own books, real visual
     weight (not just "first in the list") so the member doesn't have to compare BET buttons themselves. */
  .book-row.rec { border-color:var(--accent); background:color-mix(in srgb, var(--accent) 9%, var(--panel2)); }
  .book-info { flex:1; min-width:0; }
  .book-name { font-size:13.5px; font-weight:700; display:block; word-break:break-word; }
  .book-price { font-size:12.5px; color:var(--mut); font-variant-numeric:tabular-nums; }
  .book-price b { color:var(--txt); font-weight:700; }
  .owned-tag, .reason-tag, .reason-at { font-size:10.5px; color:var(--mut); font-weight:400; }
  /* Plays Discovery & Availability UX (2026-10-12), decision #4 — the ONE "unmistakable" variant of
     .owned-tag, used only when a card has NO owned-book row at all and this label is the sole thing
     telling the member they can't place this specific bet through their own books. Reuses the same
     warn palette .your-angle.weaker already uses for the identical "not for you" meaning, so the two
     surfaces read as one consistent visual language rather than two different warning styles. */
  .owned-tag.notmine-strong { font-size:11px; font-weight:700; color:var(--warn); }
  .rec-tag { font-size:10.5px; color:var(--accent); font-weight:800; text-transform:uppercase; letter-spacing:.04em; }
  /* real ~44px tap target (Apple HIG / Android both land near 44-48dp) — a genuine <a> button, not an inline text link */
  .cta { flex-shrink:0; display:inline-flex; align-items:center; justify-content:center; gap:4px; font-size:12.5px; font-weight:800; border-radius:8px; padding:0 14px; white-space:nowrap; text-decoration:none; border:1px solid transparent; min-height:44px; min-width:66px; }
  .cta.bet { background:var(--accent); color:#08130a; }
  /* TA-491 (UX Reviewer finding, 2026-09-22) — a runner-up row's BET CTA rendered as the SAME solid,
     equally-weighted green button as the RECOMMENDED row's — paired with its honest "Better price"/
     "Better number" reason tag, this read as a competing steer (a member skimming could tap the row
     LABELED "better" thinking that's the advice, the opposite of the intended meaning). Only the
     RECOMMENDED row keeps the solid primary treatment now; every other row's bet CTA is the SAME
     secondary outline style `.cta.view` already uses elsewhere — still a real, fully working button,
     just visually unambiguous about which one is the actual recommendation. Copy itself (SHOP_REASON_
     LABEL, confirmed with David for TA-341) is untouched — this is a visual fix, not a wording change. */
  .book-row:not(.rec) .cta.bet { background:transparent; border-color:var(--accent); color:var(--accent); }
  .cta.view { background:transparent; border-color:var(--blue); color:var(--blue); }
  .cta.open { background:transparent; border-color:var(--line); color:var(--mut); }
  .book-nolink { font-size:13px; color:var(--mut); flex-shrink:0; padding:0 6px; }
  .book-nolink-note { flex-basis:100%; font-size:11px; color:var(--mut); opacity:.75; padding:0 12px 2px; }
  /* Best Angles card — compact book buttons (David, 2026-10-17; REBUILT same day after he caught the
     first two cuts not matching what he'd actually approved — see _baBookButtonsHtml()'s own header
     comment and the Already-Bet Card Concept artifact, K24W5Y2UU5fQbZUiqYFU7C, for the exact approved
     shape). ONE outlined button per book — book + line + price inside ONE tap target, border+text tinted
     by that book's own tier, transparent fill, small arrow. Not a row, not a separate chip glued to a
     separately-colored CTA pill — that was the mistake both earlier cuts made. */
  .ba-books { margin-top:8px; display:flex; flex-wrap:wrap; gap:6px; }
  .ba-bookbtn { display:inline-flex; align-items:center; gap:4px; font-size:11.5px; font-weight:600; padding:5px 9px; border-radius:7px; border:1px solid var(--line); color:var(--txt); line-height:1.2; text-decoration:none; }
  .ba-bookbtn .ba-bn { color:var(--mut); font-weight:500; }
  .ba-bookbtn.tap { cursor:pointer; }
  .ba-bookbtn .ba-ar { margin-left:1px; }
  /* Tier is the ONLY color signal here — border + text, same tokens used everywhere else on the site
     (conf-chip's Prime/Strong tokens), transparent fill so it reads as an outline chip, not a filled CTA.
     Solid shares Strong's teal (same as .conf-Solid elsewhere) at a dimmer border — the two are told apart
     by which tier word a member already knows from the card's main chip, same as the rest of the site. */
  .ba-bookbtn.t-prime { border-color:var(--prime); color:var(--prime); }
  .ba-bookbtn.t-prime .ba-bn { color:var(--prime); }
  .ba-bookbtn.t-prime.tap:hover { background:color-mix(in srgb, var(--prime) 14%, transparent); }
  .ba-bookbtn.t-strong { border-color:var(--tier-strong); color:var(--tier-strong); }
  .ba-bookbtn.t-strong .ba-bn { color:var(--tier-strong); }
  .ba-bookbtn.t-strong.tap:hover { background:var(--tier-strong-dim); }
  .ba-bookbtn.t-solid { border-color:color-mix(in srgb, var(--tier-strong) 45%, transparent); color:var(--tier-strong); }
  .ba-bookbtn.t-solid .ba-bn { color:var(--mut); }
  .ba-bookbtn.t-solid.tap:hover { background:var(--tier-strong-dim); }
  /* Pass (the math says no) and no-tier (a Props book — no sharp reference to grade against) both stay
     plain grey, never colored like a real recommendation — showing the number is honest, coloring it
     like a good bet would not be. They're visually identical on purpose: neither is a bet to take. */
  .ba-bookbtn.t-pass, .ba-bookbtn.t-none { color:var(--mut); }
  .ba-bookbtn.t-pass .ba-bn, .ba-bookbtn.t-none .ba-bn { color:var(--mut); }
  .ba-books-hint { font-size:10.5px; color:var(--mut); margin-top:5px; }
  /* Home's play row centers its two flex children by default, fine when both sides are one line tall —
     once the left side can grow (bet badge + book buttons), top-align instead so the edge/EV figure on
     the right doesn't float in the middle of a now-taller card. */
  .home-play.ba-home-play { align-items:flex-start; }
  .betform { display:flex; gap:6px; align-items:center; flex-wrap:wrap; }
  .betform input { padding:6px 8px; font-size:13px; width:auto; }
  .betmsg { font-size:12px; }
  /* Manual "+ Log a bet" capture — for teasers/parlays/props/spreads Sharp didn't surface (incl. off-feed books like BetSaracen) */
  .logbet-open { display:inline-flex; align-items:center; gap:7px; font-size:13px; font-weight:700; color:var(--accent); background:var(--accent-dim); border:1px solid var(--accent); border-radius:9px; padding:8px 14px; cursor:pointer; margin:2px 2px 12px; }
  .logbet-open:hover { background:color-mix(in srgb,var(--accent) 20%,transparent); }
  .logbet { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:14px; margin:2px 2px 14px; }
  .logbet h4 { margin:0 0 10px; font-size:14px; color:var(--txt); }
  .lb-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(130px,1fr)); gap:9px; }
  .lb-field { display:flex; flex-direction:column; gap:4px; }
  .lb-field label { font-size:11.5px; font-weight:600; color:var(--mut); text-transform:uppercase; letter-spacing:.03em; }
  .lb-field input, .lb-field select { padding:8px 10px; font-size:14px; }
  .lb-seg { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:12px; }
  .lb-seg button { flex:1 1 auto; min-width:74px; padding:8px 6px; font-size:13px; font-weight:600; border-radius:8px; background:var(--panel2); border:1px solid var(--line); color:var(--mut); cursor:pointer; }
  .lb-seg button.on { background:var(--accent-dim); border-color:var(--accent); color:var(--accent); }
  .lb-leg { display:flex; flex-wrap:wrap; gap:8px; align-items:end; margin-bottom:8px; }
  .lb-leg .lb-field { flex:1 1 120px; }
  .lb-leg .lb-rm { padding:8px 0; font-size:15px; line-height:1; background:var(--panel2); border:1px solid var(--line); color:var(--mut); border-radius:8px; cursor:pointer; }
  .lb-addleg { font-size:12.5px; font-weight:600; color:var(--blue); background:none; border:1px dashed var(--line); border-radius:8px; padding:7px 12px; cursor:pointer; margin:2px 0 12px; }
  .lb-actions { display:flex; gap:10px; align-items:center; margin-top:12px; flex-wrap:wrap; }
  .lb-hint { font-size:12px; color:var(--mut); margin:0 0 12px; }
  /* SP-274 — the same in-form hint slot, in the problem colour. A modifier rather than an inline
     style so the size and spacing stay tied to .lb-hint and cannot drift away from its three siblings. */
  .lb-hint.warn { color:var(--warn); }
  /* The one field a completion is about. Cleared on first input (see the edit handler) so the form
     stops flagging a problem the member has just fixed. */
  .logbet input.needs { border-color:var(--warn); }
  /* Sharp-parsed "log this bet?" confirm chip (nothing saves until tapped) */
  .logbet-chips { margin-top:10px; background:var(--panel2); border:1px solid var(--accent); border-radius:10px; padding:11px 12px; }
  .lbc-head { font-size:13px; font-weight:700; color:var(--txt); margin-bottom:6px; }
  .lbc-item { font-size:13px; color:var(--txt); margin:3px 0; }
  .lbc-item.lbc-dup { opacity:.72; }
  .lbc-dupflag { color:var(--warn); font-weight:700; font-size:11.5px; white-space:nowrap; }
  .lbc-actions { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-top:9px; }
  .lbc-link { cursor:pointer; color:var(--blue); font-weight:600; font-size:13px; }
  /* Sharp-parsed Survivor pick → an EDITABLE card, not a claim (SP-243 §2).
     Accent border, matching .logbet-chips below. The first cut used a grey --line border to signal
     "this is a question, not a claim"; the Design Lead gate ruled against it and is right — it made the
     card RECEDE into the message bubble rather than read as a sibling of the log-a-bet chip, and the
     form/claim distinction is already carried, far more strongly, by the head copy ("Want to record a
     pick? Here is what I heard"), the "nothing is saved until you press confirm" line, and the labelled
     editable controls. Desaturating the container was paying for that distinction twice, in the one
     currency that also costs legibility.

     ⚠ white-space:normal IS LOAD-BEARING, DO NOT REMOVE IT. .sharp-msg sets white-space:pre-wrap so the
     model's own line breaks survive, and this card is appended INSIDE one. Every newline-and-indent
     between the divs of a multi-line template literal therefore rendered as a LITERAL BLANK LINE: the
     gate measured ~43px of dead space where the CSS asks for 4-9px, on a card already 444px tall next
     to a 140px bet chip, and it grew by five more phantom lines per extra pick row. renderLogbetChips is
     immune only by accident — it happens to be typed as single-line concatenated strings. Resetting the
     property here fixes the whole subtree by construction, so the next person to format this markup
     readably cannot re-introduce it. */
  .svpick { margin-top:10px; background:var(--panel2); border:1px solid var(--accent); border-radius:10px; padding:11px 12px; white-space:normal; }
  .svp-head { font-size:13px; font-weight:700; color:var(--txt); margin-bottom:4px; display:flex; align-items:center; gap:6px; }
  .svp-note { font-size:12px; color:var(--mut); margin-bottom:9px; }
  .svp-single { font-size:12px; margin-bottom:8px; }
  /* ⚠ A GRID, NOT A WRAPPING FLEX ROW. As flex-wrap, the remove control dropped onto a line of its own
     below the week/team pair at the ~304px a 390px phone actually gives this drawer — orphaned from the
     row it removes, and worse with every extra row in a batch. Three columns pin it beside its own
     controls at every width; the team select takes the slack and minmax(0,1fr) lets it actually shrink
     (a bare 1fr floors at the select's content width and overflows instead). */
  .svp-row { display:grid; grid-template-columns:auto minmax(0,1fr) auto; gap:8px; align-items:end; padding:7px 0; border-top:1px solid var(--line); }
  .svp-row:first-child { border-top:none; padding-top:0; }
  .svp-field { display:flex; flex-direction:column; gap:4px; min-width:0; }
  .svp-field label { font-size:11.5px; font-weight:600; color:var(--mut); text-transform:uppercase; letter-spacing:.03em; }
  .svp-field select { padding:7px 9px; font-size:14px; width:100%; }
  .svp-rm { display:inline-flex; align-items:center; justify-content:center; padding:8px 10px; font-size:15px; line-height:1; background:var(--panel); border:1px solid var(--line); color:var(--mut); border-radius:8px; cursor:pointer; }
  .svp-rm:hover { color:var(--txt); border-color:var(--mut); }
  .svp-status { grid-column:1 / -1; font-size:12px; color:var(--mut); display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
  .svp-status.good { color:var(--accent); font-weight:600; }
  .svp-status.warn { color:var(--warn); }
  .svp-status.bad  { color:var(--bad); }
  /* ⚠ 44px MINIMUM ON EVERY TAP TARGET IN THIS CARD. Confirm and Dismiss were 26px — under the touch
     guidance, for the most important tap in the feature — and the replace answers carry a decision that
     is now the primary action of their row, so they get it too. Scoped to .svpick rather than applied to
     .betbtn, which belongs to the bet form and is not this ticket's to resize. */
  .svpick .betbtn { min-height:44px; }
  .svp-status .betbtn { padding:4px 11px; font-size:12.5px; }
  /* ⚠ SAME 44px RULE, SAME REASON, FOR THE ⟦HCNOTIFY⟧ CARD (TA-772 round 5). Measured in a real
     headless render at 375px and 390px before this rule existed: the bell button came out 40px and
     BOTH "Email it to me" and "That's enough, thanks" came out 26px — the identical under-touch-guidance
     miss the .svpick rule above was written to fix, reintroduced simply because this new card reuses
     .betbtn without being .svpick. Scoped the same way, for the same stated reason: .betbtn itself
     belongs to the bet form and is not this ticket's to resize. */
  .hcnotify-chip .betbtn { min-height:44px; }
  .svp-actions { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-top:10px; }
  /* "You're on this" badge — persistent signal that you've already logged this bet (idea #2) */
  .betwrap.on { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
  .beton { display:inline-flex; align-items:center; gap:5px; font-size:12.5px; font-weight:700; color:var(--accent); background:var(--accent-dim); border:1px solid var(--accent); border-radius:7px; padding:4px 9px; }
  .betmini { padding:3px 7px; font-size:12px; border-radius:6px; background:var(--panel2); border:1px solid var(--line); color:var(--mut); cursor:pointer; line-height:1; }
  .betmini:hover { color:var(--txt); border-color:var(--mut); }
  /* TA-594 — "+ Add another": a worded (not icon-only) betmini twin, so it never gets mistaken for the
     icon-only ✎/✕ pair next to it — this one's a deliberate second bet, not an edit or a removal. */
  .betmini.betadd { display:inline-flex; align-items:center; gap:4px; padding:3px 9px; color:var(--accent); border-color:var(--accent); }
  .betmini.betadd:hover { background:var(--accent-dim); color:var(--accent); border-color:var(--accent); }
  .betclv { font-size:11.5px; font-weight:600; }
  .betclv.good { color:var(--accent); }
  .betclv.warn { color:var(--warn); }
  /* auto-update chip — a new deploy shipped while this (possibly days-old PWA) page was alive; one tap reloads */
  .update-chip { position:fixed; left:50%; transform:translateX(-50%); bottom:calc(18px + env(safe-area-inset-bottom,0px));
    z-index:9000; background:var(--accent); color:#0b1020; border:none; border-radius:999px; padding:11px 18px;
    font-size:13.5px; font-weight:800; box-shadow:0 6px 22px rgba(0,0,0,.4); cursor:pointer; animation:chipIn .25s ease-out; }
  .update-chip:hover { filter:brightness(1.06); }
  @keyframes chipIn { from { opacity:0; transform:translate(-50%,10px); } to { opacity:1; transform:translate(-50%,0); } }
  /* top-bar cluster (freshness dot · bell · refresh · account avatar) — never wraps. #who is the
     header's other flex child (besides .brand) and has no rule of its own otherwise.
     ⚠ flex-shrink:0 here, NOT min-width:0 — #who must be the flex item that DOESN'T give. Tried
     min-width:0 first: with header's two children at the default flex-shrink:1 each, the browser
     splits the shrink deficit between BOTH of them, proportional to their own max-content size — since
     the logo's natural size (~700px source image, before any of its own shrinking) so outweighs the
     topbar's (~190px), #who still got squeezed below what its flex-shrink:0 children need (measured:
     squeezed to 55px against a 188px minimum), and since #who itself has no overflow:hidden, its
     children just rendered past its shrunk box — the exact overflow this fix exists to remove, just
     moved one level down. flex-shrink:0 removes #who from the shrink calculation entirely, so 100% of
     any deficit lands on .brand (flex-shrink:1, min-width:0), which is the one thing meant to give. */
  #who { flex-shrink:0; }
  .topbar { display:flex; align-items:center; gap:10px; flex-wrap:nowrap; }
  /* Every control in the cluster is a fixed, already-tight size (36px icon/pill buttons, a 32px avatar) —
     none of it should ever be the thing that gives when space runs out. flex-shrink:0 here pairs with
     .brand's flex-shrink:1 (default) below: on a narrow phone the LOGO is the one thing allowed to get
     smaller, never these controls (a squished bell/refresh/avatar would look broken, not responsive). */
  .topbar > * { flex-shrink:0; }
  .fresh { display:inline-flex; align-items:center; gap:5px; min-width:0; cursor:pointer; border-radius:8px; }
  .fresh .dot { width:8px; height:8px; border-radius:50%; flex:0 0 auto; }
  .fresh .lbl { font-size:12px; color:var(--mut); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  /* ⚠ TA mobile-header overflow fix (2026-09-22): this label used to always read a short "Market 5h" —
     safe at any width. #348 (same day) changed it to full sentences ("Markets — freshness warning",
     "Markets — data problem"), up to ~28 chars, with no responsive treatment added alongside — that's
     what pushed the bell/refresh/avatar off the right edge of the viewport on phones (and, because
     nothing on the page stops horizontal growth, widened the whole document, which is what also made
     the second nav row's real edge look clipped mid-tab instead of at its own intentional fade+chevron).
     Below 600px the label collapses to just the colored dot; the full sentence is still reachable by tap
     (or hover on anything with a pointer) via the existing help-chip mechanism, wired in updateFreshness(). */
  @media (max-width:599px){
    .fresh { padding:8px 4px; margin:-8px -4px; } /* enlarge the tap target without changing the box's footprint in the row */
    .fresh .lbl { display:none; }
  }
  .iconbtn { position:relative; width:36px; height:36px; padding:0; display:inline-flex; align-items:center; justify-content:center; font-size:15px; border-radius:8px; }
  .iconbtn.spin { animation:spin 1s linear infinite; }
  @keyframes spin { to { transform:rotate(360deg); } }
  /* "＋ submit a promo" — subtle accent affordance in the top cluster, visible to any signed-in member */
  .iconbtn.promo-add { color:var(--accent); border-color:var(--accent); font-weight:800; }
  .iconbtn.promo-add:hover { background:var(--accent); color:var(--accent-ink); }
  .pillbtn { height:36px; padding:0 12px; display:inline-flex; align-items:center; gap:4px; font-size:13px; font-weight:700; border-radius:18px; white-space:nowrap; }
  .pillbtn.promo-add { color:var(--accent); border-color:var(--accent); }
  .pillbtn.promo-add:hover { background:var(--accent); color:var(--accent-ink); }
  /* ⚠ My Bets reverted to a plain main-nav tab, 2026-09-16 (was SP-4's top-right pill) — it takes the
     standard `.tab.active` styling like every other destination, so no dedicated rule is needed here. */
  /* A labelled Promo pill will not fit beside the bell, refresh and avatar on a phone. Below 600px it
     collapses to its icon (the title attribute carries the name). */
  @media (max-width:599px){
    .topbar .pillbtn { width:36px; padding:0; justify-content:center; border-radius:8px; }
    .topbar .pillbtn .pill-lbl { display:none; }
  }
  /* modal (shared shell for the submit-a-promo flow) */
  .modal-back { position:fixed; inset:0; background:rgba(0,0,0,.6); z-index:60; display:flex; align-items:flex-start; justify-content:center; padding:24px 12px; overflow-y:auto; }
  .modal { background:var(--panel); border:1px solid var(--line); border-radius:14px; width:min(460px,100%); padding:20px; box-shadow:0 12px 40px rgba(0,0,0,.55); }
  .modal h3 { margin:0 0 4px; font-size:18px; }
  .modal .sub { color:var(--mut); font-size:13px; margin-bottom:14px; }
  /* Was a full re-declaration of .eyebrow at .5px instead of .06em. Now it only carries what is
     genuinely its own: it is a block-level form label and needs the vertical rhythm. */
  .modal label.fld { display:block; margin:12px 0 5px; }
  .modal .file-drop { display:block; border:1px dashed var(--line); border-radius:10px; padding:16px; text-align:center; color:var(--mut); font-size:13.5px; line-height:1.5; cursor:pointer; background:var(--panel2); }
  .modal .file-drop.has { border-color:var(--accent); color:var(--txt); }
  .modal .row2 { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
  .modal .pubtoggle { display:flex; gap:8px; margin-top:6px; }
  .modal .pubtoggle button { flex:1; font-size:13px; }
  .modal .pubtoggle button.on { background:var(--accent); color:var(--accent-ink); border-color:var(--accent); font-weight:700; }
  .mid-intro { border:1px solid var(--accent); background:linear-gradient(180deg,var(--accent-dim),transparent); border-radius:10px; padding:11px 13px; }
  .modal .chips { display:flex; flex-wrap:wrap; gap:6px; }
  .modal .fld-group { margin:16px 0 2px; font-size:12px; font-weight:700; color:var(--accent); text-transform:uppercase; letter-spacing:.5px; border-top:1px solid var(--line); padding-top:12px; }
  .modal .tag-input { display:flex; flex-wrap:wrap; gap:6px; align-items:center; border:1px solid var(--line); border-radius:10px; background:var(--panel2); padding:7px 8px; min-height:42px; cursor:text; }
  .modal .tag-input .tag { display:inline-flex; align-items:center; gap:5px; background:var(--panel); border:1px solid var(--line); border-radius:999px; padding:4px 6px 4px 11px; font-size:13px; }
  .modal .tag-input .tag .tx { color:var(--mut); cursor:pointer; font-size:11px; font-weight:400; }
  .modal .tag-input .tag .tx:hover { color:var(--bad); }
  .modal .tag-input .tag-add { flex:1; min-width:90px; border:none; background:transparent; padding:4px; font-size:13.5px; outline:none; }
  .modal .pchip { font-size:12.5px; padding:6px 11px; border:1px solid var(--line); border-radius:999px; background:var(--panel2); color:var(--mut); cursor:pointer; }
  .modal .pchip.on { background:var(--accent); color:var(--accent-ink); border-color:var(--accent); font-weight:700; }
  .modal .m-actions { display:flex; gap:8px; margin-top:18px; }
  .modal .m-actions .primary { flex:1; }
  .modal .m-msg { margin-top:12px; font-size:13.5px; }
  .modal img.shot-preview { max-width:100%; border-radius:8px; margin-top:8px; border:1px solid var(--line); }
  /* ── onboarding (first-login shopping-modes intro) — additive, scoped under .onb so nothing here
     touches the shared .modal rules other flows (promo/profile) depend on. Reuses .mid-intro's
     accent-bordered gradient card for the hero, .fld-group's accent eyebrow-with-rule for step
     dividers, and .pchip for the book multi-select — same visual language as Profile, not a new
     modal chrome system. */
  .onb-hero { display:flex; gap:12px; align-items:flex-start; }
  .onb-hero .onb-hero-ic { flex:0 0 auto; width:38px; height:38px; border-radius:10px; background:var(--accent-dim); color:var(--accent); display:flex; align-items:center; justify-content:center; }
  .onb-hero h3 { margin:0 0 3px; font-size:17px; }
  .onb-hero .sub { margin:0; }
  .onb-step { margin:16px 0 2px; font-size:12px; font-weight:700; color:var(--accent); text-transform:uppercase; letter-spacing:.5px; border-top:1px solid var(--line); padding-top:14px; display:flex; align-items:center; gap:7px; }
  .onb-step .onb-step-n { display:inline-flex; align-items:center; justify-content:center; width:18px; height:18px; border-radius:50%; background:var(--accent); color:var(--accent-ink); font-size:10.5px; font-weight:800; flex:0 0 auto; }
  .onb-step-sub { color:var(--mut); font-size:13px; margin:4px 0 10px; line-height:1.4; }
  .onb-modes { display:flex; flex-direction:column; gap:8px; }
  .onb-mode { border:1px solid var(--line); border-radius:10px; padding:10px 12px; display:flex; gap:10px; align-items:flex-start; }
  .onb-mode .onb-mode-ic { flex:0 0 auto; margin-top:1px; }
  .onb-mode-name { font-size:12.5px; font-weight:800; letter-spacing:.03em; text-transform:uppercase; display:flex; align-items:center; gap:7px; flex-wrap:wrap; }
  .onb-mode-desc { color:var(--mut); font-size:12.5px; line-height:1.4; margin-top:2px; }
  .onb-mode.rec { border-color:var(--accent); background:var(--accent-faint); }
  .onb-mode.rec .onb-mode-name { color:var(--accent); }
  .onb-mode.avail .onb-mode-name { color:var(--blue); }
  .onb-rec-badge { font-size:9.5px; font-weight:800; letter-spacing:.06em; padding:1px 7px; border-radius:999px; border:1px solid var(--accent); color:var(--accent); background:var(--panel); }
  .onb-footnote { color:var(--mut); font-size:12px; margin-top:10px; line-height:1.4; }
  /* promos: two-group layout + pending strip */
  .promo-group-h { font-weight:700; font-size:14px; margin:16px 2px 6px; }
  .promo-card .shot-thumb { max-height:120px; border-radius:8px; margin-top:8px; border:1px solid var(--line); display:block; cursor:zoom-in; transition:border-color .12s, filter .12s; }
  .promo-card .shot-thumb:hover, .promo-card .shot-thumb:focus-visible { border-color:var(--accent); filter:brightness(1.06); outline:none; }
  .shot-light { position:fixed; inset:0; background:rgba(0,0,0,.85); z-index:80; display:flex; flex-direction:column; align-items:center; justify-content:center; padding:24px 12px; cursor:zoom-out; }
  .shot-light img { max-width:min(920px,96vw); max-height:88vh; border-radius:10px; border:1px solid var(--line); box-shadow:0 12px 48px rgba(0,0,0,.6); cursor:default; }
  .shot-light .shot-cap { color:#eaeaea; font-size:13px; margin-top:12px; max-width:min(920px,96vw); text-align:center; }
  .shot-light .shot-close { position:absolute; top:14px; right:18px; background:none; border:none; color:#fff; font-size:30px; line-height:1; cursor:pointer; padding:4px 10px; }
  .promo-state { font-size:11px; }
  .promo-hold { font-size:12px; margin-top:6px; font-style:italic; }
  /* discovery feed cards — visually distinct from Verified (solid) and user tips (plain) */
  .promo-feed { border-left:3px solid var(--accent); background:linear-gradient(90deg,var(--accent-faint),transparent 60%); }
  /* trust badge is ONE intact pill: inline-flex + nowrap so the ✓ and label never split across a wrap (flagship trust signal, mobile-safe) */
  .promo-src { display:inline-flex; align-items:center; gap:3px; font-size:11px; font-weight:600; padding:1px 8px; border-radius:10px; margin-left:6px; vertical-align:middle; white-space:nowrap; line-height:1.5; }
  .promo-src.verified { color:var(--accent-ink); background:var(--accent); border:1px solid var(--accent); }
  .promo-src.single { color:var(--mut); background:var(--panel2); border:1px solid var(--line); }
  /* "Best use of your boost" — an inline, precomputed shortlist that expands in place on a feed card */
  .best-use { margin:10px 0 2px; border-top:1px solid var(--line); padding-top:8px; }
  .best-use > summary { cursor:pointer; font-size:13px; font-weight:600; color:var(--accent); user-select:none; list-style:none; }
  .best-use > summary::-webkit-details-marker { display:none; }
  .best-use[open] > summary { color:var(--txt); }
  .best-use-h { font-weight:700; font-size:13px; margin:8px 0 6px; letter-spacing:.02em; }
  .best-use-row { padding:7px 0; border-top:1px dashed var(--line); }
  .best-use-row:first-of-type { border-top:none; }
  .best-use-pick { font-size:13px; font-weight:600; line-height:1.35; }
  .best-use-ev { font-size:13px; margin-top:2px; }
  .best-use-ev .n { color:var(--mut); }
  .best-use-ev .b { color:var(--accent); font-weight:700; }
  .best-use-approx { display:inline-block; font-size:10px; font-weight:700; color:var(--mut); background:var(--panel2); border:1px solid var(--line); border-radius:8px; padding:0 6px; margin-left:4px; vertical-align:middle; cursor:help; }
  .best-use-exact { display:inline-block; font-size:10px; font-weight:700; color:var(--accent); background:var(--panel2); border:1px solid var(--line); border-radius:8px; padding:0 6px; margin-left:4px; vertical-align:middle; cursor:help; }
  .best-use-wp { font-size:11px; color:var(--mut); font-weight:600; margin-left:8px; white-space:nowrap; }
  .best-use-why { font-size:12px; color:var(--mut); margin-top:3px; line-height:1.4; }
  .best-use-foot { font-size:11px; color:var(--mut); margin-top:8px; font-style:italic; line-height:1.4; }
  .best-use-empty { font-size:13px; color:var(--mut); margin-top:6px; }
  /* collapsible "More details (optional)" — keeps the fast screenshot path uncluttered */
  .promo-more { margin:12px 0 4px; border-top:1px solid var(--line); padding-top:8px; }
  .promo-more > summary { cursor:pointer; font-size:13px; color:var(--mut); user-select:none; }
  .promo-more[open] > summary { color:var(--txt); font-weight:600; }
  .promo-cta { border-color:var(--accent); }
  .promo-cta-btn { width:100%; font-size:15px; font-weight:700; }
  .promo-cta-why { font-size:13px; margin-top:10px; line-height:1.45; }
  .row-actions { white-space:nowrap; }
  .row-actions button { margin-left:6px; }
  .row-actions button:first-child { margin-left:0; }
  .promo-actions { display:flex; gap:8px; margin-top:10px; }
  .promo-actions button { font-size:12.5px; padding:5px 12px; }
  .btn-approve { background:var(--accent); color:var(--accent-ink); border-color:var(--accent); font-weight:700; }
  .btn-reject { color:var(--bad); border-color:var(--bad); }
  .acct { position:relative; display:inline-block; }
  .avatar { width:32px; height:32px; padding:0; border-radius:50%; background:var(--panel2); color:var(--txt); font-weight:700; font-size:13px; display:inline-flex; align-items:center; justify-content:center; }
  .avatar.admin { box-shadow:0 0 0 2px var(--accent); border-color:var(--accent); }
  .acct-menu { position:absolute; right:0; top:44px; width:min(260px,86vw); background:var(--panel); border:1px solid var(--line); border-radius:12px; box-shadow:0 8px 28px rgba(0,0,0,.5); z-index:40; padding:6px; text-align:left; }
  .acct-id { padding:10px 12px; border-bottom:1px solid var(--line); }
  .acct-id b { font-size:13px; word-break:break-all; }
  .acct-role { color:var(--accent); font-size:11px; text-transform:uppercase; letter-spacing:.6px; margin-top:3px; }
  .acct-item { width:100%; text-align:left; margin-top:6px; background:none; border:none; padding:10px 12px; border-radius:8px; color:var(--txt); }
  .acct-item:hover { background:var(--panel2); }
  .acct-signout { width:100%; text-align:left; margin-top:2px; background:none; border:none; padding:10px 12px; border-radius:8px; }
  .acct-signout:hover { background:var(--panel2); color:var(--bad); }
  /* ---------- HOME (the command center) — built ONLY from the captured brand palette ---------- */
  .home { max-width:1080px; margin:0 auto; padding-bottom:88px; } /* clear the fixed "Ask the Sharp" button */
  .num { font-family:var(--font-num); font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1; } /* terminal-grade figure alignment */
  /* ── SURVIVOR (SP-130) ── two axes, deliberately DIFFERENT SHAPES so a reader cannot merge two
     adjacent pills into one status. Week type = the loud rounded pill; stability = a small square
     tag that names itself. The original pass invented .chip/.good/.warn/.info, none of which exist,
     so DANGER WEEK rendered identically to PLAY IT SAFE — invisible, not alarmist. */
  .sv-week { font-size:11px; font-weight:800; padding:3px 10px; border-radius:999px; border:1px solid var(--line); letter-spacing:1px; text-transform:uppercase; white-space:nowrap; color:var(--mut); }
  .sv-week.safe   { background:var(--accent-dim); color:var(--accent); border-color:color-mix(in srgb,var(--accent) 35%,transparent); }
  .sv-week.danger { background:rgba(210,153,34,.14); color:var(--warn); border-color:var(--warn); } /* --warn amber, NOT --bad: a weak slate is a caution, not a failure */
  .sv-stab { font-size:9.5px; font-weight:800; text-transform:uppercase; letter-spacing:.3px; padding:2px 7px; border-radius:5px; border:1px solid var(--line); color:var(--mut); white-space:nowrap; }
  .sv-stab.fragile { color:var(--warn); border-color:color-mix(in srgb,var(--warn) 45%,transparent); }
  .sv-eyebrow { color:var(--mut); font-size:11px; text-transform:uppercase; letter-spacing:1px; font-weight:700; margin-bottom:8px; }
  .sv-card { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:18px; margin:12px 0; }
  .sv-card.alert { border-color:var(--warn); background:rgba(210,153,34,.07); }
  /* lookahead provenance is STRUCTURAL, not typographic — a bold word inside grey text is not
     "distinguishable at a glance". Dashed rule = not real yet, reusing the .backtest convention. */
  .sv-la { padding:6px 0 6px 12px; border-left:3px solid var(--blue); margin:4px 0; }
  .sv-la.partial { border-left-color:var(--warn); }
  .sv-la.proj { border-left:3px dashed var(--warn); }
  .sv-tag { font-size:9.5px; font-weight:800; text-transform:uppercase; letter-spacing:.4px; padding:1px 6px; border-radius:5px; white-space:nowrap; }
  .sv-tag.market { background:color-mix(in srgb,var(--blue) 14%,transparent); color:var(--blue); }
  .sv-tag.proj   { background:rgba(210,153,34,.14); color:var(--warn); }
  .sv-tag.track  { background:rgba(210,153,34,.14); color:var(--warn); }
  .sv-tag.out    { background:rgba(210,153,34,.14); color:var(--warn); }
  /* a tied GROUP is bracketed and internally unordered — same weight, no ranking cue inside it */
  .sv-tie { border-left:2px solid var(--line); padding-left:8px; }
  .sv-teams td.t { font-weight:700; }
  .sv-teams tr.pick td { background:var(--accent-faint); }
  /* ⚠ THE PAGE MUST NEVER SCROLL SIDEWAYS. At 390px this table forced the body to 430px, and the
     "outside the range we tested" badge — the one caveat a reader needs BEFORE trusting a number —
     was clipped mid-word. Wide content scrolls inside its own box, not by dragging the page.
     David works the Sunday slate from his phone. Found by the UX gate at mobile width. */
  .sv-scroll { overflow-x:auto; -webkit-overflow-scrolling:touch; max-width:100%; }
  .sv-teams { width:100%; border-collapse:collapse; }
  .sv-teams td, .sv-teams th { white-space:nowrap; padding:3px 8px 3px 0; }
  .sv-teams td:last-child, .sv-teams th:last-child { white-space:normal; }
  .home-hero { padding:44px 4px 26px; border-bottom:1px solid var(--line); }
  .home-eyebrow { color:var(--mut); font-size:11px; text-transform:uppercase; letter-spacing:2px; font-weight:700; }
  .home-h1 { font-size:clamp(28px,4vw,40px); font-weight:800; letter-spacing:-.4px; line-height:1.08; margin:12px 0 0; }
  .home-h1 .mkt { color:var(--mut); }
  .home-h1 .mine { color:var(--accent); }
  .home-sub { color:var(--mut); font-size:16px; max-width:660px; margin:14px 0 0; line-height:1.55; }
  .home-idea { color:var(--mut); font-size:13px; line-height:1.55; margin:12px 0 0; max-width:60ch; }
  .home-idea-link { color:var(--accent); font-weight:700; white-space:nowrap; cursor:pointer; display:inline-flex; align-items:center; gap:4px; }
  /* first-run "how this works" banner (localStorage onboardSeen0) */
  .onboard { border:1px solid var(--accent); background:linear-gradient(180deg,var(--accent-dim),transparent); border-radius:12px; padding:14px 16px; margin:0 0 18px; }
  .onboard-h { font-weight:800; font-size:15px; margin-bottom:5px; }
  .onboard-b { color:var(--txt); font-size:13.5px; line-height:1.55; }
  .onboard-a { display:flex; gap:14px; align-items:center; margin-top:11px; flex-wrap:wrap; }
  .onboard-go { color:var(--accent); font-weight:700; font-size:13px; cursor:pointer; display:inline-flex; align-items:center; gap:5px; }
  .onboard-x { font-size:12.5px; padding:6px 12px; }
  .home-pulse { display:inline-flex; align-items:center; gap:8px; margin-top:20px; padding:7px 13px; border:1px solid var(--line);
                border-radius:999px; background:var(--panel); font-size:13px; }
  .home-pulse .dot { width:8px; height:8px; border-radius:50%; box-shadow:0 0 0 0 currentColor; animation:pulse 2.4s infinite; }
  @keyframes pulse { 0%{box-shadow:0 0 0 0 var(--accent-glow)} 70%{box-shadow:0 0 0 7px color-mix(in srgb,var(--accent) 0%,transparent)} 100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--accent) 0%,transparent)} }
  /* ── SP-4 · deadline rail ─────────────────────────────────────────────────────────────────────
     Survivor used to sit at position two in the tab strip because it is a once-a-week, hard-deadline
     decision (SP-3). The grouped nav moves it into a menu, so the promoting job moves HERE: while a
     deadline product is live it gets a row on Home, and the menu is the complete index rather than the
     primary path. Weekly Picks has the same shape and plugs into the same rail. */
  .drail { display:grid; gap:10px; margin:18px 0 4px; }
  .dcard { display:flex; align-items:center; gap:13px; padding:13px 15px; border:1px solid var(--accent);
           border-radius:12px; background:linear-gradient(180deg,var(--accent-dim),transparent); cursor:pointer; }
  .dcard:hover { background:var(--accent-dim); }
  .dcard .d-ic { color:var(--accent); display:inline-flex; flex:0 0 auto; }
  .dcard .d-txt { flex:1 1 auto; min-width:0; }
  .dcard .d-t { font-weight:800; font-size:14.5px; }
  .dcard .d-s { color:var(--mut); font-size:12.5px; margin-top:2px; line-height:1.45; }
  .dcard .d-go { color:var(--accent); font-weight:700; font-size:13px; white-space:nowrap; display:inline-flex; align-items:center; gap:5px; }
  @media (max-width:420px){ .dcard .d-go .d-go-t { display:none; } }
  .tiles { display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin:24px 0; }
  .tile { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:16px 18px; }
  .tile .k { color:var(--mut); font-size:11px; text-transform:uppercase; letter-spacing:1px; font-weight:700; }
  .tile .v { font-family:var(--font-num); font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1; font-size:27px; font-weight:700; margin-top:8px; letter-spacing:0; }
  .tile .s { color:var(--mut); font-size:12.5px; margin-top:3px; }
  .tile .v.edge { color:var(--accent); }
  /* TA-830, 2026-10-22 — .statstrip/.stat removed (David: "I don't really think this entire bar has any
     value to a user"). Left no other caller — confirmed via grep before deleting. If a future surface
     wants this exact tile shape again, it existed here; this isn't a generic pattern worth preserving
     speculatively. */
  @media(max-width:560px){
    /* SP lean home — shrink the mobile hero so Play of the Day sits above the fold (weight stays 800) */
    .home-h1 { font-size:20px; line-height:1.12; }
    .home-hero { padding:24px 4px 16px; }
    .home-sub { font-size:14px; margin-top:10px; }
    /* David's mobile review (2026-10-17, real phone, PR #741) — in the 2x2 grid, a cell whose subtitle
       wrapped to 2-3 lines (Market Data's "each board's own timestamp..." was the worst offender) dragged
       its WHOLE ROW to match that height, making its row-mate (Open Edges/Ripe Now) look oversized with
       empty space while that same row-mate's own short label+value looked "compressed" relative to it —
       his exact description. Capping every stat's subtitle to one line, and trimming the value/padding a
       touch, keeps all 4 cells a uniform, compact height instead of two tall rows pushing Best Angles down.
       Never touches the desktop/tablet grid above 560px. */
    .stat { padding:9px 10px; }
    .stat .k { font-size:9px; }
    .stat .v { font-size:19px; }
    .stat .s { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  }
  .home-sec-h { display:flex; align-items:baseline; justify-content:space-between; margin:30px 0 10px; }
  .home-sec-sub { color:var(--mut); font-size:12.5px; line-height:1.5; margin:-4px 0 10px; }
  .home-sec-h h2 { font-size:15px; font-weight:700; margin:0; }
  .home-sec-h .lnk { color:var(--accent); font-size:13px; font-weight:600; cursor:pointer; background:none; border:none; padding:0; }
  .home-play { display:flex; align-items:center; justify-content:space-between; gap:12px; background:var(--panel); border:1px solid var(--line);
               border-radius:10px; padding:13px 16px; margin:8px 0; }
  .home-play.mine { border-left:3px solid var(--blue); }
  .home-play .p-take { font-weight:700; }
  /* D2 tier ladder (shared voice — see also .potd-prime + .conf-chip): PRIME=gold hero · STRONG=dim green ·
     SOLID=teal outline (TA-734, 2026-10-16 — same fix as .conf-Solid, kept in sync) · LEAN/WATCH=neutral */
  .home-play .p-tierpill { display:inline-block; vertical-align:middle; margin-right:8px; font-size:10px; font-weight:800; letter-spacing:.5px; text-transform:uppercase; padding:3px 8px; border-radius:999px; background:transparent; color:var(--mut); border:1px solid var(--line); white-space:nowrap; }
  .home-play .p-tierpill.t-Prime { background:var(--prime); color:var(--prime-ink); border:0; box-shadow:0 4px 14px -4px var(--prime-glow); }
  .home-play .p-tierpill.t-Strong { background:var(--tier-strong-dim); color:var(--tier-strong); border:1px solid color-mix(in srgb,var(--tier-strong) 45%,transparent); }
  .home-play .p-tierpill.t-Solid { background:transparent; color:var(--tier-strong); border:1px solid color-mix(in srgb,var(--tier-strong) 35%,transparent); }
  .home-play .p-tierpill.t-Lean, .home-play .p-tierpill.t-Watch { background:transparent; color:var(--mut); border:1px solid var(--line); }
  .home-play .p-game { color:var(--mut); font-size:12.5px; margin-top:2px; }
  .home-play .p-edge { text-align:right; white-space:nowrap; }
  .home-play .p-edge .p-pts { color:var(--accent); font-weight:700; font-size:15px; }
  .home-play .p-edge .p-net { color:var(--mut); font-weight:600; font-size:11px; margin-top:3px; }
  .home-play .p-edge .p-net b { color:var(--txt); font-weight:700; }
  .potd-wrap { margin-top:6px; }
  /* SP lean home — collapsed Play-of-the-Day bar (default state for signed-in members; expands in place). Reuses .potd-prime tier pills + @keyframes pulse. */
  .potd-bar { width:100%; display:flex; align-items:center; gap:10px; text-align:left; border:1px solid var(--accent); border-radius:12px; padding:12px 14px; background:linear-gradient(180deg,var(--accent-faint),transparent); cursor:pointer; }
  .potd-bar-txt { flex:1 1 auto; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:14px; }
  .potd-bar-edge { color:var(--accent); font-weight:700; }
  .potd-bar-cta { flex:0 0 auto; color:var(--accent); font-weight:700; font-size:13px; }
  .potd-bar-dot { flex:0 0 auto; width:8px; height:8px; border-radius:50%; background:var(--accent); animation:pulse 2.4s infinite; }
  /* TA-795 follow-up — collapsed POTD bar's opposite-side-bet presence flag. Static (no pulse — this
     isn't a "new" cue), warn-toned to match oppositeSideBetNote()'s own full-card styling. */
  .potd-bar-oppflag { flex:0 0 auto; color:var(--warn); display:flex; align-items:center; }
  .potd-bar-check { flex:0 0 auto; color:var(--accent); font-weight:800; }
  .potd-collapse { flex:0 0 auto; background:none; border:0; color:var(--mut); font-size:21px; line-height:1; cursor:pointer; padding:2px 8px; }
  /* David's mobile review (2026-10-17, real phone, PR #741) — "Angle of the Day truncated so the actual
     selection isn't visible." .potd-bar-txt's single-line nowrap+ellipsis cut the pick's own team/number
     off entirely at phone widths, well before anyone could read it. This is the top-priority content on
     the page (his own words), so the fix widens its budget rather than shrinking it further: drop the
     "tap to open" wording earlier (was 380px, now 480px — covers the common 390-430px phone range) and
     let the pick text wrap up to 2 lines instead of ellipsis-truncating to one. Height stays bounded
     (line-clamp, not unlimited wrap) so the bar can't balloon, but the actual selection is now always
     readable. Desktop/tablet (>480px) is completely unchanged. */
  @media(max-width:480px){
    .potd-bar .cta-txt { display:none; }
    .potd-bar-txt { white-space:normal; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; text-overflow:unset; }
  }
  .potd-reveal { width:100%; padding:16px; border:1px dashed var(--accent); border-radius:14px; background:linear-gradient(180deg,var(--accent-faint),transparent); color:var(--accent); font-weight:700; font-size:15px; cursor:pointer; letter-spacing:.2px; }
  .potd-reveal:hover { background:linear-gradient(180deg,var(--accent-dim),transparent); }
  .potd-card { border:1px solid var(--accent); border-radius:14px; padding:16px 18px; background:linear-gradient(180deg,var(--accent-faint),transparent); }
  .potd-card.none { border-color:var(--line); color:var(--mut); background:var(--panel); font-size:14px; line-height:1.6; }
  .potd-tag { display:inline-block; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.6px; color:var(--accent); margin-bottom:6px; }
  .potd-tag.thin { color:var(--warn); }
  .potd-take { font-size:19px; font-weight:800; line-height:1.25; }
  .potd-game { color:var(--mut); font-size:13px; margin-top:3px; }
  .potd-edge { color:var(--accent); font-weight:700; font-size:15px; margin-top:8px; }
  .potd-why { font-size:12.5px; line-height:1.6; margin-top:8px; }
  /* SP-30 conviction card — centered marquee, prominent unlock reveal, gap strip, book rows. Design+UX gated. */
  .potd-h { flex-direction:column; align-items:center; text-align:center; gap:2px; margin:34px 0 12px; }
  .potd-h h2 { justify-content:center; }
  .potd-h .potd-sub { color:var(--mut); font-size:12.5px; }
  .potd-reveal { position:relative; overflow:hidden; padding:30px 22px; border:1.5px dashed var(--accent); border-radius:16px; text-align:center; transition:transform .15s ease, border-color .15s ease, box-shadow .15s ease; }
  .potd-reveal::before { content:""; position:absolute; inset:-45% -12%; z-index:0; pointer-events:none;
    background:radial-gradient(closest-side, var(--accent-glow), transparent 70%); opacity:.5; animation:potdHalo 3.2s ease-in-out infinite; }
  .potd-reveal:hover { transform:translateY(-2px); border-color:var(--accent-bright); box-shadow:0 16px 44px -22px var(--accent-glow); }
  .potd-reveal:active { transform:translateY(0); }
  .potd-reveal > * { position:relative; z-index:1; }
  .pr-badge { width:44px; height:44px; border-radius:50%; margin:0 auto 12px; display:flex; align-items:center; justify-content:center; background:var(--accent); color:var(--accent-ink); box-shadow:0 6px 18px -6px var(--accent-glow); }
  .pr-cta { font-size:19px; font-weight:800; color:var(--accent-bright); letter-spacing:.2px; }
  .pr-chev { display:inline-block; animation:potdNudge 1.6s ease-in-out infinite; }
  .pr-sub { color:var(--mut); font-size:12.5px; font-weight:400; margin-top:7px; }
  @keyframes potdHalo { 0%,100%{ opacity:.32; } 50%{ opacity:.6; } }
  @keyframes potdNudge { 0%,100%{ transform:translateX(0); } 50%{ transform:translateX(4px); } }
  @media (prefers-reduced-motion:reduce){ .potd-reveal::before{ animation:none; } .pr-chev{ animation:none; } .potd-bar-dot{ animation:none; } }
  .potd-tagrow { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:6px; }
  .potd-tagrow .potd-tag { margin-bottom:0; }
  /* D2 tier ladder (shared voice): PRIME=gold hero · STRONG=dim green · SOLID=teal outline (TA-734,
     2026-10-16 — same fix as .conf-Solid, kept in sync) · LEAN/WATCH=neutral */
  .potd-prime { flex:0 0 auto; font-size:11px; font-weight:800; letter-spacing:.6px; text-transform:uppercase; padding:4px 10px; border-radius:999px; background:transparent; color:var(--mut); border:1px solid var(--line); white-space:nowrap; }
  .potd-prime.t-Prime { background:var(--prime); color:var(--prime-ink); border:0; box-shadow:0 4px 14px -4px var(--prime-glow); }
  .potd-prime.t-Strong { background:var(--tier-strong-dim); color:var(--tier-strong); border:1px solid color-mix(in srgb,var(--tier-strong) 45%,transparent); }
  .potd-prime.t-Solid { background:transparent; color:var(--tier-strong); border:1px solid color-mix(in srgb,var(--tier-strong) 35%,transparent); }
  .potd-prime.t-Lean, .potd-prime.t-Watch { background:transparent; color:var(--mut); border:1px solid var(--line); }
  .pgap { margin:13px 0 4px; background:var(--panel2); border:1px solid var(--line); border-radius:12px; padding:12px 14px 14px; }
  .pgap-row { display:flex; align-items:flex-end; justify-content:space-between; gap:6px; }
  .pgap-cell { text-align:center; flex:1; }
  .pgap-cell .k { font-size:10px; text-transform:uppercase; letter-spacing:.4px; color:var(--mut); font-weight:700; }
  .pgap-cell .v { font-family:var(--font-num); font-size:21px; font-weight:700; margin-top:2px; font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1; letter-spacing:.3px; }
  .pgap-cell.fair .v, .pgap-cell.edge .v { color:var(--accent); }
  .pgap-cell.edge .v small { font-size:11px; font-weight:600; letter-spacing:0; }
  .pgap-vs { color:var(--mut); font-size:13px; font-weight:700; padding-bottom:3px; }
  .pgap-bar { height:7px; border-radius:6px; margin-top:11px; position:relative; background:var(--line); overflow:hidden; }
  .pgap-bar .fill { position:absolute; left:0; top:0; bottom:0; background:linear-gradient(90deg,var(--accent-dim),var(--accent)); }
  .pgap-cap { display:flex; justify-content:space-between; font-size:10.5px; color:var(--mut); margin-top:6px; }
  .pmove { display:flex; align-items:flex-start; gap:8px; margin:11px 0 2px; padding:9px 12px; background:var(--accent-dim); border:1px solid color-mix(in srgb,var(--accent) 30%,transparent); border-radius:10px; font-size:12.5px; line-height:1.45; }
  .pmove .ic { color:var(--accent); flex:0 0 auto; margin-top:1px; }
  /* opposite-side bet reconciliation note (oppositeSideBetNote()) — same box shape as .pmove, warn-toned
     instead of accent-toned since this is "pay attention," not routine market context. */
  .ba-opp-note { display:flex; align-items:flex-start; gap:8px; margin:8px 0; padding:9px 12px; background:color-mix(in srgb,var(--warn) 10%,transparent); border:1px solid color-mix(in srgb,var(--warn) 35%,transparent); border-radius:10px; font-size:12.5px; line-height:1.45; }
  .ba-opp-note .ic { color:var(--warn); flex:0 0 auto; margin-top:1px; }
  /* TA-341 final UX piece — .pbooks/.pbook/.pbooks-more (the old separate Play of the Day book list)
     removed: every row it used to render now lives inside the consolidated .exec module. */
  .potd-acts { display:flex; align-items:center; gap:12px; margin-top:13px; padding-top:12px; border-top:1px solid var(--line); flex-wrap:wrap; }
  .potd-acts .betwrap { margin-left:auto; }
  .books-nudge { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin:14px 0 4px; padding:11px 14px; border:1px solid var(--blue); border-radius:12px; background:linear-gradient(180deg,rgba(90,170,255,.08),transparent); font-size:13.5px; }
  .books-nudge .bn-set { color:var(--blue); font-weight:700; cursor:pointer; text-decoration:underline; }
  .books-nudge .bn-x { color:var(--mut); cursor:pointer; margin-left:auto; font-size:12px; }
  .steps { display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin:12px 0; }
  .step { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:16px; }
  .step .n { color:var(--accent); font-weight:800; font-size:13px; letter-spacing:1px; }
  .step .t { font-weight:700; margin:8px 0 4px; }
  .step .d { color:var(--mut); font-size:13px; line-height:1.5; }
  .home-foot { border-top:1px solid var(--line); margin-top:30px; padding:20px 4px 40px; color:var(--mut); font-size:13px; line-height:1.7; }
  .home-foot b { color:var(--txt); font-weight:600; }
  @media(max-width:720px){ .tiles,.steps{ grid-template-columns:repeat(2,1fr);}
    /* SP-3: give the table room to be legible and let it scroll inside its card. Squeezing four columns
       into 354px is what produced the four-line "No / pro / Co / yet" sliver. */
    /* The "Ask the Sharp" bubble is position:fixed at bottom-right, so on a phone it sits over whatever
       happens to be under it — measured over live table cells and section headings. It is pre-existing
       chrome and moving it is a global decision, but nothing on a page we are shipping should be
       permanently unreachable: extra bottom room means every element can be scrolled out from under it. */
    .wrap { padding-bottom:168px; }
    .tscroll > table { min-width:520px; }
    .tscroll > table.gridtbl { min-width:566px; }                        /* 92 + three 158px data columns */
    .gridtbl th:first-child, .gridtbl td:first-child { width:92px; }
    /* ⚠ FREEZE THE CONVICTION COLUMN. Scrolled right, the reader saw "-8.4% · 16 settled", "-2.4% · 40
       settled", "+25.7% · 35 settled" with NO Prime/Strong/Solid label on screen at all — the ladder
       disappeared at exactly the moment the other bet types became visible. Even a reader who notices the
       new scroll affordance would get unlabelled numbers. The label travels with the row. */
    .gridtbl th:first-child, .gridtbl td:first-child {
      position:sticky; left:0; background:var(--panel);
      box-shadow:1px 0 0 var(--line);
      /* ⚠ ABOVE the edge fade (z-index 2), not below it. At z-index 1 the left gradient painted straight
         over the frozen column and clipped it to "onviction / rime / trong / olid" — the two fixes for B2
         and B3 fighting each other and producing something that looked more broken than either bug.
         Caught by LOOKING at the render: the automated check compared bounding boxes, and a box inside the
         card reads as "visible" while an overlay sits on top of it. Geometry is not legibility. */
      z-index:3;
    }
    /* And the left chevron starts where the frozen column ends, so it marks the scrolled region it actually
       describes instead of covering the one part of the row that never scrolls. */
    .tscroll-wrap.frozen::before { left:92px; }
    /* ⚠ THE SLIVER AT FULL RIGHT SCROLL IS DEFERRED, NOT FORGOTTEN.
       I tried `scroll-snap-align: start` and it did not help: at maximum scroll the scroller is already at
       its limit, so there is no further START edge for it to land on.
       ⚠ CORRECTION to an earlier note of mine, which overstated this: `scroll-snap-align: end`, or a
       trailing `scroll-padding-inline-end`, WOULD make the final position land cleanly. Snap is not
       incapable here — I only proved the one variant I tried does not work. The reason to defer is the
       arithmetic below, which is a layout decision rather than a scrolling one:
         viewport 390 → card 354 → scroller 308, of which the frozen label column holds 92
         → 216px of room beside it, against a 158px data column
         → one full column and a 58px remainder (measured at full scroll: the middle column is 158px wide
           with 100px of it behind the frozen column). 216 is not a multiple of 158, so a partial column at
           the extreme is unavoidable at these widths.
       The only clean fixes are to make the data column 216px (one bet type on screen at a time) or 108px
       (two, but too narrow for "+16.1% · 15 settled" on one line). Both are real layout decisions about
       what a phone reader should see at once, which belongs to the Design Lead rather than to a CSS tweak
       here. Clean at rest and mid-scroll; the frozen label stays legible throughout. */
    /* 158px of column at 15.5px holds "+16.1% · 15 bets" on ONE line. It was wrapping to two, and at the
       pre-fix widths to three, which is what made a confident figure read as a broken one. */
    .gridtbl .cellhead { font-size:15.5px; }
    .tabs{ -webkit-mask-image:linear-gradient(to right,#000 92%,transparent); mask-image:linear-gradient(to right,#000 92%,transparent);} /* hint there are more tabs to scroll */
    /* iOS auto-zooms a focused input whose font is <16px (magnifies the page, cuts off the input + chips) — pin to 16px on mobile so it never zooms */
    .sharp-input textarea, .modal input, .modal textarea, .modal select, .tag-input .tag-add,
    /* SP-223: the auth-family cards (invite / sign in / forced password change) are the most
       phone-heavy screens in the product - an invite link arrives by text - and they were NOT
       covered by the rule above, so every one of them zoomed on focus. */
    .invite-card input, .invite-card select, .card input[type="email"], .card input[type="password"], #code { font-size:16px; } }
  /* SP-312: Weekly Picks framed in-app. Starts at a sane guess so there is no zero-height flash before
     the embedded page's own script reports its real height; grows or shrinks from there. No border/
     shadow — it should read as part of this page, not as a window onto another one. */
  .wp-embed { width:100%; min-height:600px; height:600px; border:0; display:block; background:transparent; transition:height .12s ease-out; }
  /* TA-712 Game Lines <-> Game Totals integration — same embed pattern as .wp-embed just above
     (core/lib/game-totals-render.mjs's own EMBED_SCRIPT reports real height via postMessage). */
  .gt-embed { width:100%; min-height:600px; height:600px; border:0; display:block; background:transparent; transition:height .12s ease-out; }
