/* live.css — live dashboard chrome, lifted out of the per-page
   <style> block in templates/live.html (quality audit area 3).
   Loaded via {% block head_styles %} after tailwind so it keeps
   winning specificity ties. No Jinja/url() — pure CSS. */

main { min-height: 0 !important; }

/* ── Demo LIVE badge (public showcase subnav) ───────────────────────────
   The demo now runs at a fixed per-demo speed baked server-side, so the old
   play/pause/speed/scrub transport is gone. A small white-on-emerald LIVE
   badge with a pulsing mint beacon stands in its place on the sub-nav bar. */
.ls-demo-livebadge {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 11px 5px 9px; border-radius: 999px;
  font-size: var(--ls-fs-10); font-weight: 800; letter-spacing: 0.12em; color: #fff;
  background: rgba(255,255,255,0.10);
  border: 1px solid rgba(255,255,255,0.18);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.16);
}
/* Broadcast beacon — mint dot, pulses. */
.ls-demo-dot { width: 7px; height: 7px; border-radius: 999px; background: rgba(255,255,255,0.45); flex-shrink: 0; }
.ls-demo-dot.is-live { background: #a7f3d0; box-shadow: 0 0 8px #6ee7b7; animation: ls-demo-pulse 1.6s ease-out infinite; }
@keyframes ls-demo-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(167,243,208,0.7); }
  70%  { box-shadow: 0 0 0 6px rgba(167,243,208,0); }
  100% { box-shadow: 0 0 0 0 rgba(167,243,208,0); }
}
@media (prefers-reduced-motion: reduce) { .ls-demo-dot.is-live { animation: none; } }

/* Demo picker trigger — the mint beacon sits between name and chevron. */
.ls-demo-pill .ls-demo-dot.is-live { background: #34d399; box-shadow: 0 0 7px #34d399; }

/* Demo picker dropdown rows — sport icon + name/tagline + LIVE tag on the
   current demo. Mirrors the saved-slate rows but lighter (one line each). */
.ls-demo-row {
  display: flex; align-items: center; gap: 11px;
  padding: 10px 12px; text-decoration: none; cursor: pointer;
  border-radius: 10px; margin: 1px 4px; transition: background 120ms ease;
}
.ls-demo-row:hover { background: var(--ls-hover, rgba(0,0,0,0.04)); }
.dark .ls-demo-row:hover { background: rgba(255,255,255,0.05); }
.ls-demo-row.is-active { background: rgb(var(--ls-accent-rgb) / 0.10); }
.ls-demo-row-icon { width: 26px; height: 26px; flex: none; display: grid; place-items: center; }
.ls-demo-row-icon img { width: 24px; height: 24px; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.18)); }
.ls-demo-row-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.ls-demo-row-name { font-size: 13px; font-weight: 700; color: var(--ls-ink); line-height: 1.2; }
.ls-demo-row-tag { font-size: 11px; color: var(--ls-ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ls-demo-row-live {
  display: inline-flex; align-items: center; gap: 5px; flex: none;
  font-size: 9px; font-weight: 800; letter-spacing: 0.1em; color: var(--ls-accent-deep);
}
.dark .ls-demo-row-live { color: var(--ls-accent-hi); }
/* Desktop dashboard is fit-to-viewport by design (grid + rail size
   themselves to the viewport bottom), so the page-level scrollbar has
   no job here — and sub-pixel rounding under UI-scale zoom kept
   resurrecting a 1px scroll. Clip scrolling at the root and reclaim
   the reserved gutter. Tablet (<1400px) and phone keep a scrolling
   page by design, so the global html{overflow-y:scroll} stays there. */
@media (min-width: 1400px) {
  html { overflow-y: clip; scrollbar-gutter: auto; }
}

/* Phase A: only the v2-specific styles. The .ls-* widget chrome lives in
   static/style.css so the cachebust on base.html covers it.
   These rules are page-local and won't bleed onto legacy /live. */

/* ── Loading screen — branded emerald beacon + rotating status lines ──── */
.ls-loadscreen {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 20px; min-height: 55vh; padding: 60px 20px;
}
/* Static boot skeleton (#ls-boot) — server-rendered, shows on first byte, then
   the app root's x-init stamps html.ls-booted and this hands off to the Alpine
   loadscreen/board (pixel-identical, so the swap is invisible). */
html.ls-booted #ls-boot { display: none; }
.ls-loadscreen-beacon {
  position: relative; width: 13px; height: 13px; border-radius: 999px;
  background: var(--ls-accent); box-shadow: 0 0 14px rgb(var(--ls-accent-rgb) / 0.55);
}
/* Two concentric rings expanding out — the LiveSlates broadcast-beacon rhythm. */
.ls-loadscreen-beacon::before,
.ls-loadscreen-beacon::after {
  content: ''; position: absolute; inset: -1px; border-radius: 999px;
  border: 2px solid rgb(var(--ls-accent-rgb) / 0.5);
  animation: ls-beacon-ring 1.9s cubic-bezier(0,0.2,0.8,1) infinite;
}
.ls-loadscreen-beacon::after { animation-delay: 0.95s; }
@keyframes ls-beacon-ring {
  0%   { transform: scale(1);   opacity: 0.7; }
  100% { transform: scale(5.5); opacity: 0; }
}
.ls-loadscreen-msg {
  font-size: var(--ls-fs-14); font-weight: 600; letter-spacing: 0.01em;
  color: #475569; min-height: 20px; text-align: center;
  animation: ls-load-fade 1.4s ease-in-out infinite;
}
.dark .ls-loadscreen-msg { color: #94a3b8; }
/* Gentle breathe so each line lands softly rather than snapping. */
@keyframes ls-load-fade {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 1; }
}
.ls-loadscreen-dots { display: flex; gap: 5px; }
.ls-loadscreen-dots span {
  width: 5px; height: 5px; border-radius: 999px; background: var(--ls-accent);
  animation: ls-load-dot 1.2s ease-in-out infinite;
}
.ls-loadscreen-dots span:nth-child(2) { animation-delay: 0.15s; }
.ls-loadscreen-dots span:nth-child(3) { animation-delay: 0.30s; }
@keyframes ls-load-dot {
  0%, 80%, 100% { opacity: 0.3; transform: translateY(0); }
  40%           { opacity: 1;   transform: translateY(-4px); }
}

/* Transition overlay (slate load + Simple/Analytical switch). Frosted mask over
   the dash body with the same beacon as the cold-load screen; the content beneath
   renders at opacity 0 (.ls-dash-switching) so it reveals fully-formed = "BAM". */
.ls-dash-body { position: relative; }
.ls-switch-overlay {
  position: absolute; inset: 0; z-index: 30;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 16px;
  background: rgba(222,229,231,0.78);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  border-radius: 12px;
}
.dark .ls-switch-overlay { background: rgba(10,15,30,0.78); }

/* Spectator warm-up banner — slim emerald strip shown while a watched slate's
   player stats are still loading on the next live poll (~25s). Radar ping reuses
   the cold-load beacon so it reads as "tuning in", not "stuck". */
.ls-spectator-warming {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 14px;
  border: 1px solid rgb(var(--ls-accent-rgb) / 0.35);
  background: linear-gradient(90deg, rgb(var(--ls-accent-rgb) / 0.12), rgb(var(--ls-accent-rgb) / 0.03));
  border-radius: 10px;
  font-size: var(--ls-fs-13); line-height: 1.35; color: var(--ls-accent-deep);
}
.dark .ls-spectator-warming {
  color: var(--ls-accent-soft);
  border-color: rgb(var(--ls-accent-rgb) / 0.30);
  background: linear-gradient(90deg, rgb(var(--ls-accent-rgb) / 0.16), rgb(var(--ls-accent-rgb) / 0.04));
}
.ls-spectator-warming strong { font-weight: 700; }
.ls-spectator-warming-radar {
  position: relative; flex: none;
  width: 9px; height: 9px; border-radius: 999px; background: var(--ls-accent);
}
.ls-spectator-warming-radar::after {
  content: ""; position: absolute; inset: 0; border-radius: 999px;
  border: 1.5px solid var(--ls-accent);
  animation: ls-beacon-ring 1.8s ease-out infinite;
}
/* ── Headline Zone ────────────────────────────────────────────────────
   Desktop: scoreboard cards on the left flex-fill, personal strip
   pinned 280px on the right. Mobile: stack vertically, scoreboard
   becomes a horizontal scroll-snap track (one game card per page),
   personal strip wraps below with the big number on its own row. */
/* ── Scoreboard frame (Row 1 chrome) ───────────────────────────────
   Themed container with title bar so the scoreboard zone has identity
   and can never silently collapse during data swaps. Stadium-y vibe:
   slate-dark title bar, emerald accents, subtle gradient. */
.ls-scoreboard-frame {
  position: relative;
  margin-top: 14px;                    /* room for the title that sits ON the top border */
  margin-bottom: 0.5rem;
  /* Brand-coloured frame — emerald tint + darker emerald border so
     the scoreboard reads as a flagship element on the page rather
     than blending in. Outer shadow adds gentle lift. Xav 2026-05-27. */
  background:
    linear-gradient(180deg, rgb(var(--ls-accent-rgb) / 0.10) 0%, rgb(var(--ls-accent-rgb) / 0.04) 100%);
  border: 1.5px solid rgb(var(--ls-accent-rgb) / 0.42);
  border-radius: 14px;
  box-shadow: 0 6px 24px -12px rgb(var(--ls-accent-rgb) / 0.30);
  padding: 16px 12px 10px;             /* extra top padding so cards don't sit under the title */
  /* min-height tuned to pre-game card content: floating status pill +
     team mid row + pitcher footer ≈ 150px. Live cards (which add the
     innings grid + pitching/at-bat footer) push the frame above this
     floor naturally, so the value only controls how much vertical
     space pre-game cards consume. Was 200px — wasted ~40px on every
     pre-game slate. Xav 2026-05-30. */
  min-height: 150px;
  display: flex;
  flex-direction: column;
  /* overflow: visible so the title can break the top border —
     watermark still sits inside since its position math keeps it
     within the frame's bounds anyway. */
}
.dark .ls-scoreboard-frame {
  background:
    linear-gradient(180deg, rgb(var(--ls-accent-rgb) / 0.18) 0%, rgb(var(--ls-accent-rgb) / 0.07) 100%);
  border-color: rgb(var(--ls-accent-hi-rgb) / 0.45);
  box-shadow: 0 6px 24px -12px rgb(var(--ls-accent-rgb) / 0.35);
}

/* ══ Shared scoreboard SHELL (Xav 2026-07-04) ═══════════════════════════
   Every bespoke per-sport scorecard (MLB ballpark, NBA LED panel, NFL
   field, …) uses `.ls-sb` on its card CONTAINER and `.ls-sb-card` on each
   card. The shell owns LAYOUT ONLY — flex, per-column width, Full/Compact
   width, and full-width mobile stacking. It deliberately sets NO height:
   heights are content-driven per sport (a golf leaderboard is one tall
   card; an NBA card is short; an MLB stack varies). Sports author only
   their card SURFACE + INTERNALS + what they drop in Compact.

   Defaults below are MLB's proven values (MLB is the reference the other
   scorecards inherit). --cols is set inline on the container by each
   sport's _perRow getter; container-name is unified to `sb-card` so every
   sport's @container rules key off the same name. */
.ls-sb {
  display: flex;
  flex-wrap: wrap;                 /* multi-row when many cards */
  gap: 8px;
  padding: 4px;
  height: auto;                    /* NO fixed height — content decides */
  max-height: 380px;               /* desktop envelope; scroll past it */
  align-content: flex-start;
  justify-content: safe center;
  overflow-x: hidden;
  overflow-y: auto;
  scrollbar-width: none;
}
.ls-sb::-webkit-scrollbar { display: none; }

.ls-sb-card {
  flex: 0 0 auto;
  box-sizing: border-box;
  /* each card = 1/cols of the row minus the inter-card gaps */
  width: calc((100% - (var(--cols, 5) - 1) * 8px) / var(--cols, 5));
  min-width: 250px;                /* readability floor on desktop */
  max-width: 480px;                /* 1 card doesn't blow up on ultrawide */
  container-type: inline-size;     /* internals adapt to the card's OWN width */
  container-name: sb-card;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 12px 12px 8px;
  margin-top: 12px;                /* room for the floating status pill */
  position: relative;
}

/* Compact density — tighter footprint so more cards fit per row. Width
   only; each sport decides what CONTENT to drop in compact. */
.ls-sb--compact .ls-sb-card {
  min-width: 180px;
  max-width: 360px;
  padding: 10px 10px 6px;
  margin-top: 10px;
  gap: 2px;
}

/* Phone: every card is a full-width hero stacked top-to-bottom, in EVERY
   density mode (compact on a phone only means "show less inside", never
   "shrink the card" — that was the abbreviation-collision bug). Height
   stays content-driven; the frame grows and the page scrolls. */
@media (max-width: 767px) {
  .ls-sb,
  .ls-sb--compact {
    flex-direction: column;
    flex-wrap: nowrap;
    gap: 8px;
    padding: 2px;
    max-height: none;
    overflow: visible;
  }
  .ls-sb .ls-sb-card,
  .ls-sb--compact .ls-sb-card {
    flex: 1 1 auto;
    width: 100%;
    min-width: 0;
    max-width: 100%;
    padding: 12px 12px 8px;
    gap: 4px;
  }
}

/* ── Scoreboard BOTTOM band (Xav 2026-06-14) ────────────────────────
   column-reverse drops the full-width scoreboard below the main column.
   Works at every width (it's a band, not a rail); the frame's own 14px
   margin-top supplies the gap above it. */
.ls-dash-body.is-band-bottom {
  display: flex;
  flex-direction: column-reverse;
}

/* ── Scoreboard LEFT RAIL (Xav 2026-06-12) ──────────────────────────
   .ls-dash-body.is-rail turns Row 1 into a fixed-width vertical rail:
   score cards stack one-per-row and scroll inside the rail; the main
   column (KPI strip + grid) takes the remaining width, so every panel
   compresses automatically. Only ≥1024px — below that the flex never
   activates and children stack exactly like the legacy top band.
   RIGHT rail (is-rail--right) is the identical rail mirrored to the
   right edge via row-reverse — it keeps .is-rail so all rules below
   apply unchanged. (Xav 2026-06-14) */
@media (min-width: 1024px) {
  .ls-dash-body.is-rail {
    display: flex;
    gap: 14px;
    align-items: stretch;
    min-width: 0;
  }
  .ls-dash-body.is-rail.is-rail--right {
    flex-direction: row-reverse;
  }
  .ls-dash-body.is-rail > .ls-scoreboard-frame {
    flex: 0 0 326px;
    width: 326px;
    margin-bottom: 0;
    align-self: flex-start;
    position: sticky;
    top: 8px;
    /* Full page height (Xav 2026-06-12): nothing ever renders under
       the rail, so it owns the viewport bottom. 128px ≈ nav (56) +
       sub-nav strip + main padding + the 14px title headroom. NO
       overflow:hidden here — it decapitated the floating SCOREBOARD
       title that sits astride the top border; the card stack scrolls
       inside .ls-headline-scoreboard instead. */
    height: calc(100vh - 128px);
    max-height: none;
  }
  /* Floating title + counts shrink to rail proportions — 18px +
     0.34em tracking overflowed the 326px column. */
  .ls-dash-body.is-rail .ls-scoreboard-title {
    font-size: var(--ls-fs-13);
    letter-spacing: 0.20em;
    padding: 2px 10px;
  }
  .ls-dash-body.is-rail > .ls-dash-main {
    flex: 1;
    min-width: 0;
  }
  /* Vertical card stack + rail-internal scroll. */
  .ls-dash-body.is-rail .ls-headline {
    align-items: stretch;
    min-height: 0;
  }
  .ls-dash-body.is-rail .ls-headline-scoreboard {
    overflow-y: auto;
    scrollbar-width: none;
    min-height: 0;
    max-height: 100%;
  }
  .ls-dash-body.is-rail .ls-headline-scoreboard::-webkit-scrollbar { display: none; }
  /* Every bespoke scoreboard: full rail width, one per row, no band height
     cap — the rail's own overflow scrolls instead. Shell-level (.ls-sb) so
     MLB / NBA / NFL / … all inherit it. Was MLB-only, which left the base
     .ls-sb max-height:380px capping the taller NFL cards → the 2nd card
     truncated even though the rail had room. */
  .ls-dash-body.is-rail .ls-sb {
    max-height: none;
    overflow: visible;
    align-content: flex-start;
  }
  .ls-dash-body.is-rail .ls-sb-card {
    width: 100%;
    min-width: 0;
    max-width: none;
  }
  /* Wildcard scoreboard cards: same one-per-row treatment. */
  .ls-dash-body.is-rail .bgc-row .bgc {
    width: 100%;
    min-width: 0;
    max-width: none;
  }
  /* Sport watermark, rail edition (Xav 2026-06-14). The two 180px
     side-by-side emojis overflowed the 326px column, so rather than
     drop the graphic entirely we keep ONE glyph: centred, anchored to
     the BOTTOM of the rail (fills the empty space below the cards when
     a slate has few games), smaller and faint so the cards always win.
     Drop the mirrored twin. */
  .ls-dash-body.is-rail > .ls-scoreboard-frame::after { display: none; }
  .ls-dash-body.is-rail > .ls-scoreboard-frame::before {
    font-size: 150px;
    top: auto;
    bottom: 18px;
    right: auto;
    left: 50%;
    transform: translateX(-50%);
    opacity: 0.12;
  }
  .dark .ls-dash-body.is-rail > .ls-scoreboard-frame::before { opacity: 0.14; }
  /* Skeleton placeholders stack too. */
  .ls-dash-body.is-rail .ls-scoreboard-skeleton { flex-direction: column; }

  /* ── Main-column frame (Xav 2026-06-12) ──────────────────────────
     Twin of the scoreboard frame: same emerald-tint gradient, border,
     radius and lift, wrapping everything that isn't the scoreboard
     (section tabs + KPI strip + panel grid). The dashboard reads as
     two framed zones side by side instead of panels floating loose on
     the canvas. margin-top mirrors the scoreboard frame's 14px title
     headroom so the two frames' top edges align in rail mode. */
  .ls-dash-body > .ls-dash-main {
    position: relative;
    margin-top: 14px;
    background: linear-gradient(180deg, rgb(var(--ls-accent-rgb) / 0.10) 0%, rgb(var(--ls-accent-rgb) / 0.04) 100%);
    border: 1.5px solid rgb(var(--ls-accent-rgb) / 0.42);
    border-radius: 14px;
    box-shadow: 0 6px 24px -12px rgb(var(--ls-accent-rgb) / 0.30);
    padding: 8px 12px 10px;
  }
  .dark .ls-dash-body > .ls-dash-main {
    background: linear-gradient(180deg, rgb(var(--ls-accent-rgb) / 0.18) 0%, rgb(var(--ls-accent-rgb) / 0.07) 100%);
    border-color: rgb(var(--ls-accent-hi-rgb) / 0.45);
    box-shadow: 0 6px 24px -12px rgb(var(--ls-accent-rgb) / 0.35);
  }
}

/* Floating "SCOREBOARD" title — sits centered on the frame's top
   border line. Border passes BEHIND, title's solid bg masks the
   border underneath so it looks like ────[ SCOREBOARD ]────.
   Bebas Neue (tall narrow capitals, sports-poster aesthetic) loads
   from Google Fonts above; falls back to Oswald / Impact if blocked.
   The title's bg must match the PAGE bg (slate-100 light /
   slate-950 dark) — not the frame bg — because that's the colour
   that needs to mask the border line cutting through. */
.ls-scoreboard-frame .ls-scoreboard-titlebar {
  display: contents;                   /* dissolve the row wrapper so children position freely */
}
.ls-scoreboard-frame .ls-scoreboard-title-icon {
  display: none;                       /* watermark glyph already serves this role; redundant here */
}
.ls-scoreboard-frame .ls-scoreboard-title {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  padding: 2px 16px;
  font-family: 'Bebas Neue', 'Oswald', 'Impact', 'Arial Narrow', sans-serif;
  font-size: var(--ls-fs-18);
  font-weight: 400;                    /* Bebas Neue is already heavy; 400 reads as a deliberate display weight */
  letter-spacing: 0.34em;
  line-height: 1;
  color: var(--ls-accent-deep);                      /* emerald-700, matches the border */
  background: var(--ls-canvas);        /* .ls-canvas tone at frame height — masks the border line (single-sourced) */
  text-transform: uppercase;
  white-space: nowrap;
}
.dark .ls-scoreboard-frame .ls-scoreboard-title {
  color: var(--ls-accent-soft);                      /* emerald-300 */
  background: var(--ls-canvas-dark);   /* matches dark .ls-canvas */
}

/* Counts pill (LIVE / SOON / FINAL) move to top-right corner now
   that the title is floating elsewhere. Shifted further left
   (2026-06-01) to leave room for the new ls-scoreboard-cog button
   that anchors the corner. */
.ls-scoreboard-frame .ls-scoreboard-counts {
  position: absolute;
  top: 6px;
  right: 40px;
  z-index: 2;
}
/* Cog anchors the top-right corner of the frame. titlebar uses
   `display: contents` so the cog button — like the counts pill —
   needs its own absolute positioning to live on the frame border
   layer, not the static flow layer (which dumps it below the
   pill — Xav 2026-06-01). */
.ls-scoreboard-frame .ls-scoreboard-cog {
  position: absolute;
  top: 4px;
  right: 8px;
  z-index: 3;
}

/* MLB stitch pattern tried 2026-05-27 — looked like a prison wall.
   Reverted. The frame keeps the plain gradient + emoji watermark.
   If we revisit, the pattern needs: much larger tile (~200-300px),
   way lower opacity (~0.10 stroke), and a simpler glyph (single
   curve, no X-stitches). Or just abandon the texture idea entirely. */

/* Sport watermark — twin oversized faded emojis anchored to the LEFT
   and RIGHT sides of the frame, behind the cards. Becomes visible
   when the row is sparse; covered when cards fill the width. Gives
   the frame visual mass at low game counts without competing with
   content. The glyph is set inline via data-sport-glyph (defaults
   to ⚾ for MLB). The left side is horizontally mirrored via
   scaleX(-1) so the two watermarks read as a balanced PAIR rather
   than identical clones — subtle but feels intentional. */
.ls-scoreboard-frame::before,
.ls-scoreboard-frame::after {
  content: attr(data-sport-glyph);
  position: absolute;
  top: 50%;
  font-size: 180px;
  line-height: 1;
  opacity: 0.06;
  pointer-events: none;
  user-select: none;
  z-index: 0;
  filter: grayscale(0.4);
}
.ls-scoreboard-frame::before {
  right: 24px;
  transform: translateY(-50%);
}
.ls-scoreboard-frame::after {
  left: 24px;
  transform: translateY(-50%) scaleX(-1);   /* mirror so the two read as a pair */
}
.dark .ls-scoreboard-frame::before,
.dark .ls-scoreboard-frame::after { opacity: 0.08; }
/* Frame children layer above the watermark. */
.ls-scoreboard-frame > * { position: relative; z-index: 1; }
/* Now that .ls-scoreboard-frame is transparent (cards float as
   discrete cards), the titlebar reads as a slim section label, not
   a banner. Light grey eyebrow text + small icon chip + count
   pills pinned right. No background fill. */
.ls-scoreboard-titlebar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 4px 6px;
  background: transparent;
  color: #475569;
  font-size: var(--ls-fs-10);
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.dark .ls-scoreboard-titlebar { color: #94a3b8; }
.ls-scoreboard-title-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px; height: 22px;
  border-radius: 6px;
  background: rgb(var(--ls-accent-rgb) / 0.10);
  font-size: var(--ls-fs-13);
  line-height: 1;
}
.dark .ls-scoreboard-title-icon { background: rgb(var(--ls-accent-hi-rgb) / 0.14); }
.ls-scoreboard-title {
  font-weight: 800;
  letter-spacing: 0.22em;
  color: var(--ls-ink);
}
.ls-scoreboard-counts {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: var(--ls-fs-9);
}
.ls-scoreboard-count {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: 999px;
  font-weight: 800;
  letter-spacing: 0.1em;
}
/* Shared brand-pulse dot for inline LIVE indicators — wildcard
   scoreboard cards use this. Tiny solid emerald disc with an
   expanding shadow ring (echoes the LiveSlate logo's concentric
   broadcast pulse). Xav 2026-06-02. */
.ls-brand-live-dot {
  display: inline-block;
  width: 5px; height: 5px;
  border-radius: 999px;
  background: var(--ls-accent);
  animation: ls-brand-live-pulse 1.6s ease-out infinite;
}
@keyframes ls-brand-live-pulse {
  0%   { box-shadow: 0 0 0 0 rgb(var(--ls-accent-rgb) / 0.65); }
  70%  { box-shadow: 0 0 0 5px rgb(var(--ls-accent-rgb) / 0); }
  100% { box-shadow: 0 0 0 0 rgb(var(--ls-accent-rgb) / 0); }
}

/* LIVE count chip — brand emerald (Xav 2026-06-02). Was red; flipped
   so every "LIVE" indicator on the dashboard reads the same brand
   colour the slate-picker LIVE pill already uses. */
.ls-scoreboard-count-live {
  background: rgb(var(--ls-accent-rgb) / 0.10);
  color: var(--ls-accent-deep);
}
.dark .ls-scoreboard-count-live {
  background: rgb(var(--ls-accent-rgb) / 0.18);
  color: var(--ls-accent-soft);
}
.ls-scoreboard-count-dot {
  width: 5px; height: 5px;
  border-radius: 999px;
  background: var(--ls-accent);
  animation: mlb-pulse 1.6s ease-out infinite;
}
.ls-scoreboard-count-soon  {
  background: rgb(var(--ls-warn-soft-rgb) / 0.10);
  color: #b45309;
}
.dark .ls-scoreboard-count-soon {
  background: rgb(var(--ls-warn-soft-rgb) / 0.18);
  color: #fcd34d;
}
.ls-scoreboard-count-final {
  background: rgb(var(--ls-ink-soft-rgb) / 0.12);
  color: #64748b;
}
.dark .ls-scoreboard-count-final {
  background: rgb(var(--ls-ink-soft-rgb) / 0.18);
  color: #cbd5e1;
}
/* Cog — Xav 2026-06-01. Added when option A (cog popover) shipped
   to give the headline scoreboard a discoverable settings entry
   point. Same emerald hover treatment as panel_chrome cogs. */
.ls-scoreboard-cog {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border: none;
  background: transparent;
  border-radius: 6px;
  color: #94a3b8;
  cursor: pointer;
  transition: color 150ms ease, background 150ms ease;
}
.ls-scoreboard-cog:hover {
  color: var(--ls-accent-hover);
  background: rgb(var(--ls-accent-rgb) / 0.10);
}
.dark .ls-scoreboard-cog { color: #64748b; }
.dark .ls-scoreboard-cog:hover {
  color: var(--ls-accent-hi);
  background: rgb(var(--ls-accent-hi-rgb) / 0.12);
}

/* Skeleton placeholders — pulsing rectangles so the zone has SOMETHING
   to render while the API call is in flight. Without these the layout
   briefly collapses (zero-height) which reads as "the scoreboard
   vanished". */
.ls-scoreboard-skeleton {
  display: flex;
  gap: 8px;
  padding: 8px;
  height: 100%;
  min-height: 130px;
}
.ls-scoreboard-skel-card {
  flex: 1;
  border-radius: 10px;
  background: linear-gradient(90deg,
              rgb(var(--ls-ink-soft-rgb) / 0.08) 0%,
              rgb(var(--ls-ink-soft-rgb) / 0.18) 50%,
              rgb(var(--ls-ink-soft-rgb) / 0.08) 100%);
  background-size: 200% 100%;
  animation: ls-skel-shimmer 1.4s ease-in-out infinite;
}

/* ── Shared panel-card surface (audit v5, Lever E) ────────────────────
   One soft-surface base for every widget's inner cards. The grouped
   selector single-sources the padding/radius/border/background + dark
   flip that .bp-card/.pp-card/.act-card/.hr-card each used to re-declare
   (they now keep only their layout + accent deltas). .ls-panel-card is
   the class new widgets opt into. */
.ls-panel-card,
.bp-card, .pp-card, .act-card, .hr-card {
  padding: 8px 10px;
  border-radius: 10px;
  border: 1px solid rgb(var(--ls-ink-rgb) / 0.06);
  background: rgb(var(--ls-ink-rgb) / 0.02);
  min-width: 0;
}
.dark .ls-panel-card,
.dark .bp-card, .dark .pp-card, .dark .act-card, .dark .hr-card {
  background: rgb(var(--ls-surface-rgb) / 0.02);
  border-color: rgb(var(--ls-surface-rgb) / 0.05);
}
.ls-panel-card--live {
  border-color: var(--ls-accent);
  background: rgb(var(--ls-accent-rgb) / 0.05);
  box-shadow: 0 0 0 1px rgb(var(--ls-accent-rgb) / 0.35), 0 4px 12px -4px rgb(var(--ls-accent-rgb) / 0.30);
}

/* Generic loading skeleton — a card-shaped shimmer for any panel waiting
   on first data (reuses ls-skel-shimmer). Panels branch to this instead
   of the empty state while loading, so "loading" stops reading as "empty". */
.ls-panel-skeleton { display: flex; flex-direction: column; gap: 6px; padding: 2px; }
.ls-panel-skel-row {
  height: 38px; border-radius: 10px;
  background: linear-gradient(90deg,
              rgb(var(--ls-ink-soft-rgb) / 0.08) 0%,
              rgb(var(--ls-ink-soft-rgb) / 0.18) 50%,
              rgb(var(--ls-ink-soft-rgb) / 0.08) 100%);
  background-size: 200% 100%;
  animation: ls-skel-shimmer 1.4s ease-in-out infinite;
}
.ls-panel-skel-row:nth-child(2) { opacity: 0.7; }
.ls-panel-skel-row:nth-child(3) { opacity: 0.45; }

/* ── Team token (shared team-identity component, modes A–E) ──────────
   MOVED to static/css/team_token.css (loaded globally in base.html) so the
   same pill can render off /live too — the /lineups Exposure tab and future
   dashboard widget panels reuse it. The .tt-switch dev chip stays below (it's
   dashboard-only). */

/* Eval switcher chip — fixed, unobtrusive; pre-launch dev tool for picking
   the mode. Flips ttMode → the root ttm-<X> class → every token at once. */
.tt-switch {
  position: fixed; left: 12px; bottom: 12px; z-index: 90;
  display: inline-flex; align-items: center; gap: 2px;
  background: #fff; border: 1px solid rgb(var(--ls-ink-rgb) / 0.15);
  border-radius: 8px; padding: 3px 4px; box-shadow: 0 6px 18px -6px rgb(0 0 0 / 0.4);
  font-family: var(--font-mono); font-size: 10px;
}
.dark .tt-switch { background: #0f172a; border-color: rgb(var(--ls-surface-rgb) / 0.15); }
.tt-switch b { color: var(--ls-ink-soft); font-weight: 700; padding: 0 4px; letter-spacing: 0.08em; }
.tt-switch button {
  border: 0; background: transparent; color: var(--ls-ink-2); font: inherit; font-weight: 800;
  width: 20px; height: 20px; border-radius: 5px; cursor: pointer; transition: background 0.12s, color 0.12s;
}
.tt-switch button.on { background: var(--ls-accent); color: #fff; }

@keyframes ls-skel-shimmer {
  0%   { background-position: 100% 0; }
  100% { background-position: -100% 0; }
}

/* Row 1 — scoreboard full width (personal strip moved to Row 2). */
.ls-headline {
  /* Flex with center alignment on BOTH axes so the cards sit
     dead-centre of the frame regardless of how many there are
     (1 card or 14 — same vertical balance). Xav 2026-05-27. */
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  flex: 1;
}
.ls-headline-scoreboard {
  width: 100%;
  overflow: hidden;
}
/* Original behaviour — cards stretch to fill the row width naturally
   via the partial's own flex layout. Reverted 2026-05-27 after the
   capped variants (320 then 520) consistently looked off; the
   partial's native sizing is what works. Frame bg-fill + watermark
   above handle the sparse-row aesthetic without touching card size. */
.ls-headline-scoreboard .bgc { max-width: none; }

/* Legacy :fullscreen rules removed 2026-05-30. TV mode (which
   auto-enters on any fullscreenchange) now owns chrome stripping via
   the html[data-tv-mode] selector further down — it does strictly
   more than the old :fullscreen subset (also hides KPIs, scales font
   to 130%, expands grid to 100vh). Keeping two CSS systems for the
   same trigger created the "TV and Fullscreen look the same" confusion. */

/* Saved-pulse dot — green LED-style pip on the Edit/Done tab that
   flashes briefly after every successful layout auto-save. Acts as
   passive feedback so users see their changes were persisted without
   a toast or other disruption. Xav 2026-05-27. */
.ls-edit-tab { position: relative; }

/* ── Phase 4 section tab strip (Xav 2026-05-31) ──────────────────
   Sits between the sub-nav and the dashboard content. Only renders
   when a system preset has >1 section enabled. Uses a horizontal
   pill row that visually echoes the .sp-segmented family but more
   compact (it's secondary navigation, not page-level). Active tab
   gets an emerald wash + bottom-accent. Hides cleanly in edit mode
   so the user can focus on layout without sections distracting. */
/* Dashboard/preset flip crossfade (Xav 2026-06-14). The main column fades
   out, the layout swaps while invisible, then it fades back in together so
   a Simple↔Analytical flip reads like a page transition instead of widgets
   popping piecemeal. Driven by _switching in live_v2/index.js. */
.ls-dash-main {
  transition: opacity 170ms ease;
}
.ls-dash-main.ls-dash-switching {
  opacity: 0;
  pointer-events: none;
}

.ls-section-tabs {
  position: relative;
  display: flex;
  align-items: center;
  /* Left-aligned (Xav 2026-06-12, after a brief stint on the right) —
     reads as the main frame's heading row, mirroring how the
     SCOREBOARD title anchors the rail. */
  justify-content: flex-start;
  gap: 4px;
  /* Breathing room (Xav 2026-06-14): the heading row and the content
     below were crowding. Space above the rail (padding) + space below
     it (margin) frame the divider rather than just pushing pixels. */
  padding: 0 0 13px;
  margin: 0 0 16px 0;
}
/* Hairline rail that fades to transparent at both ends — separates the
   tab row from its content without a hard box border (which would fight
   the frame's rounded corners). The faded edge is the premium tell. */
.ls-section-tabs::after {
  content: '';
  position: absolute;
  left: 2px; right: 2px; bottom: 0;
  height: 1px;
  background: linear-gradient(90deg,
    transparent 0%,
    rgb(var(--ls-ink-rgb) / 0.10) 7%,
    rgb(var(--ls-ink-rgb) / 0.10) 93%,
    transparent 100%);
}
.dark .ls-section-tabs::after {
  background: linear-gradient(90deg,
    transparent 0%,
    rgb(var(--ls-surface-rgb) / 0.09) 7%,
    rgb(var(--ls-surface-rgb) / 0.09) 93%,
    transparent 100%);
}
/* Live beacon on THE ACTION tab — pulses while any game is live. */
.ls-section-tab .ls-tab-live-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 9999px;
  background: var(--ls-accent);
  margin-left: 6px;
  vertical-align: 1px;
  animation: ls-tab-live-pulse 1.6s ease-out infinite;
}
@keyframes ls-tab-live-pulse {
  0%   { box-shadow: 0 0 0 0 rgb(var(--ls-accent-rgb) / 0.65); }
  70%  { box-shadow: 0 0 0 5px rgb(var(--ls-accent-rgb) / 0); }
  100% { box-shadow: 0 0 0 0 rgb(var(--ls-accent-rgb) / 0); }
}
.ls-section-tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  white-space: nowrap;
  padding: 7px 16px 8px;
  border: 0;
  background: transparent;
  color: var(--ls-ink-muted);
  font-size: var(--ls-fs-11);
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  cursor: pointer;
  border-radius: 8px;
  transition: color 140ms ease-out, background 140ms ease-out;
}
.ls-section-tab:hover {
  color: var(--ls-ink);
  background: rgb(var(--ls-ink-rgb) / 0.04);
}
.dark .ls-section-tab:hover { background: rgb(var(--ls-surface-rgb) / 0.04) }
.ls-section-tab.is-active {
  color: var(--ls-accent-deep);
  background: rgb(var(--ls-accent-rgb) / 0.10);
}
.dark .ls-section-tab.is-active {
  color: var(--ls-accent-hi);
  background: rgb(var(--ls-accent-rgb) / 0.18);
}
/* Active tab gets a 2px emerald underline-style accent — anchored at
   the bottom of the pill. Reads as "you are here" without forcing the
   eye onto a separate visual element. */
.ls-section-tab.is-active::after {
  content: '';
  position: absolute;
  left: 14px; right: 14px; bottom: 2px;
  height: 2px;
  border-radius: 2px;
  background: var(--ls-accent);
}

/* ── Editable user-dashboard tabs (Xav 2026-06-14) ───────────────────── */
.ls-section-tab-wrap { position: relative; display: inline-flex; flex-shrink: 0; }
/* Delete × inside a tab — edit mode only. */
.ls-section-tab-x {
  margin-left: 6px;
  font-size: var(--ls-fs-13); line-height: 1; font-weight: 700;
  color: #94a3b8; cursor: pointer; border-radius: 4px;
  padding: 0 2px; transition: color 120ms ease, background 120ms ease;
}
.ls-section-tab-x:hover { color: #ef4444; background: rgb(var(--ls-danger-rgb) / 0.1); }
/* Inline rename input — sized/styled like the tab label. */
.ls-section-tab-input {
  font-size: var(--ls-fs-11); font-weight: 700; letter-spacing: 0.10em; text-transform: uppercase;
  padding: 7px 12px 8px; border-radius: 8px;
  border: none; outline: none;
  box-shadow: 0 0 0 2px rgb(var(--ls-accent-rgb) / 0.6);
  background: rgb(var(--ls-accent-rgb) / 0.06); color: #0f172a;
  width: 130px; max-width: 50vw;
}
.dark .ls-section-tab-input { color: #f1f5f9; background: rgb(var(--ls-accent-rgb) / 0.14); }
/* ＋ Add tab — quieter than the real tabs; emerald on hover. */
.ls-section-tab-add {
  color: #94a3b8; font-size: 15px; font-weight: 700;
  padding: 6px 12px 7px;
}
.ls-section-tab-add:hover { color: var(--ls-accent-deep); background: rgb(var(--ls-accent-rgb) / 0.08); }
.dark .ls-section-tab-add:hover { color: var(--ls-accent-soft); background: rgb(var(--ls-accent-rgb) / 0.14); }

/* Phone — section tabs become scrollable horizontal so 4 labels don't
   wrap. Same row, just lets it scroll if the screen's too tight. */
@media (max-width: 640px) {
  .ls-section-tabs {
    overflow-x: auto;
    scrollbar-width: none;
    padding-bottom: 11px;
    margin-bottom: 13px;
  }
  .ls-section-tabs::-webkit-scrollbar { display: none; }
  .ls-section-tab { flex-shrink: 0; padding: 6px 12px 7px; font-size: var(--ls-fs-10); }
}

/* Locked Edit pill — system presets are read-only in Phase 2+. Subdued
   visual treatment + not-allowed cursor signals "this works but not
   here". Phase 5 replaces the disabled-click with a Customize CTA. */
.ls-edit-tab.is-locked {
  opacity: 0.45;
  cursor: not-allowed !important;
}
.ls-edit-tab.is-locked:hover {
  background: transparent !important;
  border-color: inherit !important;
  transform: none !important;
}
.ls-saved-pulse {
  position: absolute;
  top: 4px;
  right: 6px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ls-accent);                      /* emerald-500 */
  box-shadow: 0 0 6px rgb(var(--ls-accent-rgb) / 0.55);
  opacity: 0;
  transform: scale(0.6);
  transition: opacity 220ms ease-out, transform 220ms ease-out;
  pointer-events: none;
}
.ls-saved-pulse.is-on {
  opacity: 1;
  transform: scale(1);
}
/* Failed save — persistent red (doesn't fade like the green pulse) with a
   gentle attention pulse; pointer-events on so the title tooltip shows. */
.ls-saved-pulse.is-failed {
  opacity: 1;
  transform: scale(1);
  background: var(--ls-danger);
  box-shadow: 0 0 6px rgb(var(--ls-danger-rgb) / 0.6);
  pointer-events: auto;
  animation: ls-saved-fail-pulse 1.6s ease-in-out infinite;
}
@keyframes ls-saved-fail-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.45; }
}

/* Sub-nav stacking-context fix (2026-05-27).
   discover_tabs.css gives .sp-tabs-wrap z-index: 5, which exactly tied
   the .ls-kpi-chrome / .ls-cell-chrome z-index: 5 on this page —
   later DOM order put the chrome IN FRONT of the picker dropdown
   (which lives inside .sp-tabs-wrap). Bumping just this page's wrap
   to 30 lets the picker float above any in-grid chrome without
   affecting the global rule. */
.sp-tabs-wrap { z-index: 30 !important; }

/* Flush fix (2026-07-06). Butt the live dashboard against the main nav — kill
   the grey gap under the nav that only this page showed. main's pt-2 / sm:pt-3
   is cancelled on the ROOT container (.ls-flush-top, a DIRECT child of main):
   a negative margin there reliably offsets up into the padding. The obvious
   attempt — the sub-nav's own -mt-2 / -mt-3 — does NOT work here: it has to
   collapse up through .sp → .space-y-3, and discover_tabs.css' `.sp-tabs-wrap
   { margin: 0 }` reset defeats it. Neutralise the sub-nav margin so it can
   never double-pull; the root does the whole job. */
.ls-flush-top { margin-top: -8px; }
@media (min-width: 640px) { .ls-flush-top { margin-top: -12px; } }
.sp-tabs-wrap.sp { margin-top: 0 !important; }

/* Edit button pushes to the far right of the sub-nav in normal mode —
   visually separates "view/config controls" (Pick slate / Pick mode /
   Settings / Admin) from the "enter editor" action.
   `order: 100` sends it to the END of the flex row regardless of DOM
   source position, so Settings + Admin (defined AFTER Edit in source)
   still render before it visually. `margin-left: auto` then claims all
   remaining horizontal space, pushing Edit hard against the right edge.
   Removed when editMode is active so the edit-mode bar can keep Done
   first followed by Add KPI / Add Panel / Reset / Save in source order.
   (Xav 2026-05-30 sub-nav tidy.) */
.sp-tab.is-pushed-right {
  order: 100;
  margin-left: auto;
}

/* ── Tablet / narrow-desktop sub-nav (Xav 2026-05-30 #2 pass) ──
   At 641-1399px (covers iPad in landscape + portrait, plus older
   1366×768 laptops), the default 100px-wide pills + margin-left:auto
   leave ~600px of dead space between Admin and Edit. Pull everything
   in: tighter tabs, a fixed 12px gap before Edit instead of
   margin-left:auto, and a cap on slate-picker width so a long slate
   name can't push the rest off-axis. */
@media (min-width: 641px) and (max-width: 1399px) {
  .sp-tab {
    width: 88px;
    min-width: 88px;
    padding: 8px 8px 7px;
  }
  .sp-tab-search { width: 44px; min-width: 44px; padding: 8px 6px 7px; }
  .sp-tabs-flex { gap: 6px; }
  /* Keep Edit at the end of the row (order: 100 still applies) but
     stop it from claiming every remaining pixel — sit right next to
     the cluster so the bar reads as one tight group. */
  .sp-tab.is-pushed-right { margin-left: 12px; }
  /* Slate picker — cap width and ellipsis long slate names so they
     don't shove every other tab off-screen. The chevron stays visible. */
  .ls-subnav-slate > .sp-tab {
    min-width: 120px !important;
    max-width: 200px;
    width: auto;
  }
  .ls-subnav-slate-name {
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 140px;
    display: inline-block;
    vertical-align: middle;
  }
}

/* ── Edit-mode sub-nav (Xav 2026-05-30) ────────────────────────────
   Inverts the normal sub-nav palette: the WRAP background becomes
   emerald (the colour normally reserved for active accents), and the
   pills inside stay structurally identical to normal mode — so the bar
   stays exactly the same height, no layout jump. The visual mode shift
   is carried entirely by the background-colour swap. Premium "tools
   light up" treatment (Stripe / Linear / macOS-toolbar pattern).
   Pills get a touch of contrast tweak (label colour goes white on the
   tabs that were previously dark) so they read against the emerald
   surface. Smooth 240ms transition on the wrap. */
.sp-tabs-wrap {
  transition: background-color 240ms ease-out,
              box-shadow       240ms ease-out;
}
/* Edit-mode bar — TRUE COLOUR INVERSION of normal mode (Xav 2026-05-30).
   Normal: emerald bar + white icons.
   Edit:   white bar + emerald icons.
   Every accent flips. Pills, labels, the title chip — everywhere
   emerald becomes white and white becomes emerald. */
.sp-tabs-wrap.is-edit-mode {
  background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
  box-shadow:
    inset 0  1px 0 rgb(var(--ls-ink-rgb) / 0.06),
    inset 0 -1px 0 rgb(var(--ls-ink-rgb) / 0.08),
    0 2px 8px -2px rgb(var(--ls-ink-rgb) / 0.12);
}
.dark .sp-tabs-wrap.is-edit-mode {
  background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%);
  box-shadow:
    inset 0  1px 0 rgb(var(--ls-surface-rgb) / 0.45),
    inset 0 -1px 0 rgb(var(--ls-ink-rgb) / 0.10),
    0 2px 10px -2px rgb(var(--ls-ink-rgb) / 0.18);
}

/* EDIT MODE watermark banner — anchored to .sp-tabs-header (the
   inner positioned container) rather than the wrap itself. Anchoring
   to the wrap activated its z-index:30 from discover_tabs.css which
   leaked a thin emerald sliver across the top of the viewport
   (Xav 2026-05-30 bug). The header is already in normal flow and
   doesn't trigger that stacking-context cascade. */
.sp-tabs-wrap.is-edit-mode .sp-tabs-header {
  position: relative;
}
.sp-tabs-wrap.is-edit-mode .sp-tabs-header::before {
  content: "EDIT MODE  ·  EDIT MODE";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--ls-fs-44);
  font-weight: 900;
  letter-spacing: 0.45em;
  color: rgb(var(--ls-accent-rgb) / 0.14);                   /* emerald-500 @ 14% */
  white-space: nowrap;
  overflow: hidden;
  pointer-events: none;
  user-select: none;
  text-transform: uppercase;
  font-family: 'Bebas Neue', system-ui, sans-serif;  /* broadcast-style display face */
  z-index: 0;
}
.dark .sp-tabs-wrap.is-edit-mode .sp-tabs-header::before {
  color: rgb(var(--ls-accent-rgb) / 0.22);
}
/* Lift the page-title pill + the scrollable tab row above the watermark.
   These are direct children of .sp-tabs-header — bumping them to
   z-index:1 ensures the watermark stays behind. */
.sp-tabs-wrap.is-edit-mode .sp-tabs-header > .sp-page-title-pill,
.sp-tabs-wrap.is-edit-mode .sp-tabs-header > .sp-tabs-scroll {
  position: relative;
  z-index: 1;
}

/* Force the title pill visible in edit mode — discover_tabs.css hides
   `.sp-page-title-pill` at every viewport (it was designed for the
   nav-context where the main menu already names the section). In edit
   mode we NEED it to surface "EDITING · SIMPLE" so the user knows
   which layout they're editing.
   Also push it to the FAR RIGHT next to the Edit/Done tab via flex
   `order` — .sp-tabs-header is a flex row of [pill, scroll-area];
   pill order:1 + default scroll order:0 swaps them so the pill lands
   at the right edge, immediately after the scrollable tab strip.
   Margin-left adds a small breathing gap from the Edit tab. */
.sp-tabs-wrap.is-edit-mode .sp-page-title-pill {
  display: inline-flex !important;
  order: 1;
  margin-left: 8px;
}

/* EDITING page-title pill — inverse of LIVE: solid emerald with white
   text, sitting on the now-white bar. Pulsing dot becomes white. */
.sp-page-title-pill--editing {
  background: var(--ls-accent) !important;                    /* emerald-500 */
  color: #ffffff !important;
  box-shadow: 0 1px 4px rgb(var(--ls-accent-rgb) / 0.35);
}
.sp-page-title-pill--editing .sp-page-title-pill-dot {
  background: #ffffff !important;
}
/* Preset suffix (SIMPLE / ANALYTICAL / TV) in the EDITING pill.
   Slightly softer than the main label so it reads as "context" not
   "primary identity". Inherits pill text colour. */
.sp-page-title-pill-preset {
  margin-left: 6px;
  font-weight: 600;
  font-size: var(--ls-fs-11);
  letter-spacing: 0.10em;
  opacity: 0.78;
}

/* .sp-tab label flips to emerald — was white on the emerald bar. */
.sp-tabs-wrap.is-edit-mode .sp-tab .sp-tab-name {
  color: var(--ls-accent-deep);                                    /* emerald-700 */
}
.sp-tabs-wrap.is-edit-mode .sp-tab[title]:hover .sp-tab-name {
  color: #065f46;                                    /* emerald-800 */
}

/* Thin emerald border on every pill (Xav 2026-05-30) — overrides the
   default subtle dark .sp-tab border. Defines each pill cleanly against
   the white edit-mode bar; the colour matches the inverted icon palette.
   Hover deepens for a subtle "lift" cue. */
.sp-tabs-wrap.is-edit-mode .sp-tab {
  border-color: rgb(var(--ls-accent-rgb) / 0.40);            /* emerald-500 @ 40% */
}
.sp-tabs-wrap.is-edit-mode .sp-tab:hover:not(:disabled) {
  border-color: rgb(var(--ls-accent-rgb) / 0.65);
}
.sp-tabs-wrap.is-edit-mode .sp-tab.active {
  border-color: var(--ls-accent);                             /* full emerald-500 */
}
.dark .sp-tabs-wrap.is-edit-mode .sp-tab {
  border-color: rgb(var(--ls-accent-rgb) / 0.50);
}

/* Add KPI / Add Panel — keep the .sp-action-pill--emerald default look
   (solid emerald-600 fill with white text) since the bar is now white
   and the emerald CTAs pop naturally. No override needed. */

/* Row 2 — slate picker | KPI tiles | personal strip. Single horizontal
   flex on desktop; stacks vertically on phone (picker → kpis wrap → strip). */
.ls-row2 {
  display: flex;
  gap: 0.5rem;
  align-items: stretch;
  margin-bottom: 0.5rem;
}
.ls-row2-picker {
  flex: 0 0 auto;
  position: relative;
  display: flex;
  align-items: center;
  padding: 4px 8px;
  border-radius: 12px;
  border: 1px solid rgba(0, 0, 0, 0.08);
  background: #fff;
}
.dark .ls-row2-picker { border-color: rgb(var(--ls-surface-rgb) / 0.06); background: #0f172a; }

/* Sub-nav slate picker — keep .sp-tab's default column-flex shape
   (icon on top, label row underneath) so the picker matches the
   height of the KPI / PANEL / RESET / SAVE tabs exactly. We only
   widen the min/max width to fit slate names; the label row inside
   .sp-tab-row already lays out icon + chevron horizontally. */
.ls-subnav-slate > .sp-tab {
  min-width: 140px !important;
  /* width: max-content forces the tab to grow to fit the slate name
     instead of stopping at min-width and clipping (the .sp-tab-row
     inside is justify-content:center so any overflow happens
     symmetrically — leading characters get hidden). Sub-nav scrolls
     horizontally if the cluster overflows. */
  width: max-content;
}
.ls-subnav-slate-name {
  /* Single line, no ellipsis. The tab grows; let the text dictate. */
  white-space: nowrap;
}

/* Phase 7 upgrade banner — sits above the dashboard grid when the
   admin has bumped this sport's default layout version above what
   the user last acknowledged. Two visual states (offer vs preview)
   share the same row layout. Emerald-tinted at rest (positive,
   inviting); preview state lifts to amber so the user knows the
   underlying layout has been swapped temporarily. */
.ls-upgrade-banner {
  margin: 6px 0 10px;
  border-radius: 12px;
  background: linear-gradient(180deg, rgb(var(--ls-accent-rgb) / 0.10) 0%, rgb(var(--ls-accent-rgb) / 0.05) 100%);
  border: 1px solid rgb(var(--ls-accent-rgb) / 0.30);
  overflow: hidden;
}
.dark .ls-upgrade-banner {
  background: linear-gradient(180deg, rgb(var(--ls-accent-rgb) / 0.16) 0%, rgb(var(--ls-accent-rgb) / 0.08) 100%);
  border-color: rgb(var(--ls-accent-hi-rgb) / 0.40);
}
.ls-upgrade-banner-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 14px;
}
.ls-upgrade-banner-row--preview {
  background: rgb(var(--ls-warn-soft-rgb) / 0.10);
  border-left: 4px solid #f59e0b;
}
.dark .ls-upgrade-banner-row--preview {
  background: rgb(var(--ls-warn-soft-rgb) / 0.16);
}
.ls-upgrade-banner-icon {
  font-size: 22px;
  line-height: 1;
  flex-shrink: 0;
}
.ls-upgrade-banner-text {
  flex: 1;
  min-width: 0;
}
.ls-upgrade-banner-title {
  font-size: var(--ls-fs-13);
  font-weight: 700;
  color: var(--ls-accent-deep);
  line-height: 1.2;
  display: flex;
  align-items: center;
  gap: 6px;
}
.dark .ls-upgrade-banner-title { color: var(--ls-accent-soft); }
.ls-upgrade-banner-row--preview .ls-upgrade-banner-title { color: #b45309; }
.dark .ls-upgrade-banner-row--preview .ls-upgrade-banner-title { color: #fbbf24; }
.ls-upgrade-banner-version {
  font-size: var(--ls-fs-10);
  font-weight: 800;
  padding: 1px 6px;
  border-radius: 999px;
  background: rgb(var(--ls-accent-rgb) / 0.20);
  color: inherit;
  letter-spacing: 0.04em;
}
.ls-upgrade-banner-row--preview .ls-upgrade-banner-version {
  background: rgb(var(--ls-warn-soft-rgb) / 0.25);
}
.ls-upgrade-banner-subtitle {
  font-size: var(--ls-fs-11);
  color: #6b7280;
  margin-top: 2px;
  line-height: 1.3;
}
.dark .ls-upgrade-banner-subtitle { color: #94a3b8; }
.ls-upgrade-banner-actions {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}
.ls-upgrade-banner-cancel {
  background: transparent;
  border: 1px solid rgb(var(--ls-ink-rgb) / 0.18);
  color: #6b7280;
  font-size: var(--ls-fs-11);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 4px 12px;
  border-radius: 999px;
  cursor: pointer;
  transition: all 120ms;
}
.dark .ls-upgrade-banner-cancel {
  border-color: rgb(var(--ls-ink-soft-rgb) / 0.30);
  color: #94a3b8;
}
.ls-upgrade-banner-cancel:hover {
  background: rgb(var(--ls-ink-rgb) / 0.05);
  color: #111827;
}
.dark .ls-upgrade-banner-cancel:hover {
  background: rgb(var(--ls-surface-rgb) / 0.05);
  color: #f1f5f9;
}
.ls-upgrade-banner-x {
  background: transparent;
  border: 0;
  color: #94a3b8;
  font-size: var(--ls-fs-20);
  line-height: 1;
  width: 28px;
  height: 28px;
  border-radius: 999px;
  cursor: pointer;
  transition: all 120ms;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.ls-upgrade-banner-x:hover {
  background: rgb(var(--ls-ink-rgb) / 0.06);
  color: #111827;
}
.dark .ls-upgrade-banner-x:hover {
  background: rgb(var(--ls-surface-rgb) / 0.08);
  color: #f1f5f9;
}

@media (max-width: 640px) {
  .ls-upgrade-banner-row {
    flex-wrap: wrap;
  }
  .ls-upgrade-banner-text { flex-basis: 100%; }
  .ls-upgrade-banner-actions { margin-left: auto; }
}

/* .ls-edit-toolbar block deleted 2026-05-30. The edit-mode toolbar
   now lives inline in .sp-tabs-wrap (see .sp-tabs-wrap.is-edit-mode
   skin + the Add KPI / Add Panel pills inserted into .sp-tabs-flex).
   Zero vertical real estate cost. */

/* Drop the drop-shadow on sub-nav action-tab icons. Was fine for the
   filled-colour SVGs; with the new stroke-based monochrome icons the
   shadow softens edges and reads as fuzzy. Kept on slate-picker's
   sport image because PNGs need the lift. */
.sp-tab > .sp-tab-icon > svg {
  filter: none !important;
}

/* Sub-nav tab icons get a contrasting emerald shade — using
   currentColor on the SVG strokes + scoping `color` here on the
   icon container. Two key constraints:
   1. The wider .sp-tabs-wrap selector is required because the
      @media (min-width:641px) rule in discover_tabs.css promotes
      .sp-tab text to translucent white — we need to win that
      specificity battle so icons don't inherit white.
   2. The wrap's bg is emerald-600 var(--ls-accent-hover), so any same-shade
      emerald (-600/-700) would visually disappear into the band.
      emerald-200 (#a7f3d0) reads as a soft mint accent on the
      darker green. When a tab is .active its bg flips to solid
      white — then a deep emerald-700 icon contrasts properly. */
.sp-tabs-wrap .sp-tab > .sp-tab-icon { color: #a7f3d0; }
.sp-tabs-wrap .sp-tab.active > .sp-tab-icon { color: var(--ls-accent-deep); }
.dark .sp-tabs-wrap .sp-tab > .sp-tab-icon { color: var(--ls-accent-soft); }
.dark .sp-tabs-wrap .sp-tab.active > .sp-tab-icon { color: var(--ls-accent-hi); }
.ls-subnav-slate-chev {
  width: 12px; height: 12px;
  /* currentColor tracks the tab's text colour — slate-500 normally,
     dark emerald when .sp-tab.active. Always visible against the
     white tab background. */
  color: currentColor;
  flex-shrink: 0;
  transition: transform 160ms ease-out;
}
/* Sub-nav dropdowns use position:fixed so they escape the
   .sp-tabs-scroll wrapper's implicit overflow-y:auto clipping
   (set when overflow-x:auto enables horizontal scroll). Position
   is set inline by the host's _anchorDropdown helper on each open. */
.ls-subnav-dropdown {
  position: fixed !important;
  z-index: 200;
  /* Slightly elevated shadow + rounded corners so the dropdown reads
     as a floating card, not just an absolutely-positioned panel. */
  box-shadow:
    0 12px 32px -8px rgb(var(--ls-ink-rgb) / 0.22),
    0 4px 12px -4px rgb(var(--ls-ink-rgb) / 0.12),
    0 0 0 1px rgb(var(--ls-ink-rgb) / 0.04);
}
.dark .ls-subnav-dropdown {
  box-shadow:
    0 12px 32px -8px rgba(0, 0, 0, 0.6),
    0 4px 12px -4px rgba(0, 0, 0, 0.45),
    0 0 0 1px rgb(var(--ls-ink-soft-rgb) / 0.06);
}

/* Premium dropdown menu — for the KPI / Panel pickers. Three layers:
   header (eyebrow + title), scrollable item list, optional footer hint. */
.ls-subnav-menu {
  padding: 6px;
  min-width: 260px;
  max-height: min(420px, 70vh);
  overflow-y: auto;
}
.ls-subnav-menu-header {
  padding: 10px 12px 8px;
  border-bottom: 1px solid rgb(var(--ls-ink-rgb) / 0.06);
  margin-bottom: 4px;
}
.dark .ls-subnav-menu-header { border-bottom-color: rgb(var(--ls-ink-soft-rgb) / 0.08); }
.ls-subnav-menu-eyebrow {
  display: block;
  font-size: var(--ls-fs-9);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--ls-accent);
  margin-bottom: 2px;
}
.dark .ls-subnav-menu-eyebrow { color: var(--ls-accent-hi); }
.ls-subnav-menu-title {
  font-size: var(--ls-fs-13);
  font-weight: 700;
  color: var(--ls-ink);
  letter-spacing: -0.01em;
}

/* ── Widget info popover (ⓘ) ───────────────────────────────────────────
   Premium help card, one consistent surface for every panel + KPI.
   Reuses the floating-card shadow from .ls-subnav-dropdown; adds an
   emerald-accented header rail, generous type, and requirement badges.
   Theme + tone stay identical across all widgets. (Xav 2026-06-21) */
.ls-info-pop {
  padding: 0;
  overflow: hidden;
  border-radius: 16px;
  background: linear-gradient(180deg, #ffffff 0%, #f8fafb 100%);
  border: 1px solid rgb(var(--ls-ink-rgb) / 0.07);
  transform-origin: top right;
}
.dark .ls-info-pop {
  background: linear-gradient(180deg, #111c33 0%, #0d1526 100%);
  border-color: rgb(var(--ls-ink-soft-rgb) / 0.10);
}
/* Header — emerald glyph + eyebrow/title, sitting on a faint tinted rail
   with a hairline emerald top edge so the card reads as "ours". */
.ls-info-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 15px 11px;
  background: linear-gradient(180deg, rgb(var(--ls-accent-rgb) / 0.07) 0%, rgb(var(--ls-accent-rgb) / 0) 100%);
  border-top: 2px solid var(--ls-accent);
  border-bottom: 1px solid rgb(var(--ls-ink-rgb) / 0.06);
}
.dark .ls-info-head {
  background: linear-gradient(180deg, rgb(var(--ls-accent-rgb) / 0.12) 0%, rgb(var(--ls-accent-rgb) / 0) 100%);
  border-top-color: var(--ls-accent-hi);
  border-bottom-color: rgb(var(--ls-ink-soft-rgb) / 0.08);
}
.ls-info-glyph {
  flex-shrink: 0;
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border-radius: 9px;
  color: var(--ls-accent-hover);
  background: rgb(var(--ls-accent-rgb) / 0.12);
  box-shadow: inset 0 0 0 1px rgb(var(--ls-accent-rgb) / 0.22);
}
.dark .ls-info-glyph {
  color: var(--ls-accent-soft);
  background: rgb(var(--ls-accent-rgb) / 0.16);
  box-shadow: inset 0 0 0 1px rgb(var(--ls-accent-rgb) / 0.28);
}
.ls-info-glyph svg { width: 17px; height: 17px; }
.ls-info-head-text { min-width: 0; }
.ls-info-eyebrow {
  display: block;
  font-size: var(--ls-fs-9);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--ls-accent);
  line-height: 1;
  margin-bottom: 3px;
}
.dark .ls-info-eyebrow { color: var(--ls-accent-hi); }
.ls-info-title {
  font-size: var(--ls-fs-14);
  font-weight: 800;
  color: var(--ls-ink);
  letter-spacing: -0.015em;
  line-height: 1.1;
}
/* What it shows — the lead body line. */
.ls-info-what {
  margin: 0;
  padding: 12px 15px 4px;
  font-size: var(--ls-fs-12);
  font-weight: 500;
  line-height: 1.5;
  color: #475569;
}
.dark .ls-info-what { color: #cbd5e1; }
/* How to read it — secondary block, set off by a labelled micro-header. */
.ls-info-howto { padding: 8px 15px 4px; }
.ls-info-howto-label {
  display: block;
  font-size: var(--ls-fs-8);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.15em;
  color: var(--ls-ink-soft);
  margin-bottom: 4px;
}
.ls-info-howto-text {
  margin: 0;
  font-size: var(--ls-fs-12);
  font-weight: 500;
  line-height: 1.5;
  color: var(--ls-ink-muted);
}
/* Requirement / context badges. */
.ls-info-badges {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 10px 15px 14px;
}
.ls-info-badge {
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  border-radius: 999px;
  font-size: var(--ls-fs-10);
  font-weight: 700;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.ls-info-badge.is-warn {
  color: #b45309;
  background: rgb(var(--ls-warn-soft-rgb) / 0.12);
  box-shadow: inset 0 0 0 1px rgb(var(--ls-warn-soft-rgb) / 0.28);
}
.dark .ls-info-badge.is-warn {
  color: #fcd34d;
  background: rgb(var(--ls-warn-soft-rgb) / 0.14);
  box-shadow: inset 0 0 0 1px rgb(var(--ls-warn-soft-rgb) / 0.30);
}
.ls-info-badge.is-muted {
  color: #475569;
  background: rgb(var(--ls-ink-muted-rgb) / 0.10);
  box-shadow: inset 0 0 0 1px rgb(var(--ls-ink-muted-rgb) / 0.20);
}
.dark .ls-info-badge.is-muted {
  color: #cbd5e1;
  background: rgb(var(--ls-ink-soft-rgb) / 0.12);
  box-shadow: inset 0 0 0 1px rgb(var(--ls-ink-soft-rgb) / 0.18);
}

/* ⓘ chrome button — sits left of the cog. Inherits .ls-cell-btn sizing;
   this just tints the resting glyph a touch softer than the cog so the
   pair reads as [understand][configure], not two identical buttons. */
.ls-cell-btn-info svg { width: 15px; height: 15px; }

/* ⓘ on KPI tiles — KPIs have no cog, so this is their lone resting
   affordance. Hover-revealed top-right (always visible on touch), a
   quiet emerald glyph that brightens on hover. Sits clear of the label
   pill (which floats centre-top). */
/* Touch devices have no hover — keep the ⓘ gently visible so it's
   discoverable. Hidden in edit mode (the move/remove chrome takes over). */
@media (hover: none) {
}

/* Mode pill — matches the slate-picker shape (Xav 2026-06-02 round 4).
   Same min-width baseline + width:max-content so short names
   ("Simple", "Mine") sit comfortably and longer ones grow to fit
   (same as slate names like "MLB Classic Sunday Showdown" do on
   the slate-picker pill next door). The 24-char input cap on
   dashboard names keeps the pill from running away. */
.ls-mode-pill {
  min-width: 140px !important;
  width: max-content;
}
.ls-mode-pill-label {
  /* Single line, no ellipsis — let the pill grow to fit, same
     pattern as .ls-subnav-slate-name. */
  white-space: nowrap;
}

/* ── Phase 3 Mode picker (LiveSlates / Mine / + Create new) ────────
   Two-section dropdown that replaced the old Simple/Analytical/TV
   list. Section headers use the same .sp-h-section style language as
   the rest of the app. Items mirror .ls-subnav-menu-item but tighter
   (one line, no blurb on user dashboards). User-dashboard rows get
   inline pencil + trash affordances. Footer "Create new" sits below a
   hairline so it reads as an action, not another item. */
/* Consumes the canonical --ls-menu-section-* tokens. Do NOT hardcode
   font-size / weight / colour here — see style.css :root. */
.ls-mode-section-header {
  padding: var(--ls-menu-section-padding);
  font-size: var(--ls-menu-section-size);
  font-weight: var(--ls-menu-section-weight);
  text-transform: uppercase;
  letter-spacing: var(--ls-menu-section-tracking);
  color: var(--ls-menu-section-color);
  background: var(--ls-menu-section-bg);
}
.ls-mode-section-header + .ls-mode-section-header { padding-top: 14px; }   /* spacer when adjacent */

/* All four below consume the canonical --ls-menu-* tokens — see
   style.css :root. Anything that diverges (e.g. is-active state
   colour) lives here as a delta, not a full re-declaration. */
.ls-mode-item {
  width: 100%;
  display: block;
  padding: var(--ls-menu-row-padding);
  margin: 1px 4px;
  border-radius: var(--ls-menu-row-radius);
  background: transparent;
  border: 0;
  cursor: pointer;
  text-align: left;
  transition: background 140ms ease-out;
}
.ls-mode-item:hover { background: var(--ls-menu-row-hover-bg); }
.ls-mode-item.is-active { background: var(--ls-menu-row-active-bg); }

.ls-mode-item-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.ls-mode-item-name {
  font-size: var(--ls-menu-title-size);
  font-weight: var(--ls-menu-title-weight);
  color: var(--ls-menu-title-color);
  letter-spacing: var(--ls-menu-title-tracking);
}
.ls-mode-item.is-active .ls-mode-item-name { color: var(--ls-accent-deep); }
.dark .ls-mode-item.is-active .ls-mode-item-name { color: var(--ls-accent-hi); }
.ls-mode-item-active {
  font-size: var(--ls-menu-state-size);
  font-weight: var(--ls-menu-state-weight);
  text-transform: uppercase;
  letter-spacing: var(--ls-menu-state-tracking);
  color: var(--ls-accent-hover);
}
.dark .ls-mode-item-active { color: var(--ls-accent-hi); }
.ls-mode-item-blurb {
  margin-top: 2px;
  font-size: var(--ls-menu-blurb-size);
  font-weight: var(--ls-menu-blurb-weight);
  color: var(--ls-menu-blurb-color);
  line-height: 1.3;
}

.ls-mode-empty {
  padding: 6px 16px 10px;
  font-size: var(--ls-fs-11);
  color: var(--ls-ink-soft);
  font-style: italic;
}

/* User-dashboard row: button takes most of the width; two small action
   buttons sit at the right. flex-grow on the .ls-mode-item gives the
   actions natural right-alignment. */
.ls-mode-row {
  display: flex;
  align-items: stretch;
  gap: 2px;
  padding-right: 4px;
}
.ls-mode-row .ls-mode-item--user { flex: 1; margin-right: 0; }
.ls-mode-row-action {
  align-self: center;
  width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: transparent;
  color: #94a3b8;
  border-radius: 6px;
  cursor: pointer;
  transition: background 140ms, color 140ms;
}
.ls-mode-row-action:hover {
  color: var(--ls-accent-deep);
  background: rgb(var(--ls-accent-rgb) / 0.10);
}
.ls-mode-row-action--delete:hover {
  color: #dc2626;
  background: rgba(220, 38, 38, 0.10);
}
.ls-mode-row-action svg { width: 13px; height: 13px; }

.ls-mode-footer-action {
  display: flex;
  align-items: center;
  gap: 8px;
  width: calc(100% - 8px);
  margin: 8px 4px 4px;
  padding: 10px 12px;
  border: 0;
  background: rgb(var(--ls-accent-rgb) / 0.08);
  color: var(--ls-accent-deep);
  border-radius: 8px;
  font-size: var(--ls-fs-12);
  font-weight: 700;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: background 140ms;
  border-top: 1px solid rgb(var(--ls-ink-rgb) / 0.06);
  padding-top: 11px;
}
.dark .ls-mode-footer-action {
  background: rgb(var(--ls-accent-rgb) / 0.14);
  color: var(--ls-accent-hi);
  border-top-color: rgb(var(--ls-surface-rgb) / 0.06);
}
.ls-mode-footer-action:hover { background: rgb(var(--ls-accent-rgb) / 0.18); }
.dark .ls-mode-footer-action:hover { background: rgb(var(--ls-accent-rgb) / 0.22); }
.ls-mode-footer-plus {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px; height: 18px;
  border-radius: 999px;
  background: var(--ls-accent);
  color: #ffffff;
  font-weight: 900;
  font-size: var(--ls-fs-13);
  line-height: 1;
}

/* ── Create-new-dashboard modal — Phase 3 (Xav 2026-05-31) ─────────
   Lives at the top of the dashboard host so it can be triggered from
   the Mode picker without nesting. Reuses .ls-modal-backdrop +
   .ls-modal-card from the existing drill-down modal system; this
   block adds the create-flow-specific bits (radio grid, label inputs).
*/
.ls-create-modal { max-width: 520px; }
.ls-create-modal-body {
  padding: 16px 20px 8px;
}

.ls-create-label {
  display: block;
  margin-bottom: 10px;
}
.ls-create-label-text {
  display: block;
  font-size: var(--ls-fs-10);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ls-ink-muted);
  margin-bottom: 6px;
}
.ls-create-input {
  display: block;
  width: 100%;
  padding: 9px 12px;
  border: 1px solid rgb(var(--ls-ink-rgb) / 0.10);
  border-radius: 8px;
  background: var(--ls-surface);
  color: var(--ls-ink);
  font-size: var(--ls-fs-13);
  font-weight: 500;
  transition: border-color 140ms, box-shadow 140ms;
}
.dark .ls-create-input { border-color: rgb(var(--ls-surface-rgb) / 0.10) }
.ls-create-input:focus {
  outline: none;
  border-color: var(--ls-accent);
  box-shadow: 0 0 0 3px rgb(var(--ls-accent-rgb) / 0.18);
}

.ls-create-section-header {
  margin-top: 16px;
  margin-bottom: 8px;
  font-size: var(--ls-fs-10);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ls-ink-muted);
}

.ls-create-source-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.ls-create-source-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 10px 12px;
  border: 1.5px solid rgb(var(--ls-ink-rgb) / 0.08);
  border-radius: 10px;
  background: rgb(var(--ls-ink-rgb) / 0.02);
  cursor: pointer;
  transition: border-color 140ms, background 140ms, box-shadow 140ms;
}
.dark .ls-create-source-card {
  background: rgb(var(--ls-surface-rgb) / 0.02);
  border-color: rgb(var(--ls-surface-rgb) / 0.08);
}
.ls-create-source-card:hover {
  border-color: rgb(var(--ls-accent-rgb) / 0.40);
}
.ls-create-source-card.is-selected {
  border-color: var(--ls-accent);
  background: rgb(var(--ls-accent-rgb) / 0.06);
  box-shadow: 0 4px 12px -4px rgb(var(--ls-accent-rgb) / 0.25);
}
.dark .ls-create-source-card.is-selected {
  background: rgb(var(--ls-accent-rgb) / 0.10);
}
.ls-create-source-card.is-disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.ls-create-source-card.is-disabled:hover {
  border-color: rgb(var(--ls-ink-rgb) / 0.08);
}

.ls-create-source-name {
  font-size: var(--ls-fs-13);
  font-weight: 800;
  color: var(--ls-ink);
  letter-spacing: -0.01em;
}
.ls-create-source-card.is-selected .ls-create-source-name { color: var(--ls-accent-deep); }
.dark .ls-create-source-card.is-selected .ls-create-source-name { color: var(--ls-accent-hi); }

.ls-create-source-blurb {
  font-size: var(--ls-fs-10);
  color: var(--ls-ink-soft);
  line-height: 1.35;
}

.ls-create-error {
  margin-top: 14px;
  padding: 8px 12px;
  border-radius: 8px;
  background: rgba(220, 38, 38, 0.10);
  color: #b91c1c;
  font-size: var(--ls-fs-12);
  font-weight: 600;
}
.dark .ls-create-error {
  background: rgba(220, 38, 38, 0.18);
  color: #fca5a5;
}

/* Modal close button + footer buttons — generic styles so they can
   be reused by any future modal that adopts the same shell. */
.ls-modal-close {
  position: absolute;
  top: 10px; right: 12px;
  width: 28px; height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: transparent;
  color: #94a3b8;
  border-radius: 6px;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  transition: background 140ms, color 140ms;
}
.ls-modal-close:hover {
  background: rgb(var(--ls-ink-rgb) / 0.06);
  color: #0f172a;
}
.dark .ls-modal-close:hover {
  background: rgb(var(--ls-surface-rgb) / 0.08);
  color: #ffffff;
}

.ls-modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 12px 20px 18px;
  border-top: 1px solid rgb(var(--ls-ink-rgb) / 0.06);
}
.dark .ls-modal-footer { border-top-color: rgb(var(--ls-surface-rgb) / 0.06); }

.ls-modal-btn {
  display: inline-flex;
  align-items: center;
  padding: 9px 18px;
  border-radius: 8px;
  font-size: var(--ls-fs-12);
  font-weight: 700;
  letter-spacing: 0.04em;
  cursor: pointer;
  border: 1px solid transparent;
  transition: background 140ms, border-color 140ms, color 140ms;
}
.ls-modal-btn--secondary {
  background: transparent;
  color: var(--ls-ink-muted);
  border-color: rgb(var(--ls-ink-rgb) / 0.12);
}
.ls-modal-btn--secondary:hover {
  background: rgb(var(--ls-ink-rgb) / 0.04);
  color: var(--ls-ink);
}
.dark .ls-modal-btn--secondary { border-color: rgb(var(--ls-surface-rgb) / 0.12) }
.dark .ls-modal-btn--secondary:hover { background: rgb(var(--ls-surface-rgb) / 0.06) }

.ls-modal-btn--primary {
  background: var(--ls-accent-hover);
  color: #ffffff;
  border-color: var(--ls-accent-deep);
  box-shadow: 0 4px 12px -4px rgb(var(--ls-accent-hover-rgb) / 0.40);
}
.ls-modal-btn--primary:hover:not(:disabled) {
  background: var(--ls-accent);
  box-shadow: 0 4px 14px -4px rgb(var(--ls-accent-rgb) / 0.50);
}
.ls-modal-btn--primary:disabled {
  opacity: 0.6;
  cursor: not-allowed;
  box-shadow: none;
}

/* Destructive confirm (delete / reset) — solid red, distinct from the
   emerald primary so a one-way action reads as one at a glance. */
.ls-modal-btn--danger {
  background: var(--ls-danger);
  color: #ffffff;
  border-color: var(--ls-danger-deep);
  box-shadow: 0 4px 12px -4px rgb(var(--ls-danger-rgb) / 0.40);
}
.ls-modal-btn--danger:hover:not(:disabled) {
  background: var(--ls-danger-deep);
}

/* Message line in the branded confirm/prompt modal. --ls-ink-muted already
   flips light↔dark, so no separate .dark rule needed. */
.ls-action-modal-msg {
  font-size: var(--ls-fs-13);
  line-height: 1.5;
  color: var(--ls-ink-muted);
  margin: 0;
}

/* Each menu item — icon | title + meta. Hover slides a 2px emerald
   accent in from the left and tints the row. */
.ls-subnav-menu-item {
  position: relative;
  width: 100%;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  margin: 1px 0;
  border-radius: 8px;
  background: transparent;
  border: none;
  cursor: pointer;
  text-align: left;
  transition: background 140ms ease-out, transform 140ms ease-out;
}
/* At-cap state — KPI picker items become non-clickable when user
   has hit the 7-KPI max. Greys the row + kills hover. The button
   element's `disabled` attribute also blocks clicks at the browser
   level (defensive). Xav 2026-05-27. */
.ls-subnav-menu-item.is-disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.ls-subnav-menu-item.is-disabled:hover {
  background: transparent;
  transform: none;
}
.ls-subnav-menu-item::before {
  content: '';
  position: absolute;
  left: 0; top: 8px; bottom: 8px;
  width: 2px;
  border-radius: 2px;
  background: var(--ls-accent);
  transform: scaleY(0);
  transform-origin: center;
  transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
.ls-subnav-menu-item:hover {
  background: rgb(var(--ls-accent-rgb) / 0.07);
  transform: translateX(1px);
}
.dark .ls-subnav-menu-item:hover { background: rgb(var(--ls-accent-hi-rgb) / 0.10); }
.ls-subnav-menu-item:hover::before { transform: scaleY(1); }

.ls-subnav-menu-item-icon {
  flex-shrink: 0;
  width: 28px; height: 28px;
  border-radius: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: rgb(var(--ls-ink-rgb) / 0.04);
  color: var(--ls-ink-muted);
  font-size: var(--ls-fs-11);
  font-weight: 800;
}
.dark .ls-subnav-menu-item-icon { background: rgb(var(--ls-ink-soft-rgb) / 0.08) }
.ls-subnav-menu-item:hover .ls-subnav-menu-item-icon {
  background: rgb(var(--ls-accent-rgb) / 0.15);
  color: var(--ls-accent-deep);
}
.dark .ls-subnav-menu-item:hover .ls-subnav-menu-item-icon {
  background: rgb(var(--ls-accent-hi-rgb) / 0.18);
  color: var(--ls-accent-hi);
}

.ls-subnav-menu-item-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.ls-subnav-menu-item-title {
  font-size: var(--ls-menu-title-size);
  font-weight: var(--ls-menu-title-weight);
  color: var(--ls-menu-title-color);
  letter-spacing: var(--ls-menu-title-tracking);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ls-subnav-menu-item:hover .ls-subnav-menu-item-title { color: var(--ls-accent-deep); }
.dark .ls-subnav-menu-item:hover .ls-subnav-menu-item-title { color: var(--ls-accent-soft); }

/* Description line — one-sentence prose under the title in picker
   items. Replaces the earlier pill row; reads as ordinary copy
   rather than badges. Xav 2026-05-27. */
.ls-subnav-menu-item-desc {
  font-size: var(--ls-menu-blurb-size);
  font-weight: var(--ls-menu-blurb-weight);
  color: var(--ls-menu-blurb-color);
  line-height: 1.35;
  display: block;
  margin-top: 1px;
}

.ls-subnav-menu-item-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--ls-fs-10);
}
.ls-subnav-menu-item-scope {
  display: inline-flex;
  padding: 1px 5px;
  border-radius: 3px;
  background: rgb(var(--ls-accent-rgb) / 0.12);
  color: var(--ls-accent-deep);
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  font-size: var(--ls-fs-9);
}
.dark .ls-subnav-menu-item-scope {
  background: rgb(var(--ls-accent-hi-rgb) / 0.16);
  color: var(--ls-accent-soft);
}
.ls-subnav-menu-item-dim {
  font-family: ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  color: #94a3b8;
  font-weight: 600;
  font-size: var(--ls-fs-10);
}

/* Requirement pill — picker label like "needs projections". Amber
   when unmet (user attention), slate when met (informational).
   Xav 2026-05-27 metadata layer. */
.ls-subnav-menu-item-req {
  display: inline-flex;
  padding: 1px 5px;
  border-radius: 3px;
  background: rgb(var(--ls-warn-soft-rgb) / 0.14);            /* amber-500 @ 14% — UNMET */
  color: #b45309;                                  /* amber-700 */
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: lowercase;
  font-size: var(--ls-fs-9);
}
.dark .ls-subnav-menu-item-req {
  background: rgb(var(--ls-warn-soft-rgb) / 0.20);
  color: #fbbf24;
}
.ls-subnav-menu-item-req.is-met {                  /* requirement satisfied — quieter */
  background: rgb(var(--ls-ink-rgb) / 0.06);
  color: var(--ls-ink-muted);
}
.dark .ls-subnav-menu-item-req.is-met { background: rgb(var(--ls-ink-soft-rgb) / 0.10) }

/* Scale-hint pill — picker label like "best for solo" / "best at scale".
   Slate by default (informational); amber when mismatched against the
   user's current lineup count. */
.ls-subnav-menu-item-scale {
  display: inline-flex;
  padding: 1px 5px;
  border-radius: 3px;
  background: rgb(var(--ls-ink-rgb) / 0.06);
  color: #64748b;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: lowercase;
  font-size: var(--ls-fs-9);
}
.dark .ls-subnav-menu-item-scale {
  background: rgb(var(--ls-ink-soft-rgb) / 0.10);
  color: #94a3b8;
}
.ls-subnav-menu-item-scale.is-mismatch {           /* user count doesn't fit this scale */
  background: rgb(var(--ls-warn-soft-rgb) / 0.14);
  color: #b45309;
}
.dark .ls-subnav-menu-item-scale.is-mismatch {
  background: rgb(var(--ls-warn-soft-rgb) / 0.20);
  color: #fbbf24;
}

.ls-subnav-menu-empty {
  padding: 16px 14px;
  font-size: var(--ls-fs-11);
  font-style: italic;
  color: var(--ls-ink-soft);
  text-align: center;
}

/* Suggest-a-widget footer in the Add-widget picker (Xav 2026-06-14). */
.ls-subnav-menu-suggest {
  margin-top: 4px;
  padding-top: 6px;
  border-top: 1px solid rgb(var(--ls-ink-rgb) / 0.06);
}
.dark .ls-subnav-menu-suggest { border-top-color: rgb(var(--ls-ink-soft-rgb) / 0.08); }
.ls-subnav-menu-suggest-btn {
  width: 100%;
  text-align: left;
  padding: 8px 12px;
  border-radius: 8px;
  font-size: var(--ls-fs-11);
  font-weight: 600;
  color: var(--ls-ink-muted);
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.ls-subnav-menu-suggest-btn span { color: var(--ls-accent-deep); font-weight: 700; }
.ls-subnav-menu-suggest-btn:hover { background: rgb(var(--ls-accent-rgb) / 0.08); color: var(--ls-ink); }
.dark .ls-subnav-menu-suggest-btn span { color: var(--ls-accent-soft); }
.dark .ls-subnav-menu-suggest-btn:hover { background: rgb(var(--ls-accent-rgb) / 0.12) }

.ls-subnav-menu-footer {
  margin-top: 4px;
  padding: 8px 12px 6px;
  border-top: 1px solid rgb(var(--ls-ink-rgb) / 0.06);
}
.dark .ls-subnav-menu-footer { border-top-color: rgb(var(--ls-ink-soft-rgb) / 0.08); }
.ls-subnav-menu-foot-link {
  font-size: var(--ls-fs-10);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ls-ink-muted);
  text-decoration: none;
  transition: color 120ms;
}
.ls-subnav-menu-foot-link:hover { color: var(--ls-accent-deep); }
.dark .ls-subnav-menu-foot-link:hover { color: var(--ls-accent-hi); }

/* Section header within a dropdown — like 'Game', 'Player', 'Form',
   'Preferences', 'Admin'. Subtle eyebrow above a group of rows. */
/* Consumes the canonical --ls-menu-section-* tokens — see
   style.css :root. Matches the Mode picker's section header byte
   for byte. (The previous emerald pill background was the visual
   outlier across menus, which is why Settings looked different to
   the Dashboard picker — Xav 2026-06-06.) */
.ls-subnav-menu-section {
  margin: 8px 0 4px;
  padding: var(--ls-menu-section-padding);
  font-size: var(--ls-menu-section-size);
  font-weight: var(--ls-menu-section-weight);
  text-transform: uppercase;
  letter-spacing: var(--ls-menu-section-tracking);
  color: var(--ls-menu-section-color);
  background: var(--ls-menu-section-bg);
}
/* No dark override needed — bg is now `transparent` via the
   canonical token, applies in both themes. */

/* Legend row — colored dot/emoji + label, used by the Legend dropdown. */
.ls-subnav-menu-legend-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 14px;
  font-size: var(--ls-fs-11);
  color: #475569;
}
.dark .ls-subnav-menu-legend-row { color: #cbd5e1; }
.ls-subnav-legend-dot {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 999px;
  flex-shrink: 0;
}
.ls-subnav-legend-emoji {
  display: inline-flex;
  width: 8px;
  font-size: var(--ls-fs-11);
  line-height: 1;
  align-items: center;
  justify-content: center;
}

/* Toggle row — used inside Settings for the preference + admin toggles.
   Label on the left, ON/OFF state pill on the right. */
.ls-subnav-menu-toggle {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: var(--ls-menu-row-padding);
  margin: 1px 0;
  background: transparent;
  border: none;
  border-radius: var(--ls-menu-row-radius);
  cursor: pointer;
  text-align: left;
  transition: background 140ms ease-out;
}
.ls-subnav-menu-toggle:hover { background: var(--ls-menu-row-hover-bg); }
.ls-subnav-menu-toggle-label {
  font-size: var(--ls-menu-title-size);
  font-weight: var(--ls-menu-title-weight);
  color: var(--ls-menu-title-color);
  letter-spacing: var(--ls-menu-title-tracking);
  flex: 1;
  min-width: 0;
}
/* Keyboard-hint chip inside a toggle label (e.g. "TV mode  ⇧T").
   Same visual language as the .ls-tv-exit-kbd chip. Quiet — it's a
   discoverability nudge, not the primary action. */
.ls-subnav-menu-toggle-kbd {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 5px;
  border-radius: 3px;
  background: rgb(var(--ls-ink-rgb) / 0.08);
  font-family: ui-monospace, monospace;
  font-size: var(--ls-fs-9);
  font-weight: 600;
  color: #64748b;
  letter-spacing: 0.04em;
}
.dark .ls-subnav-menu-toggle-kbd {
  background: rgb(var(--ls-surface-rgb) / 0.10);
  color: #94a3b8;
}
.ls-subnav-menu-toggle-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px; height: 24px;
  color: var(--ls-ink-muted);
  flex-shrink: 0;
}
.ls-subnav-menu-toggle-state {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: var(--ls-menu-state-size);
  font-weight: var(--ls-menu-state-weight);
  letter-spacing: var(--ls-menu-state-tracking);
  background: rgb(var(--ls-ink-soft-rgb) / 0.18);
  color: #64748b;
}
.ls-subnav-menu-toggle-state.is-on {
  background: rgb(var(--ls-accent-rgb) / 0.18);
  color: var(--ls-accent-deep);
}
.dark .ls-subnav-menu-toggle-state.is-on { color: var(--ls-accent-soft); }

/* ── Subnav menu canonical variants (Xav 2026-06-06 chrome pass) ─────
   Anywhere a subnav menu currently freelances with inline Tailwind
   for warning/alert/demo affordances, use these variants instead. */

/* Toggle row variants. The base .ls-subnav-menu-toggle uses neutral
   slate; --alert tints the label amber for destructive-ish admin
   actions (force refresh), --demo tints violet for replay entries. */
.ls-subnav-menu-toggle--alert .ls-subnav-menu-toggle-label {
  color: #b45309;
}
.dark .ls-subnav-menu-toggle--alert .ls-subnav-menu-toggle-label {
  color: #fbbf24;
}
.ls-subnav-menu-toggle--demo .ls-subnav-menu-toggle-label {
  color: #7c3aed;
}
.dark .ls-subnav-menu-toggle--demo .ls-subnav-menu-toggle-label {
  color: #c4b5fd;
}
.ls-subnav-menu-toggle--demo.is-stopping .ls-subnav-menu-toggle-label {
  animation: ls-subnav-pulse 1.4s ease-in-out infinite;
}
@keyframes ls-subnav-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.55; }
}

/* Demo platform/sport eyebrow on multi-line entries */
.ls-subnav-menu-toggle-eyebrow {
  display: block;
  font-size: var(--ls-fs-9);
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  opacity: 0.70;
  line-height: 1.4;
}

/* Utility strip at the top of a picker (KPI count + warning) */
.ls-subnav-menu-utility {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 10px 14px 6px;
}
.ls-subnav-menu-utility-count {
  font-family: ui-monospace, monospace;
  font-size: var(--ls-fs-10);
  font-weight: 700;
  color: var(--ls-ink-soft);
  letter-spacing: 0.02em;
}
.ls-subnav-menu-utility-count.is-at-cap { color: #b45309; }
.dark .ls-subnav-menu-utility-count.is-at-cap { color: #fbbf24; }

/* Generic warning text used inside menu sections */
.ls-subnav-menu-warning {
  font-size: var(--ls-fs-10);
  font-weight: 500;
  color: #b45309;
  line-height: 1.4;
}
.dark .ls-subnav-menu-warning { color: #fcd34d; }

/* Slate-picker row — wraps a slate_badge partial. Distinct from
   .ls-subnav-menu-item (which is for the simpler icon + title rows
   used by the KPI/panel pickers) because the badge is a richer
   structure already styled by the partial. We just need hover +
   active chrome around it. */
.ls-subnav-menu-slate-row {
  padding: 6px 8px;
  margin: 2px 4px;
  border-radius: 8px;
  cursor: pointer;
  transition: background 120ms;
}
.ls-subnav-menu-slate-row:hover {
  background: rgb(var(--ls-ink-rgb) / 0.04);
}
.dark .ls-subnav-menu-slate-row:hover {
  background: rgb(var(--ls-ink-soft-rgb) / 0.08);
}
.ls-subnav-menu-slate-row.is-active {
  background: rgb(var(--ls-accent-rgb) / 0.07);
}
.dark .ls-subnav-menu-slate-row.is-active {
  background: rgb(var(--ls-accent-hi-rgb) / 0.10);
}

/* Footer link — bottom-of-menu action like "Go to Slates →".
   Subtler than the .ls-mode-footer-action emerald CTA; this is a
   "see more, go elsewhere" affordance not a primary action. */
.ls-subnav-menu-footer-link {
  display: block;
  margin: 6px 8px 4px;
  padding: 6px 8px;
  border-top: 1px solid rgb(var(--ls-ink-rgb) / 0.06);
  padding-top: 8px;
  font-size: var(--ls-fs-10);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #94a3b8;
  text-decoration: none;
  transition: color 120ms;
}
.dark .ls-subnav-menu-footer-link {
  border-top-color: rgb(var(--ls-ink-soft-rgb) / 0.10);
  color: #64748b;
}
.ls-subnav-menu-footer-link:hover { color: var(--ls-accent); }
.dark .ls-subnav-menu-footer-link:hover { color: var(--ls-accent-hi); }

/* Pill row — used by Settings → Stakes filter (and any future
   pill-grid section). Replaces the freelance Tailwind utility
   classes that previously rendered the stakes chips. */
.ls-subnav-menu-pill-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 4px 14px 10px;
}
.ls-subnav-menu-pill {
  font-size: var(--ls-fs-10);
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: 3px 8px;
  border-radius: 9999px;
  border: 1px solid rgb(var(--ls-ink-rgb) / 0.10);
  background: #ffffff;
  color: var(--ls-ink-soft);
  cursor: pointer;
  transition: background 120ms, color 120ms, border-color 120ms;
}
.dark .ls-subnav-menu-pill { border-color: rgb(var(--ls-ink-soft-rgb) / 0.18);
  background: #1e293b }
.ls-subnav-menu-pill:hover { opacity: 0.85; }
.ls-subnav-menu-pill.is-active {
  background: rgb(var(--ls-accent-rgb) / 0.10);
  color: var(--ls-accent-deep);
  border-color: rgb(var(--ls-accent-rgb) / 0.45);
}
.dark .ls-subnav-menu-pill.is-active {
  background: rgb(var(--ls-accent-hi-rgb) / 0.14);
  color: var(--ls-accent-soft);
  border-color: rgb(var(--ls-accent-hi-rgb) / 0.40);
}
.ls-row2-kpis {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.ls-row2-personal {
  flex: 0 0 280px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 6px;
  padding: 10px 14px;
  border-radius: 12px;
  border: 1px solid rgba(0, 0, 0, 0.08);
  background: linear-gradient(135deg, rgb(var(--ls-accent-rgb) / 0.04), rgb(var(--ls-surface-rgb) / 0));
}
.dark .ls-row2-personal {
  border-color: rgb(var(--ls-surface-rgb) / 0.06);
  background: linear-gradient(135deg, rgb(var(--ls-accent-rgb) / 0.08), rgb(var(--ls-ink-rgb) / 0));
}
.ls-h-row-1 { display: flex; align-items: baseline; gap: 8px; }
.ls-h-row-2 { display: flex; flex-wrap: wrap; gap: 4px 6px; align-items: center; }
.ls-h-big {
  font-size: var(--ls-fs-32);
  font-weight: 900;
  line-height: 1;
  color: var(--ls-accent-hover);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}
.dark .ls-h-big { color: var(--ls-accent-hi); }
.ls-h-label {
  font-size: var(--ls-fs-9);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: #94a3b8;
}
.ls-h-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 7px;
  border-radius: 999px;
  font-size: var(--ls-fs-10);
  font-weight: 600;
  color: #475569;
  background: rgb(var(--ls-ink-rgb) / 0.04);
  white-space: nowrap;
}
.dark .ls-h-chip { color: #cbd5e1; background: rgb(var(--ls-surface-rgb) / 0.05); }
.ls-h-chip-icon { font-size: 8px; color: var(--ls-accent); }
.ls-h-chip-suffix { font-size: 8px; text-transform: uppercase; letter-spacing: 0.08em; color: #94a3b8; }
.ls-h-chip-num { font-variant-numeric: tabular-nums; font-weight: 700; }
.ls-h-chip-hot { background: rgb(var(--ls-hot-rgb) / 0.1); color: var(--ls-hot-deep); }
.dark .ls-h-chip-hot { background: rgb(var(--ls-hot-rgb) / 0.15); color: var(--ls-hot-soft); }
.ls-h-chip-warn { background: rgba(244, 63, 94, 0.1); color: #be123c; }
.dark .ls-h-chip-warn { background: rgba(244, 63, 94, 0.15); color: #fda4af; }
.ls-h-chip-muted { color: #94a3b8; background: transparent; }

/* Graphs modal — shared chart chrome.
   ls-chart-head    — title + right-aligned context stat (Top / Total / etc)
   ls-chart-stat    — the right-aligned summary pill
   ls-chart-row     — every data row across every chart. Hover state
                      shows an emerald accent + slight inset shadow.
   The row hover is the single biggest "designed" cue across the modal —
   makes every chart feel interactive, not static. */
.ls-chart-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 12px;
}
.ls-chart-stat {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 999px;
  background: rgb(var(--ls-accent-rgb) / 0.08);
  border: 1px solid rgb(var(--ls-accent-rgb) / 0.18);
  font-size: var(--ls-fs-10);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ls-accent-deep);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
}
.dark .ls-chart-stat {
  background: rgb(var(--ls-accent-rgb) / 0.10);
  border-color: rgb(var(--ls-accent-rgb) / 0.30);
  color: var(--ls-accent-hi);
}
.ls-chart-stat-val {
  font-size: var(--ls-fs-13);
  font-weight: 900;
  letter-spacing: 0;
  color: #065f46;
}
.dark .ls-chart-stat-val { color: var(--ls-accent-soft); }
.ls-chart-row {
  position: relative;
  padding: 4px 6px 4px 12px;
  margin: 0 -6px;
  border-radius: 8px;
  transition: background 160ms ease-out,
              transform   160ms ease-out,
              box-shadow  160ms ease-out;
}
/* Persistent left accent on every row — readable even before hover so
   the structure is visible at rest. Hover bumps the accent emerald and
   tints the row. (Fixes Xav 2026-05-29 feedback: "feels like it looks
   exactly the same" — the polish was firing only on interaction.) */
.ls-chart-row::before {
  content: "";
  position: absolute;
  left: 0; top: 6px; bottom: 6px;
  width: 2px;
  background: rgb(var(--ls-ink-rgb) / 0.08);
  border-radius: 999px;
  transition: background 160ms ease-out, width 160ms ease-out;
}
.dark .ls-chart-row::before { background: rgb(var(--ls-ink-soft-rgb) / 0.16); }
.ls-chart-row:hover {
  background: linear-gradient(90deg,
    rgb(var(--ls-accent-rgb) / 0.08) 0%,
    rgb(var(--ls-accent-rgb) / 0.02) 100%);
  transform: translateX(2px);
  box-shadow: inset 0 0 0 1px rgb(var(--ls-accent-rgb) / 0.06);
}
.ls-chart-row:hover::before {
  background: var(--ls-accent);
  width: 3px;
}
.dark .ls-chart-row:hover {
  background: linear-gradient(90deg,
    rgb(var(--ls-accent-rgb) / 0.12) 0%,
    rgb(var(--ls-accent-rgb) / 0.04) 100%);
  box-shadow: inset 0 0 0 1px rgb(var(--ls-accent-rgb) / 0.10);
}

/* Zebra striping on chart rows so adjacent rows read as discrete even
   without hovering. The :nth-child(even) bg is the lightest tint that
   still reads — anything stronger competes with the bar fills. */
.ls-chart-row:nth-child(even) {
  background-color: rgb(var(--ls-ink-rgb) / 0.018);
}
.dark .ls-chart-row:nth-child(even) {
  background-color: rgb(var(--ls-ink-soft-rgb) / 0.04);
}
/* Hover wins over zebra (more specific via the explicit linear-gradient
   above, but make sure by repeating the override). */
.ls-chart-row:nth-child(even):hover {
  background-image: linear-gradient(90deg,
    rgb(var(--ls-accent-rgb) / 0.08) 0%,
    rgb(var(--ls-accent-rgb) / 0.02) 100%);
}
.dark .ls-chart-row:nth-child(even):hover {
  background-image: linear-gradient(90deg,
    rgb(var(--ls-accent-rgb) / 0.12) 0%,
    rgb(var(--ls-accent-rgb) / 0.04) 100%);
}

/* Player Heat Map — players × lineups grid in the Graphs modal. Each
   cell's background is set inline via a JS heatColor() helper on the
   parent controller (graphPlayerHeatmap), so CSS only handles layout +
   typography. Sticky first column so the player name stays visible
   while you scroll horizontally through dozens of lineups. */
.ls-heatmap-table {
  border-collapse: collapse;
  font-size: var(--ls-fs-11);
  font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
  width: max-content;
  min-width: 100%;
}
.ls-heatmap-table th,
.ls-heatmap-table td {
  border: 1px solid rgb(var(--ls-ink-rgb) / 0.06);
  padding: 4px 6px;
  text-align: center;
  white-space: nowrap;
  line-height: 1;
}
.dark .ls-heatmap-table th,
.dark .ls-heatmap-table td {
  border-color: rgb(var(--ls-ink-soft-rgb) / 0.10);
}
.ls-heatmap-corner,
.ls-heatmap-row-head {
  position: sticky; left: 0; z-index: 2;
  background: #ffffff;
  text-align: left;
  min-width: 140px;
  font-weight: 700;
  font-family: ui-sans-serif, system-ui, sans-serif;
}
.dark .ls-heatmap-corner,
.dark .ls-heatmap-row-head { background: #0f172a; }
.ls-heatmap-col-head {
  font-size: var(--ls-fs-9);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ls-ink-muted);
  padding: 4px 6px;
  min-width: 52px;
}
.ls-heatmap-col-head span { vertical-align: middle; margin-right: 3px; }
.ls-heatmap-cell {
  min-width: 52px;
  color: var(--ls-ink);
  font-weight: 600;
}
/* Dense mode — switch on when the matrix has >25 lineups. Cells shrink
   to 8px wide pixel-art tiles; numbers are hidden (only the colour
   carries signal at this density), but hovering a cell still surfaces
   the exact value via the existing `title` attribute. (Xav MME audit
   2026-05-29.) */
.ls-heatmap-table.is-dense .ls-heatmap-col-head { min-width: 14px; padding: 4px 2px; font-size: 7px; }
.ls-heatmap-table.is-dense .ls-heatmap-col-head span:first-child { margin-right: 0; }
.ls-heatmap-table.is-dense .ls-heatmap-col-head span:last-child  { display: none; }
.ls-heatmap-table.is-dense .ls-heatmap-cell {
  min-width: 8px;
  padding: 4px 2px;
  font-size: 0;                 /* hide cell number — colour does the work */
}
.ls-heatmap-tot-head,
.ls-heatmap-tot {
  font-weight: 800;
  background: #f8fafc;
  position: sticky; right: 0; z-index: 1;
}
.dark .ls-heatmap-tot-head,
.dark .ls-heatmap-tot { background: #111c33; }
.ls-heatmap-tot-head {
  font-size: var(--ls-fs-9);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ls-ink-muted);
}

/* Preset segmented control — Simple / Analytical / TV pills inside the
   sub-nav. Same visual family as the Matches filter pills (rounded-full
   group with active emerald fill) so the LIVE chrome stays coherent.
   See dashboard-presets memory for the architecture rationale. */
.ls-preset-group {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border-radius: 999px;
  background: rgb(var(--ls-accent-rgb) / 0.10);
  border: 1px solid rgb(var(--ls-accent-rgb) / 0.22);
  margin: 0 0.5rem;
  flex-shrink: 0;
}
.dark .ls-preset-group {
  background: rgb(var(--ls-accent-rgb) / 0.12);
  border-color: rgb(var(--ls-accent-rgb) / 0.28);
}
.ls-preset-pill {
  padding: 4px 12px;
  font-size: var(--ls-fs-11);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgb(var(--ls-ink-rgb) / 0.55);
  border-radius: 999px;
  transition: background 140ms ease-out, color 140ms ease-out;
  cursor: pointer;
  border: 0;
  background: transparent;
  white-space: nowrap;
  line-height: 1;
}
.ls-preset-pill:hover { color: var(--ls-accent); }
.ls-preset-pill.is-active {
  background: var(--ls-accent);
  color: #ffffff;
}
.ls-preset-pill.is-active:hover { color: #ffffff; }
.dark .ls-preset-pill         { color: rgb(var(--ls-surface-rgb) / 0.50); }
.dark .ls-preset-pill:hover   { color: var(--ls-accent-hi); }
.dark .ls-preset-pill.is-active        { background: var(--ls-accent); color: #ffffff; }
.dark .ls-preset-pill.is-active:hover  { color: #ffffff; }

/* Compact variant used by the Graphs modal section tabs — small,
   tight, low-weight when inactive so the modal chrome whispers
   rather than shouts (Xav 2026-05-29: "looks clunky, not premium"). */
.ls-preset-group--sm           { padding: 2px; gap: 0; }
.ls-preset-group--sm .ls-preset-pill {
  padding: 2px 8px;
  font-size: var(--ls-fs-9);
  font-weight: 600;
  letter-spacing: 0.02em;
}
.ls-preset-group--sm .ls-preset-pill.is-active { font-weight: 700; }

/* On phone the sub-nav scrolls horizontally; the preset pill group is
   compact enough to stay visible alongside the slate picker — no special
   collapse rules needed at this width. */

/* Number tween change pulse — fires for ~800ms after a value tick.
   Background flash + slight scale bump so the eye catches the change.
   Inline-block + padding so the flash has a hit area on inline numbers. */
/* --pulse-rgb lets the factory pick the flash colour by DIRECTION: a rise
   flashes accent (good), a drop flashes neutral slate (changed, not a gain)
   — set on the element by LSTween.num(), defaults to accent so anything not
   wired stays green. A benching / negative pitcher line no longer flashes
   the same celebratory green as a score (v4 KPI audit). */
@keyframes ls-tween-pulse-anim {
  0%   { background: rgb(var(--pulse-rgb, var(--ls-accent-rgb)) / 0.00); transform: scale(1);    }
  20%  { background: rgb(var(--pulse-rgb, var(--ls-accent-rgb)) / 0.20); transform: scale(1.06); }
  100% { background: rgb(var(--pulse-rgb, var(--ls-accent-rgb)) / 0.00); transform: scale(1);    }
}
.ls-tween-pulse {
  display: inline-block;
  border-radius: 6px;
  padding: 0 4px;
  margin: 0 -4px;
  animation: ls-tween-pulse-anim 800ms ease-out;
  will-change: background, transform;
}

/* Card glow — fires on DISCRETE events (HR scored, status flip, identity
   swap), distinct from the continuous tween pulse. Halo radiates outward
   then fades; applied on .ls-kpi-card and .mlb-card so the user can spot
   the changed tile across the grid. */
@keyframes ls-card-glow-anim {
  0%   { box-shadow:
           0 0 0 0 rgb(var(--ls-accent-rgb) / 0.00),
           0 0 0 0 rgb(var(--ls-accent-rgb) / 0.00); }
  18%  { box-shadow:
           0 0 0 2px rgb(var(--ls-accent-rgb) / 0.55),
           0 0 30px 8px rgb(var(--ls-accent-rgb) / 0.45); }
  100% { box-shadow:
           0 0 0 0 rgb(var(--ls-accent-rgb) / 0.00),
           0 0 0 0 rgb(var(--ls-accent-rgb) / 0.00); }
}
.ls-card-glow {
  animation: ls-card-glow-anim 1500ms ease-out;
  will-change: box-shadow;
}
/* Hot variant — genuinely "hot" tiles (HRs, hot hitters) flare orange, not
   accent green, so a home run doesn't celebrate in the good/positive hue.
   Applied per-tile: my_bombs, cooking, hottest_player (audit v5, Colour). */
@keyframes ls-card-glow-hot-anim {
  0%   { box-shadow: 0 0 0 0 rgb(var(--ls-hot-rgb) / 0), 0 0 0 0 rgb(var(--ls-hot-rgb) / 0); }
  18%  { box-shadow: 0 0 0 2px rgb(var(--ls-hot-rgb) / 0.55), 0 0 30px 8px rgb(var(--ls-hot-rgb) / 0.45); }
  100% { box-shadow: 0 0 0 0 rgb(var(--ls-hot-rgb) / 0), 0 0 0 0 rgb(var(--ls-hot-rgb) / 0); }
}
.ls-card-glow--hot {
  animation: ls-card-glow-hot-anim 1500ms ease-out;
  will-change: box-shadow;
}

/* ── Player avatar ─────────────────────────────────────────────────────
   Initials chip by default (always visible via ::before). When
   p.player_photo is truthy the partial renders an <img alt=""> that stacks
   on top via z-index. img @error removes itself so the initials chip
   shows through if the URL 404s. Sized per widget via the .ls-av-* size
   modifier; default 28px. */
.ls-av {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: linear-gradient(135deg, #e2e8f0, #cbd5e1);
  color: #475569;
  font-size: var(--ls-fs-10);
  font-weight: 800;
  letter-spacing: 0.02em;
  flex-shrink: 0;
  overflow: hidden;
  position: relative;
  user-select: none;
}
.dark .ls-av { background: linear-gradient(135deg, #334155, #1e293b); color: #cbd5e1; }
.ls-av::before { content: attr(data-initials); }
.ls-av img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 1;
}
.ls-av-lg { width: 36px; height: 36px; font-size: var(--ls-fs-12); }
.ls-av-sm { width: 22px; height: 22px; font-size: var(--ls-fs-9); }

/* Phone: stack the two halves; scoreboard cards become a swipeable
   carousel (one game card snaps per page). Personal strip drops the
   fixed width and lets the big number breathe on its own row. */
@media (max-width: 767px) {
  /* Row 1 (scoreboard) stays full-width. NB: the old swipeable-carousel
     rules that lived here (overflow-x:auto + scroll-snap on
     .ls-headline-scoreboard) were dead — the later block below forces
     `overflow: visible` and .ls-sb stacks the cards in a column on phone.
     Removed so the next edit doesn't resurrect the snap conflict (v4 audit). */

  /* Row 2 stacks: picker (full width) → KPIs wrap → personal strip. */
  .ls-row2 { flex-direction: column; gap: 0.4rem; }
  .ls-row2-picker   { flex: 0 0 auto; }
  .ls-row2-kpis     { flex: 1 1 auto; }
  .ls-row2-personal { flex: 0 0 auto; padding: 8px 12px; }
  .ls-h-big         { font-size: var(--ls-fs-28); }
}

/* ── KPI Strip (Row 2) ────────────────────────────────────────────────
   Horizontal flex of compact info tiles below the headline. Tiles auto-
   distribute equal width. Each tile is a fixed-height card with label,
   big value, and optional footnote. Hover reveals a small × to remove.
   Below md the strip wraps to 2 cols (then 1 col under 480px). */
.ls-kpi-strip {
  display: flex;
  gap: 0.5rem;
  margin-top: 10px;                 /* room for floating KPI label pills */
  margin-bottom: 0.5rem;
  flex-wrap: wrap;
}
.ls-kpi-slot {
  flex: 1 1 0;
  min-width: 160px;
  position: relative;
}
/* KPI card — minimal chrome, hero number. Layout is straight column-
   center alignment: tiny label, huge number, tiny foot. Surface stays
   quiet (subtle gradient + soft shadow); the number is the entire show. */
.ls-kpi-card {
  position: relative;
  height: 100%;
  /* Top padding clears the floating label pill that sits on the top
     border. Same trick as .mlb-card (Xav 2026-05-27). */
  padding: 18px 12px 10px;
  border-radius: 14px;
  border: 1px solid rgb(var(--ls-ink-rgb) / 0.06);
  background: linear-gradient(180deg, #ffffff 0%, #f7f9fb 100%);
  box-shadow:
    0 1px 2px rgb(var(--ls-ink-rgb) / 0.04),
    0 8px 20px -8px rgb(var(--ls-ink-rgb) / 0.10);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 92px;
  text-align: center;
  overflow: visible;                /* let the label pill overhang the top border */
  transition:
    transform 200ms cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 200ms ease-out,
    border-color 200ms ease-out;
}
.dark .ls-kpi-card {
  border-color: rgb(var(--ls-ink-soft-rgb) / 0.08);
  background: linear-gradient(180deg, #111c33 0%, #0f172a 100%);
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.35),
    0 8px 20px -8px rgba(0, 0, 0, 0.5);
}
.ls-kpi-slot:hover .ls-kpi-card {
  transform: translateY(-1px);
  box-shadow:
    0 1px 2px rgb(var(--ls-ink-rgb) / 0.04),
    0 12px 24px -8px rgb(var(--ls-accent-rgb) / 0.20);
  border-color: rgb(var(--ls-accent-rgb) / 0.28);
}

/* Floating label pill — sits ON the card's top border, centered. Same
   "title cuts the border" pattern as .mlb-status / the SCOREBOARD frame
   title. Solid card-matched background so the pill looks like the card's
   chrome extending UP through the border line. Card padding-top + strip
   margin-top were bumped above so the pill has room. */
.ls-kpi-label {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  border-radius: 999px;
  /* KPI label — DEEPER emerald, attention-grabbing. KPIs are the
     headline status signals; the warmer/stronger tint pulls the eye
     there before the bigger panel titles. (Xav 2026-05-30: "attention
     should be on kpis" — swap the two shades.) */
  background: linear-gradient(180deg, #ffffff 0%, #d1fae5 100%); /* white → emerald-100 */
  border: 1px solid rgb(var(--ls-accent-hover-rgb) / 0.30);                     /* emerald-600 @ 30% */
  box-shadow: 0 1px 3px rgb(var(--ls-accent-hover-rgb) / 0.12);
  white-space: nowrap;
  font-size: var(--ls-fs-9);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: #065f46;                                                /* emerald-800 */
  line-height: 1;
}

/* Widget rename (Xav 2026-06-14) — the KPI wrapper owns the label pill so
   it can carry the rename UI; hide each partial's own in-card label so only
   the wrapper pill (a direct child of .ls-kpi-slot) shows. */
.ls-kpi-card .ls-kpi-label { display: none; }

/* Edit-mode rename affordance — pill reads as clickable. Shared by the KPI
   label and the panel title pill. The panel pill is pointer-events:none by
   default (so it never blocks the panel beneath) — clicks fell THROUGH to
   the draggable cell, starting a micro-drag (the flash) and never firing
   the title's @click (couldn't rename). Re-enable pointer events in edit
   mode so the click lands on the pill. (Xav 2026-06-14) */
.ls-kpi-label.is-renamable,
.ls-cell-title-pill.is-renamable { cursor: text; pointer-events: auto; }
.ls-kpi-label.is-renamable:hover,
.ls-cell-title-pill.is-renamable:hover {
  box-shadow: 0 0 0 2px rgb(var(--ls-accent-rgb) / 0.45);
}

/* Inline rename input — inherits the pill's shape/background/font from the
   pill class it shares; just swaps the focus ring + a fixed width so it
   sits where the pill did. pointer-events:auto overrides the panel pill's
   none so the field is actually typeable. */
.ls-title-input {
  outline: none;
  border: none;
  box-shadow: 0 0 0 2px rgb(var(--ls-accent-rgb) / 0.65);
  text-align: center;
  width: 150px;
  max-width: 70vw;
  pointer-events: auto;
}
.dark .ls-kpi-label {
  background: linear-gradient(180deg, #0f172a 0%, #064e3b 100%); /* slate-900 → emerald-900 */
  border-color: rgb(var(--ls-accent-rgb) / 0.36);
  box-shadow: 0 1px 3px rgb(var(--ls-accent-rgb) / 0.22);
  color: #a7f3d0;                                                /* emerald-200 */
}

/* Floating panel-title pill — same family as .ls-kpi-label. Single
   shared rule covers every panel widget; the .panel_chrome Jinja
   macro renders the markup with the title arg, so per-widget
   templates declare the name once and inherit the floating-pill
   treatment for free. Marginally larger than the KPI pill because
   panel titles are slightly longer ("Pitcher Pulse", "All Players")
   and panels themselves are bigger surfaces. (Xav 2026-05-30
   "smart programming — CSS over duplication".) */
.ls-cell-title-pill {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 5;
  display: inline-flex;
  align-items: center;
  padding: 4px 12px;
  border-radius: 999px;
  /* Panel title pill — LIGHTER emerald hint, quieter than the KPI
     label so the eye lands on KPIs first. Pills are larger surfaces
     so the subtle tint still reads at this scale. (Xav 2026-05-30:
     swap the two shades, attention on kpis.) */
  background: linear-gradient(180deg, #ecfdf5 0%, #ffffff 100%); /* emerald-50 → white */
  border: 1px solid rgb(var(--ls-accent-rgb) / 0.22);                    /* emerald-500 @ 22% */
  box-shadow: 0 1px 3px rgb(var(--ls-accent-rgb) / 0.10);
  white-space: nowrap;
  font-size: var(--ls-fs-10);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ls-accent-deep);                                                /* emerald-700 */
  line-height: 1;
  pointer-events: none;
  user-select: none;
}
.dark .ls-cell-title-pill {
  background: linear-gradient(180deg, #064e3b 0%, #0f172a 100%); /* emerald-900 → slate-900 */
  border-color: rgb(var(--ls-accent-rgb) / 0.32);
  box-shadow: 0 1px 3px rgb(var(--ls-accent-rgb) / 0.18);
  color: var(--ls-accent-soft);                                                /* emerald-300 */
}

/* ── Mini filter dropdown (Xav 2026-05-30) ───────────────────────────
   Shared family for in-panel filter pickers. Replaces wide horizontal
   pill rows (Matches All/Live/Upcoming/Ended, Vidi Mine/Big/All) that
   crowded the panel headers. Pattern: tiny trigger pill showing the
   current selection + chevron; click opens a small menu of options.
   Sized to fit comfortably next to the cog icon in the panel header. */
/* Trigger wrapper anchors absolutely at the LEFT mirror of the chrome
   cog (which sits top:10 right:14 in .ls-cell-chrome) so the two icons
   share the exact same Y baseline — flow alignment via min-h kept
   drifting by a couple of pixels. Pointer-events + z-index keep it
   clickable above the card. */
.ls-mini-filter-anchor {
  position: absolute !important;
  top: 8px !important;
  left: 14px !important;
  height: 24px !important;
  display: inline-flex !important;
  align-items: center !important;
  z-index: 6;
}
.ls-mini-filter {
  display: inline-flex !important;
  align-items: center !important;
  gap: 4px !important;
  padding: 0 !important;
  height: 24px !important;
  background: transparent !important;
  border: 0 !important;
  font-size: var(--ls-fs-10) !important;
  font-weight: 500 !important;
  letter-spacing: 0.14em !important;
  text-transform: uppercase !important;
  color: rgb(var(--ls-ink-muted-rgb) / 0.85) !important;                   /* slate-500 */
  white-space: nowrap !important;
  cursor: pointer !important;
  line-height: 1 !important;
  font-family: inherit !important;
  transition: color 160ms ease-out !important;
}
.ls-mini-filter:hover         { color: var(--ls-accent) !important; }
.ls-mini-filter.is-open       { color: var(--ls-accent-deep) !important; }
.dark .ls-mini-filter         { color: rgb(var(--ls-ink-soft-rgb) / 0.78) !important; }
.dark .ls-mini-filter:hover   { color: var(--ls-accent-hi) !important; }
.dark .ls-mini-filter.is-open { color: var(--ls-accent-soft) !important; }
.ls-mini-filter-chev {
  width: 9.6px;
  height: 9.6px;
  flex-shrink: 0;
  opacity: 0.55;
  transition: transform 160ms ease-out;
}
.ls-mini-filter-chev.is-flipped { transform: rotate(180deg); }

/* Dropdown menu — refined glassmorphism panel with hairline border,
   backdrop blur, premium hover treatment. */
.ls-mini-filter-menu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 50;
  min-width: 96px;
  padding: 4px;
  background: rgb(var(--ls-surface-rgb) / 0.96);
  backdrop-filter: blur(12px) saturate(140%);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  border: 1px solid rgb(var(--ls-ink-rgb) / 0.06);
  border-radius: 8px;
  box-shadow:
    0 12px 28px -8px rgb(var(--ls-ink-rgb) / 0.16),
    0 2px 6px -2px rgb(var(--ls-ink-rgb) / 0.08),
    inset 0 1px 0 rgb(var(--ls-surface-rgb) / 0.6);
}
.dark .ls-mini-filter-menu {
  background: rgb(var(--ls-ink-rgb) / 0.92);
  border-color: rgb(var(--ls-ink-soft-rgb) / 0.12);
  box-shadow:
    0 12px 28px -8px rgba(0, 0, 0, 0.55),
    0 2px 6px -2px rgba(0, 0, 0, 0.35),
    inset 0 1px 0 rgb(var(--ls-surface-rgb) / 0.06);
}
.ls-mini-filter-menu .ls-mini-filter-item {
  display: flex !important;
  align-items: center !important;
  width: 100% !important;
  padding: 6.4px 15.6px !important;
  border-radius: 5px !important;
  font-size: var(--ls-fs-13) !important;
  font-weight: 600 !important;
  letter-spacing: 0.14em !important;
  text-transform: uppercase !important;
  color: #64748b !important;
  background: transparent !important;
  border: 0 !important;
  cursor: pointer !important;
  text-align: left !important;
  line-height: 1 !important;
  font-family: inherit !important;
  transition: background 120ms ease-out, color 120ms ease-out !important;
}
.ls-mini-filter-menu .ls-mini-filter-item + .ls-mini-filter-item { margin-top: 1px !important; }
.ls-mini-filter-menu .ls-mini-filter-item:hover {
  background: rgb(var(--ls-accent-rgb) / 0.08) !important;
  color: var(--ls-accent-deep) !important;
}
.ls-mini-filter-menu .ls-mini-filter-item.is-active {
  background: rgb(var(--ls-accent-rgb) / 0.14) !important;
  color: #065f46 !important;
  font-weight: 700 !important;
}
.dark .ls-mini-filter-item { color: #94a3b8; }
.dark .ls-mini-filter-item:hover {
  background: rgb(var(--ls-accent-rgb) / 0.14);
  color: var(--ls-accent-soft);
}
.dark .ls-mini-filter-item.is-active {
  background: rgb(var(--ls-accent-rgb) / 0.22);
  color: #a7f3d0;
}
/* Strip the pill inside dropped modals — the modal header already
   carries the panel name, so the floating pill duplicates it. In the
   mobile stack we DO keep it: each stacked panel needs an identifier
   and the floating pill is the only thing telling you which is which
   now that inline headers are gone (Xav 2026-05-30 #4 pass). */
.ls-modal-body .ls-cell-title-pill { display: none; }

/* ─────────────────────────────────────────────────────────────────────
   .ls-rank-table — premium leaderboard table.

   Shared by the Top 10 family (top10, top10_my_players,
   top10_my_lineups). Designed to feel like the pitcher/batter pulse
   cards: soft surface, generous rhythm, emerald accent on the leader.
   Use by adding `ls-rank-table` to a <table> alongside (or replacing)
   the legacy `top10-table` / `dream-table` class.
   ────────────────────────────────────────────────────────────────── */
.ls-rank-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0 2px;
  font-size: var(--ls-fs-11);
  line-height: 1.2;
}
.ls-rank-table thead th {
  font-size: var(--ls-fs-9);
  font-family: ui-monospace, monospace;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ls-ink-soft);
  padding: 0 6px 6px;
  text-align: left;
  border-bottom: 1px solid rgb(var(--ls-ink-rgb) / 0.06);
}
.dark .ls-rank-table thead th { border-bottom-color: rgb(var(--ls-surface-rgb) / 0.06) }
.ls-rank-table thead th.is-right { text-align: right; }
.ls-rank-table thead th.is-center { text-align: center; }

.ls-rank-table tbody tr {
  background: rgb(var(--ls-ink-rgb) / 0.02);
  transition: background 160ms ease-out, transform 160ms ease-out;
}
.dark .ls-rank-table tbody tr { background: rgb(var(--ls-surface-rgb) / 0.02); }
.ls-rank-table tbody tr:hover {
  background: rgb(var(--ls-accent-rgb) / 0.06);
}
.dark .ls-rank-table tbody tr:hover { background: rgb(var(--ls-accent-rgb) / 0.10); }

/* Leader row — soft emerald wash + a thin left accent bar. The bar
   comes from a box-shadow inset so we don't need a pseudo or an extra
   cell. */
.ls-rank-table tbody tr.is-leader {
  background: linear-gradient(90deg, rgb(var(--ls-accent-rgb) / 0.12), rgb(var(--ls-accent-rgb) / 0.04) 60%, transparent);
  box-shadow: inset 3px 0 0 0 var(--ls-accent);
}
.dark .ls-rank-table tbody tr.is-leader {
  background: linear-gradient(90deg, rgb(var(--ls-accent-rgb) / 0.18), rgb(var(--ls-accent-rgb) / 0.05) 60%, transparent);
}

.ls-rank-table tbody td {
  padding: 5px 6px;
  vertical-align: middle;
  border-top: 1px solid rgb(var(--ls-ink-rgb) / 0.04);
  border-bottom: 1px solid rgb(var(--ls-ink-rgb) / 0.04);
}
.dark .ls-rank-table tbody td {
  border-top-color: rgb(var(--ls-surface-rgb) / 0.03);
  border-bottom-color: rgb(var(--ls-surface-rgb) / 0.03);
}
.ls-rank-table tbody td:first-child { border-left: 1px solid rgb(var(--ls-ink-rgb) / 0.04); border-top-left-radius: 8px; border-bottom-left-radius: 8px; padding-left: 8px; }
.ls-rank-table tbody td:last-child  { border-right: 1px solid rgb(var(--ls-ink-rgb) / 0.04); border-top-right-radius: 8px; border-bottom-right-radius: 8px; padding-right: 10px; }
.dark .ls-rank-table tbody td:first-child { border-left-color: rgb(var(--ls-surface-rgb) / 0.03); }
.dark .ls-rank-table tbody td:last-child  { border-right-color: rgb(var(--ls-surface-rgb) / 0.03); }

/* Cell roles — apply with utility classes on <td>. */
.ls-rank-cell-rank {
  font-family: ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  font-size: var(--ls-fs-11);
  color: #94a3b8;
  text-align: right;
  white-space: nowrap;
  width: 1%;
}
.dark .ls-rank-cell-rank { color: #64748b; }
.is-leader .ls-rank-cell-rank { color: var(--ls-accent-hover); font-weight: 900; }
.dark .is-leader .ls-rank-cell-rank { color: var(--ls-accent-hi); }

.ls-rank-cell-pos {
  font-family: ui-monospace, monospace;
  font-size: var(--ls-fs-9);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ls-ink-soft);
  white-space: nowrap;
  width: 1%;
}

.ls-rank-cell-name {
  font-size: var(--ls-fs-12);
  font-weight: 700;
  color: var(--ls-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 0;
}
.is-leader .ls-rank-cell-name { color: #064e3b; }
.dark .is-leader .ls-rank-cell-name { color: #ffffff; }

.ls-rank-cell-team {
  text-align: center;
  white-space: nowrap;
  width: 1%;
}
.ls-rank-cell-fpts {
  font-family: ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  font-weight: 900;
  font-size: var(--ls-fs-13);
  text-align: right;
  white-space: nowrap;
  width: 1%;
  color: var(--ls-accent-hover);              /* live FPts is green everywhere (panels) */
}
.dark .ls-rank-cell-fpts { color: var(--ls-accent-hi); }

.ls-rank-cell-num {
  font-family: ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  font-size: var(--ls-fs-11);
  text-align: right;
  white-space: nowrap;
  width: 1%;
  color: var(--ls-ink-soft);
}

/* ── Stretch-to-fit (.ls-rank-fill) ──────────────────────────────────
   Leaderboard panels used to render 10 fixed ~28px rows and leave the
   bottom half of a tall cell as dead white card. The fix is native:
   a table with height:100% distributes surplus height across its rows,
   so 10 rows always fill the panel exactly — broadcast-leaderboard
   style. Height-based container queries then scale the type up in two
   steps so a stretched table reads as deliberate, not zoomed.

   Apply .ls-rank-fill to the scroll wrapper around a .ls-rank-table
   (top10 family + dreamteam). container-type:size is safe here: the
   wrapper is flex-1 inside a definite-height widget card, so its size
   never depends on content. */
.ls-rank-fill { container-type: size; container-name: rankpanel; }
/* Rows size by PANEL HEIGHT (via the @container tiers below), not by
   dividing the panel among however many rows there are. Was height:100%
   which made a single lineup balloon to fill the whole panel (Xav
   2026-06-14). Top-aligned; sparse leaderboards leave space below rather
   than stretching one card silly-tall. */
.ls-rank-fill .ls-rank-table { height: auto; }
.ls-rank-fill .ls-rank-table tbody td { padding-top: 5px; padding-bottom: 5px; }

/* Row-card inversion — the leaderboard well gets a soft tint and each
   row becomes a discrete white card with its own shadow. This is what
   makes a tall stretched panel read as a designed leaderboard instead
   of spread-out table text on a white void. */
.ls-rank-fill {
  background: linear-gradient(180deg, rgb(var(--ls-ink-rgb) / 0.030) 0%, rgb(var(--ls-ink-rgb) / 0.055) 100%);
  border-radius: 10px;
}
.dark .ls-rank-fill {
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.16) 0%, rgba(0, 0, 0, 0.28) 100%);
}
.ls-rank-fill .ls-rank-table { border-spacing: 0 3px; }
.ls-rank-fill .ls-rank-table thead th { border-bottom: none; }
.ls-rank-fill .ls-rank-table tbody tr {
  background: #ffffff;
  box-shadow: 0 1px 2px rgb(var(--ls-ink-rgb) / 0.06);
}
.dark .ls-rank-fill .ls-rank-table tbody tr {
  background: rgba(30, 41, 59, 0.85);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}
.ls-rank-fill .ls-rank-table tbody tr:hover {
  background: linear-gradient(90deg, rgb(var(--ls-accent-rgb) / 0.07), #ffffff 65%);
}
.dark .ls-rank-fill .ls-rank-table tbody tr:hover {
  background: linear-gradient(90deg, rgb(var(--ls-accent-rgb) / 0.14), rgba(30, 41, 59, 0.85) 65%);
}
.ls-rank-fill .ls-rank-table tbody tr.is-leader {
  background: linear-gradient(90deg, rgb(var(--ls-accent-rgb) / 0.16), rgb(var(--ls-accent-rgb) / 0.05) 55%, #ffffff);
  box-shadow: inset 3px 0 0 0 var(--ls-accent), 0 1px 3px rgb(var(--ls-accent-rgb) / 0.18);
}
.dark .ls-rank-fill .ls-rank-table tbody tr.is-leader {
  background: linear-gradient(90deg, rgb(var(--ls-accent-rgb) / 0.22), rgb(var(--ls-accent-rgb) / 0.07) 55%, rgba(30, 41, 59, 0.85));
  box-shadow: inset 3px 0 0 0 var(--ls-accent), 0 1px 3px rgb(var(--ls-accent-rgb) / 0.25);
}

/* Pre-slate proj column — when nothing has scored yet the leaderboards
   rank by projection; the value column flips blue to say "this is a
   projection, not points". */
.ls-rank-cell-fpts.is-proj { color: var(--ls-proj); }
.dark .ls-rank-cell-fpts.is-proj { color: var(--ls-proj-hi); }
.is-leader .ls-rank-cell-fpts.is-proj { color: #2563eb; }
.dark .is-leader .ls-rank-cell-fpts.is-proj { color: #bfdbfe; }

@container rankpanel (min-height: 400px) {
  .ls-rank-table { border-spacing: 0 4px !important; }
  .ls-rank-table tbody td { padding-top: 6px; padding-bottom: 6px; }
  .ls-rank-cell-rank { font-size: var(--ls-fs-14); }
  .ls-rank-cell-pos  { font-size: var(--ls-fs-10); }
  .ls-rank-cell-name { font-size: var(--ls-fs-14); }
  .ls-rank-cell-fpts { font-size: var(--ls-fs-18); }
  .ls-rank-own { font-size: var(--ls-fs-9); }
}
@container rankpanel (min-height: 580px) {
  .ls-rank-table { border-spacing: 0 5px !important; }
  .ls-rank-table tbody td { padding-top: 8px; padding-bottom: 8px; }
  .ls-rank-cell-rank { font-size: var(--ls-fs-16); }
  .ls-rank-cell-pos  { font-size: var(--ls-fs-11); }
  .ls-rank-cell-name { font-size: var(--ls-fs-16); }
  .ls-rank-cell-fpts { font-size: var(--ls-fs-20); }
  .ls-rank-own { font-size: var(--ls-fs-10); }
}

/* Rank-movement arrow — fixed-width slot so rows don't shift when an
   arrow appears/disappears. Fades in on each poll diff. */
.ls-rank-move {
  display: inline-block;
  width: 10px;
  font-size: 7px;
  vertical-align: 1px;
  text-align: left;
}
.ls-rank-move-up   { color: var(--ls-accent); animation: ls-rank-move-in 500ms ease-out; }
.ls-rank-move-down { color: #ef4444; animation: ls-rank-move-in 500ms ease-out; }
@keyframes ls-rank-move-in {
  0%   { opacity: 0; transform: translateY(2px); }
  100% { opacity: 1; transform: translateY(0); }
}

/* Ownership pill — "do I have him?" exposure count on player
   leaderboards (×2 = in 2 of my lineups). */
.ls-rank-own {
  font-size: 8px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  padding: 0 4px;
  border-radius: 9999px;
  line-height: 1.5;
  background: rgb(var(--ls-accent-rgb) / 0.12);
  color: var(--ls-accent-deep);
}
.dark .ls-rank-own { background: rgb(var(--ls-accent-hi-rgb) / 0.16); color: var(--ls-accent-soft); }

/* Podium tints — rank numbers on My Rankings. Defined after the
   .is-leader rank colour so gold outranks the leader emerald. */
.ls-rank-table tbody td.ls-rank-podium-1,
.is-leader .ls-rank-cell-rank.ls-rank-podium-1 { color: #d97706; font-weight: 900; }
.dark .ls-rank-table tbody td.ls-rank-podium-1,
.dark .is-leader .ls-rank-cell-rank.ls-rank-podium-1 { color: #fcd34d; }
.ls-rank-table tbody td.ls-rank-podium-2 { color: #64748b; font-weight: 900; }
.dark .ls-rank-table tbody td.ls-rank-podium-2 { color: #e2e8f0; }
.ls-rank-table tbody td.ls-rank-podium-3 { color: #b45309; font-weight: 900; }
.dark .ls-rank-table tbody td.ls-rank-podium-3 { color: #fdba74; }

.ls-kpi-value {
  /* HERO number — large, tight, confident. tabular-nums keeps multi-
     digit values column-aligned across the strip. */
  font-size: var(--ls-fs-36);
  font-weight: 900;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.035em;
  color: var(--ls-ink);
}

.ls-kpi-foot {
  font-size: var(--ls-fs-10);
  font-weight: 500;
  color: #94a3b8;
  line-height: 1.1;
  letter-spacing: 0.01em;
  margin-top: 0;
}

/* ── Graph-KPI primitives ─────────────────────────────────────────────
   The Lab's KPIs are chart-ish (gauge / sparkline / share-bar) — the
   value slot pairs a smaller number with a tiny inline visual instead
   of the hero-number-only layout used in The Action's KPI strip.
   Monochrome emerald (+ amber-only for the HOTTEST card) keeps the
   visual language tight. (Xav 2026-06-20 Lab rebuild.) */
.ls-kpi-graph-value {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
/* Smaller number than .ls-kpi-value so the inline visual reads */
.ls-kpi-spark-num,
.ls-kpi-gauge-num {
  font-size: var(--ls-fs-22);
  font-weight: 900;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.025em;
}
.ls-kpi-spark {
  width: 100%;
  height: 22px;
  display: block;
}
.ls-kpi-gauge {
  width: 72px;
  height: 38px;
  display: block;
  margin-top: -4px;       /* tuck the number into the gauge bowl */
}
.ls-kpi-gauge-num { margin-top: -19px; }   /* sit inside the half-arc */
.ls-kpi-share-bar {
  width: 100%;
  height: 5px;
  background: rgb(var(--ls-ink-rgb) / 0.08);
  border-radius: 999px;
  overflow: hidden;
}
.dark .ls-kpi-share-bar { background: rgb(var(--ls-ink-soft-rgb) / 0.16); }
.ls-kpi-share-fill {
  height: 100%;
  border-radius: 999px;
  transition: width 350ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* KPI chrome — move-left / move-right / remove bar at the top-right
   of each tile. Only visible inside Edit mode (.ls-editing on
   .ls-row2). Replaces the old hover-shown .ls-kpi-close × button. */
.ls-kpi-chrome {
  position: absolute;
  top: 4px;
  right: 4px;
  display: none;
  align-items: center;
  gap: 1px;
  padding: 2px;
  border-radius: 6px;
  background: rgb(var(--ls-ink-rgb) / 0.88);
  z-index: 5;
}
.ls-editing .ls-kpi-chrome { display: flex; }
.ls-editing .ls-kpi-slot .ls-kpi-card {
  outline: 1px dashed rgb(var(--ls-accent-rgb) / 0.35);
  outline-offset: 2px;
}
.ls-kpi-chrome-btn {
  width: 22px; height: 22px;
  border: 0;
  background: transparent;
  color: #e2e8f0;
  cursor: pointer;
  border-radius: 4px;
  font-size: var(--ls-fs-14);
  line-height: 1;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 120ms;
}
.ls-kpi-chrome-btn:hover { background: rgb(var(--ls-surface-rgb) / 0.16); color: #ffffff; }

/* (.ls-kpi-mismatch — the top-left amber ⓘ — removed 2026-06-21; the
   standard ⓘ help popover now carries the "best with 5+ lineups" signal
   as a badge, so the separate indicator was a duplicate info icon.) */
.ls-kpi-chrome-x:hover   { background: rgb(var(--ls-danger-rgb) / 0.40); color: #ffffff; }

@media (max-width: 767px) {
  .ls-kpi-strip { gap: 0.4rem; }
  /* Two-up on phone — 4 KPIs render as 2 rows × 2 cols which matches
     Xav's Simple-mode default (4 KPIs visible, no scroll). The strip
     keeps `flex-wrap: wrap` from the base rule so a 5th or 6th tile
     flows to a 3rd row instead of squeezing the existing two. */
  .ls-kpi-slot  { flex: 1 1 calc(50% - 0.4rem); min-width: 0; }
  /* Don't let the KPI strip GROW to fill the column — that stretched every
     tile tall (height:100%), so the numbers floated in a sea of whitespace and
     shrinking the font did nothing. Content-size the strip + the cards, then a
     small padding + font actually bites. This is the real phone win. */
  .ls-row2-kpis { flex: 0 0 auto; }
  .ls-kpi-card  { height: auto; min-height: 0; padding: 10px 10px 8px; gap: 2px; }
  .ls-kpi-value { font-size: var(--ls-fs-22); }
  .ls-kpi-foot  { font-size: var(--ls-fs-9); }

  /* ── Global phone density pass (Xav 2026-07-10) ────────────────────────
     Curation removes widgets; this SHRINKS the ones that stay so a phone
     shows glance-sized cards, not desktop cards stacked full-width. Broad +
     safe: tighten the shared primitives every widget is built from. Desktop
     and tablet are untouched (phone-only media block). Widgets that need a
     different SHAPE (the scoreboard's mini scorecard) do it in their own
     template; this just tightens the common chrome. */
  .ls-panel-card, .bp-card, .pp-card, .act-card, .hr-card { padding: 6px 8px; border-radius: 9px; }
  .ls-rank-table tbody td { padding: 3px 5px; }
  .ls-rank-table thead th { padding-top: 2px; padding-bottom: 2px; }
  .ls-player-name { font-size: var(--ls-fs-12); }

  /* Scoreboard flagship chrome — the big "SCOREBOARD" title + fat frame ate
     the top of the screen. Shrink the title, thin the frame, drop the tall
     min-height (mini cards define their own short height). */
  .ls-scoreboard-frame { margin-top: 11px; padding: 11px 7px 7px; min-height: 0; }
  .ls-scoreboard-frame .ls-scoreboard-title { font-size: var(--ls-fs-12); letter-spacing: 0.16em; padding: 2px 11px; }
}
/* Tiniest phones (≤360px — iPhone SE-era) get one column so the value
   font doesn't crash into the label. Modern phones (375px+) stay 2-up. */
@media (max-width: 360px) {
  .ls-kpi-slot  { flex: 1 1 100%; }
}

/* Fit-to-viewport grid: rows divide the visible height proportionally
   (1fr each) so the whole dashboard lives in one viewport without
   scroll. The CSS variable --ls-rows is stamped by the host controller
   (liveDashboardV2.gridRows) from the layout's max y+h. Height math
   subtracts the page chrome: nav (56) + topbar (~52) + sync bar (~8)
   + main padding (~24) + footer note (~28) ≈ 168px. Tune if chrome
   changes. Uses 100dvh so mobile browser collapsing toolbars don't
   produce a scrollbar. */
.ls-grid              {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  grid-template-rows: repeat(var(--ls-rows, 3), minmax(0, 1fr));
  /* row gap matches the KPI→row1 visual spacing so floating
     .ls-cell-title-pill overhangs evenly between rows. Column gap
     stays narrow because there are no horizontal pills to clear.
     (Xav 2026-05-30: "same gap between all rows".) */
  column-gap: 0.5rem;
  row-gap: 1.25rem;
  /* padding-top reserves room for the floating .ls-cell-title-pill on
     row-1 cells so the pill's upper half can overhang without being
     clipped by the KPI strip above. */
  padding-top: 12px;
  /* Height is set inline by liveDashboardV2.gridHeight — measured from
     the grid's distance to the viewport bottom on mount + resize, so we
     fill the remaining viewport regardless of nav / topbar / footer
     size. CSS fallback below for the first paint before JS lands. */
  height: calc(100dvh - 12rem);
  min-height: 360px;
}
/* Mobile: drop fit-to-viewport, go back to a scrollable flex stack.
   Each cell gets a min-height so widgets don't collapse to nothing —
   the second-screen use case is desktop/tablet, not phone. */
.ls-grid.ls-grid-stack{
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  height: auto;
  min-height: 0;
}
/* Stack-mode cells size to their intrinsic content (was 280px then
   160px). With min-height present, panels with `flex-1` bodies (Vidi,
   Top 10s, Pulse cards) would stretch their body to fill the floor,
   producing 100px+ of trailing whitespace inside the card that read
   as a "gap" between panels. Letting cells collapse means empty-state
   panels render as small flat cards (~50px) — accurate to their data
   density — and the gap between panels is uniform `0.75rem` everywhere.
   Xav 2026-05-30. */
.ls-grid.ls-grid-stack .ls-cell { min-height: 0; }

/* Tablet (768-1279px): 12-col grid stays, but liveDashboardV2 projects
   every saved panel into a 6-col half so the dashboard reads as a
   forced 2-up. Drop the fixed viewport-fit height — at half-width we
   need more vertical room than one viewport gives — and let rows
   auto-size around content. Page scrolls; that's the right trade vs
   crushed panels.
   _tabletPlacements (live_v2/index.js) drives the projection. */
.ls-grid.ls-grid-tablet {
  height: auto;
  min-height: 0;
  grid-template-rows: none;
  grid-auto-rows: minmax(120px, auto);
}
.ls-grid.ls-grid-tablet .ls-cell { min-height: 240px; }

/* ─────────────────────────────────────────────────────────────────────
   Simple preset — larger headline content (Xav 2026-05-31).

   Simple ships fewer widgets than Analytical, so each one inherits more
   canvas — but the existing px-based fonts were sized for Analytical
   density and leave visible whitespace. These rules bump the HEADLINE
   content (KPI values, panel name labels, big stat numbers, FPts pills)
   ~15-25% when `data-preset="simple"` is on <html>. Chrome (sub-nav,
   pills, toolbar) stays the same size — only the data scales.

   Stamped by liveDashboardV2.initWidgets via $watch('_preset', …).
   Inverse pattern of TV mode below: TV scales everything globally;
   Simple selectively scales data while keeping chrome compact.
   ──────────────────────────────────────────────────────────────────── */
html[data-preset="simple"] .ls-kpi-value     { font-size: var(--ls-fs-44); }
html[data-preset="simple"] .ls-kpi-foot      { font-size: var(--ls-fs-11); }
html[data-preset="simple"] .ls-cell-title-pill { font-size: var(--ls-fs-11); padding: 5px 13px; }

/* Vidi cards — bump name + FPts, ease description for readability. */
html[data-preset="simple"] .vidi-card  { padding: 8px 11px 9px; gap: 4px; }
html[data-preset="simple"] .vidi-name  { font-size: var(--ls-fs-13); }
html[data-preset="simple"] .vidi-fpts  { font-size: 17px; }
html[data-preset="simple"] .vidi-time  { font-size: var(--ls-fs-10); }
html[data-preset="simple"] .vidi-desc  { font-size: var(--ls-fs-11); }
html[data-preset="simple"] .vidi-total-val { font-size: var(--ls-fs-11); }
html[data-preset="simple"] .vidi-team-logo { width: 16px; height: 16px; }

/* Pitcher Pulse — bump name + K count + footer stats. */
html[data-preset="simple"] .pp-card        { padding: 10px 12px; gap: 6px; }
html[data-preset="simple"] .pp-name        { font-size: var(--ls-fs-13); }
html[data-preset="simple"] .pp-k-count     { font-size: var(--ls-fs-32); }
html[data-preset="simple"] .pp-k-label     { font-size: var(--ls-fs-13); }
html[data-preset="simple"] .pp-fpts        { font-size: var(--ls-fs-12); }
html[data-preset="simple"] .pp-stat-val    { font-size: var(--ls-fs-12); }
html[data-preset="simple"] .pp-stat-label  { font-size: var(--ls-fs-9); }

/* Batter Pulse — same bump set as Pitcher Pulse. */
html[data-preset="simple"] .bp-card          { padding: 10px 12px; gap: 6px; }
html[data-preset="simple"] .bp-name          { font-size: var(--ls-fs-13); }
html[data-preset="simple"] .bp-batting-line  { font-size: 15px; }
html[data-preset="simple"] .bp-headline-val  { font-size: var(--ls-fs-20); }
html[data-preset="simple"] .bp-headline-label{ font-size: var(--ls-fs-10); }
html[data-preset="simple"] .bp-fpts          { font-size: var(--ls-fs-12); }
html[data-preset="simple"] .bp-stat-val      { font-size: var(--ls-fs-12); }
html[data-preset="simple"] .bp-stat-label    { font-size: var(--ls-fs-9); }

/* Matches cards — slightly bigger team + score. */
html[data-preset="simple"] .match-card       { padding: 9px 12px; }
html[data-preset="simple"] .match-team       { font-size: var(--ls-fs-13); }
html[data-preset="simple"] .match-score      { font-size: var(--ls-fs-16); }
html[data-preset="simple"] .match-logo       { width: 20px; height: 20px; }

/* ─────────────────────────────────────────────────────────────────────
   TV / couch mode (Xav 2026-05-30 #3 pass).

   Activation: Shift+T, ?tv=1, Mode dropdown → TV, or browser fullscreen.
   `data-tv-mode` is stamped on <html> by liveDashboardV2's $watch, so
   these rules can cascade to elements outside our component subtree
   (the main nav lives on base.html).

   Behaviour: strip every chrome surface (main nav, sub-nav, main
   padding), scale the entire UI to 130% so a 10ft viewing distance
   reads cleanly, expand the grid to fill the viewport. The user's
   saved layout is preserved as-is — TV is a visual mode, not a
   re-layout. (Layout-level TV preset is a separate concern handled by
   _preset === 'tv'.)
   ──────────────────────────────────────────────────────────────────── */
html[data-tv-mode] { font-size: 130%; }
html[data-tv-mode] body { background: #020617; }
html[data-tv-mode] > body > nav,                    /* main nav in base.html */
html[data-tv-mode] .sp-tabs-wrap,                   /* sub-nav inside live_v2 */
html[data-tv-mode] .ls-upgrade-banner,              /* Phase 7 upgrade prompt */
html[data-tv-mode] .ls-row2 {                       /* KPI strip / picker row — drop the KPI rail too so the grid gets the full vertical */
  display: none !important;
}
/* main has px-3/sm:px-6 + pt-2/sm:pt-3 in base.html. Cancel them so the
   grid spans the full viewport with no inset. */
html[data-tv-mode] main {
  padding: 0 !important;
}
html[data-tv-mode] .ls-grid {
  height: 100vh !important;
  max-height: 100vh !important;
  padding-top: 18px;
  padding-left: 18px;
  padding-right: 18px;
  padding-bottom: 18px;
  column-gap: 16px;
  row-gap: 24px;
}
/* TV-mode exit hint — small chip top-right that closes the mode.
   Pinned via fixed positioning so it stays visible regardless of grid
   scroll. Hover bumps to a slightly bolder treatment so it's clearly
   interactive without being a hard CTA. */
.ls-tv-exit {
  position: fixed;
  top: 14px;
  right: 14px;
  z-index: 9999;
  display: none;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: 9999px;
  background: rgb(var(--ls-ink-rgb) / 0.72);
  color: #e2e8f0;
  font-size: var(--ls-fs-11);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border: 1px solid rgb(var(--ls-surface-rgb) / 0.10);
  cursor: pointer;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  transition: background 160ms, opacity 160ms;
  opacity: 0.55;
}
.ls-tv-exit:hover { background: rgb(var(--ls-ink-rgb) / 0.92); opacity: 1; }
.ls-tv-exit-kbd {
  font-family: ui-monospace, monospace;
  font-size: var(--ls-fs-9);
  font-weight: 600;
  padding: 1px 5px;
  border-radius: 3px;
  background: rgb(var(--ls-surface-rgb) / 0.10);
  letter-spacing: 0.04em;
}
html[data-tv-mode] .ls-tv-exit { display: inline-flex; }
/* Drag ghost — sits in the grid track of the prospective drop position
   so the user can see exactly where the widget will land before they
   release. Solid emerald outline + soft inner gradient + breathing halo.
   Dashed looked draft-y; solid + halo reads as "destination locked in". */
.ls-drag-ghost {
  border: 2px solid rgb(var(--ls-accent-rgb) / 0.85);
  background: linear-gradient(135deg,
    rgb(var(--ls-accent-rgb) / 0.13) 0%,
    rgb(var(--ls-accent-rgb) / 0.06) 100%);
  border-radius: 12px;
  pointer-events: none;
  z-index: 10;
  animation: ls-drag-ghost-pulse 1.6s ease-in-out infinite;
}
@keyframes ls-drag-ghost-pulse {
  0%, 100% {
    box-shadow:
      inset 0 0 0 1px rgb(var(--ls-surface-rgb) / 0.15),
      0 0 0 4px rgb(var(--ls-accent-rgb) / 0.10),
      0 12px 28px -8px rgb(var(--ls-accent-rgb) / 0.30);
  }
  50% {
    box-shadow:
      inset 0 0 0 1px rgb(var(--ls-surface-rgb) / 0.20),
      0 0 0 7px rgb(var(--ls-accent-rgb) / 0.14),
      0 14px 30px -8px rgb(var(--ls-accent-rgb) / 0.42);
  }
}

/* Grid-snap overlay — faint 12-column dashed track that fades in during
   any active drag or resize, so the user has a spatial reference for the
   underlying snap grid. Placed via grid-column: 1 / -1 + grid-row: 1 / -1
   so it spans the parent grid's full extent without needing position:
   absolute (which would have required position:relative on the parent and
   broken the existing layout calcs). Inner grid mirrors the parent's 12
   columns + gap so the column boundaries line up with real cells. Behind
   the ghost (z-index 1 vs ghost z-index 10) and pointer-events:none so
   it never intercepts clicks. */
.ls-grid-lines {
  grid-column: 1 / -1;
  grid-row: 1 / -1;
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 0.5rem;
  pointer-events: none;
  opacity: 0;
  transition: opacity 180ms ease-out;
  /* No explicit z-index → paints in DOM order (lines first → below cells
     → below the ghost which has z:10). pointer-events:none guarantees
     clicks always reach cells regardless. */
}
.ls-grid-snap-on .ls-grid-lines { opacity: 1; }
.ls-grid-lines > div {
  border: 1px dashed rgb(var(--ls-accent-rgb) / 0.16);
  border-radius: 8px;
  background: rgb(var(--ls-accent-rgb) / 0.025);
}

/* Dim + lift during drag/resize.
   When .ls-grid-active-op is set on the grid (i.e. _dragSrcId or _resize
   is truthy), every non-active cell fades to half opacity so the user's
   eye locks onto the operation. The active cell — flagged with
   .ls-cell-is-active by _markActiveCell() in index.js — scales up ~2%,
   tilts a tiny degree, and floats above siblings via z-index + a heavy
   shadow. Same trick Notion / Linear use to make drag feel "lifted"
   instead of "shuffled". */
.ls-grid-active-op .ls-cell {
  opacity: 0.45;
  transition:
    opacity 180ms ease-out,
    transform 220ms cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 220ms ease-out;
}
.ls-grid-active-op .ls-cell.ls-cell-is-active {
  opacity: 1;
  transform: scale(1.02) rotate(0.6deg);
  box-shadow:
    0 22px 44px -12px rgb(var(--ls-ink-rgb) / 0.30),
    0 0 0 1px rgb(var(--ls-accent-rgb) / 0.45);
  z-index: 20;
}
.dark .ls-grid-active-op .ls-cell.ls-cell-is-active {
  box-shadow:
    0 22px 44px -10px rgba(0, 0, 0, 0.55),
    0 0 0 1px rgb(var(--ls-accent-rgb) / 0.55);
}

/* Stagger reveal on initial dashboard load.
   Each cell sets `--ls-stagger-i` to its index in the layout array
   (computed in the :style binding). The reveal runs once per cell mount —
   so it fires on first paint AND when the user adds a new widget, but
   NOT on reorder (cells keep their DOM via the x-for :key, FLIP handles
   movement). Skipped on mobile (.ls-grid-stack) where the visual lands
   instantly is fine and the stagger fights the scroll. */
@keyframes ls-cell-reveal {
  0%   { opacity: 0; transform: scale(0.96) translateY(8px); }
  100% { opacity: 1; transform: scale(1)    translateY(0);   }
}
.ls-grid:not(.ls-grid-stack) > .ls-cell {
  animation: ls-cell-reveal 360ms cubic-bezier(0.22, 1, 0.36, 1) both;
  animation-delay: calc(var(--ls-stagger-i, 0) * 55ms);
}
/* Active-cell transform must beat the reveal animation in case the user
   drags a cell during the first ~1s. animation: none cancels the running
   keyframes so the active scale/rotate takes effect cleanly. */
.ls-grid-active-op .ls-cell.ls-cell-is-active {
  animation: none;
}
/* Live resize readout — floating "W × H" pill that tracks the cursor
   during edge/corner drags. Solid emerald so it reads even on busy
   widget content. Pointer-events:none so it doesn't block the resize. */
.ls-resize-badge {
  position: fixed;
  z-index: 9999;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 11px;
  border-radius: 999px;
  background: var(--ls-accent);
  color: #ffffff;
  font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
  font-size: var(--ls-fs-11);
  font-weight: 800;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  box-shadow: 0 6px 16px -4px rgb(var(--ls-ink-rgb) / 0.35);
  pointer-events: none;
  user-select: none;
}

/* SE-corner resize handle — RETIRED 2026-05-30 (Xav). The 8-way edge
   detection in onCellEdgeMouseDown gives the user a 10px hit zone on
   every side and corner of the cell, with the OS-standard cursor on
   hover (n/s/e/w/ne/nw/se/sw-resize). The SE triangle was redundant
   visual noise on top of that — same affordance, twice. Display:none
   on the rendered divs is faster than ripping 16 instances out of the
   template; keeps the option to revert via a single CSS toggle. */
.ls-resize-handle { display: none; }
/* overflow: visible so the floating .ls-cell-title-pill (rendered by
   panel_chrome) can overhang the top border of the widget card. Inner
   .ls-widget keeps overflow:hidden so widget content is still clipped
   to the card's rounded border. (Xav 2026-05-30 title-pill pass.) */
.ls-cell              { position: relative; display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: visible; }
.ls-cell[draggable="true"] { cursor: grab; }
.ls-cell[draggable="true"]:active { cursor: grabbing; }
.ls-cell > .ls-widget { flex: 1; min-height: 0; overflow: hidden; container-type: inline-size; }
/* Density tier defaults — partials opt in by adding .ls-w-full/.ls-w-compact/.ls-w-mini
   inside .ls-widget; @container rules switch which is visible based on widget WIDTH,
   generalising the existing .match-card seed pattern in dash_matches_duel.html. */
.ls-widget .ls-w-full    { display: block; }
.ls-widget .ls-w-compact { display: none;  }
.ls-widget .ls-w-mini    { display: none;  }
@container (max-width: 360px) {
  .ls-widget .ls-w-full    { display: none; }
  .ls-widget .ls-w-compact { display: block; }
}
@container (max-width: 160px) {
  .ls-widget .ls-w-compact { display: none; }
  .ls-widget .ls-w-mini    { display: block; }
}

/* ── Card-per-player mobile transform ────────────────────────────────────
   Widgets that include partials/player_grid.html (My Players, My Lineups,
   All Players) render a wide table on desktop. Below ~500px widget width,
   the table flips to a stack of cards: player name big at the top, each
   stat becomes a labeled row driven by `data-label="..."` on every <td>
   (added to the macro). Driven by the WIDGET width via @container, not
   the viewport — so a half-width cell on desktop also gets cards.

   Scoped to `.player-grid` (the marker class added to the table in
   partials/player_grid.html) so Top 10 / Rankings / Scoreboard tables —
   which have their own column structure and NO data-label attrs — are
   not mangled when their cell is narrow. Legacy live.html ignores the
   data-labels entirely (no .ls-widget host selector matches). */
@container (max-width: 500px) {
  /* Dense box score (~18 cols): DON'T explode each cell onto its own row —
     that turned every player into an 18-row tower. Keep it a real table and
     scroll it sideways. Every player_grid host (all_players / my_players /
     my_lineups) already wraps this in an overflow-auto/-x container, so
     min-width:max-content stops the columns collapsing and the wrapper
     scrolls. Then freeze the player-name column (left) and the header row
     (top) — spreadsheet-style — so you never lose whose line or which stat
     you're reading while you swipe through the numbers. */
  .ls-widget table.player-grid { min-width: max-content; }

  /* Frozen header row */
  .ls-widget table.player-grid thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: #f1f5f9;
  }
  .dark .ls-widget table.player-grid thead th { background: #0f172a; }

  /* Frozen player-name column (first cell of every body row). The right-edge
     shadow is the scroll affordance: it tells the eye that stat columns run
     off under the frozen name — "swipe → for more" without a caption. */
  .ls-widget table.player-grid tbody td:first-child,
  .ls-widget table.player-grid thead th:first-child {
    box-shadow: 6px 0 8px -6px rgba(15, 23, 42, 0.22);
  }
  .ls-widget table.player-grid tbody td:first-child {
    position: sticky;
    left: 0;
    z-index: 1;
    background: #fff;
  }
  .dark .ls-widget table.player-grid tbody td:first-child { background: #0f172a; }
  /* Leader/hover wash sits on the row; give the frozen cell the same tint edge
     so it doesn't read as a detached white block against a highlighted row. */
  .ls-widget table.player-grid tbody tr:hover td:first-child {
    background: linear-gradient(90deg, rgb(var(--ls-accent-rgb) / 0.06) 0%, #fff 90%);
  }
  .dark .ls-widget table.player-grid tbody tr:hover td:first-child {
    background: linear-gradient(90deg, rgb(var(--ls-accent-hi-rgb) / 0.07) 0%, #0f172a 90%);
  }

  /* Top-left corner (name header) sits above both frozen planes */
  .ls-widget table.player-grid thead th:first-child {
    left: 0;
    z-index: 3;
  }

  /* Empty-state row (single colspan'd td) shouldn't get pinned/opaque —
     let it span and centre normally. */
  .ls-widget table.player-grid tbody tr td:only-child {
    position: static;
    background: transparent;
    text-align: center;
  }
}

/* ── Touch targets (v4 + v5 audit) ─────────────────────────────────────────
   On a coarse pointer (finger) every chrome icon button is mouse-sized
   (20–24px) — under the 44px thumb floor. The dashboard is a couch/phone-
   primary surface, so lift them ALL to a real 44px. The glyph stays small and
   centred — only the BOX grows, and it's a transparent tap area, so the icons
   don't visually bloat. Discrete boxes + a wider cluster gap (8px) keep the
   adjacent clustered buttons (info / cog / expand) from stealing each other's
   taps — the earlier 34px compromise is gone. */
@media (pointer: coarse) {
  .ls-cell-btn,
  .ls-cell-btn-expand,
  .ls-scoreboard-cog,
  .ls-mode-row-action      { width: 44px; height: 44px; }
  .ls-cell-chrome          { gap: 8px; }
  .ls-section-tab          { min-height: 44px; }
}
/* Cell chrome — INTEGRATED top-right icons, not a floating pill.
   Per Xav 2026-05-27: the dark-pill overlay felt detached from the
   widget. Now the icons sit flush inside the widget's own header
   area, picking up the widget's text colour so they read as part
   of it. Every panel reserves this top-right corner — widget
   partials must leave it clear (.ls-widget rule below adds the
   padding to the widget's header to enforce it).

   Position: absolute, inside the panel cell, aligned to where a
   typical widget header's right edge sits (top-right with small
   inset for breathing room around the rounded card corner). */
.ls-cell-chrome {
  position: absolute;
  top: 10px;
  right: 14px;
  display: flex;
  align-items: center;
  gap: 4px;
  z-index: 5;
  /* No background — icons live ON the widget surface, not floating
     above it. Inherit colour via currentColor below. */
  color: #6b7280;                                /* gray-500 */
  opacity: 0;
  pointer-events: none;
  transition: opacity 140ms ease-out;
}
.dark .ls-cell-chrome { color: #64748b; }        /* slate-500 */

/* View-mode visibility — icons stay always visible when the chrome
   carries view-actions (the cog is on every panel now, so this is
   effectively always). Subtler at rest, full opacity + emerald on
   hover so the affordance reads. */
.ls-cell-chrome.has-view-actions {
  opacity: 0.7;
  pointer-events: auto;
}
.ls-cell:hover .ls-cell-chrome.has-view-actions { opacity: 1; }
/* Edit mode — chrome always at full opacity. */
.ls-cell-chrome.is-editing,
.ls-editing .ls-cell-chrome {
  opacity: 1;
  pointer-events: auto;
}
.ls-editing .ls-cell { outline: 1px dashed rgb(var(--ls-accent-rgb) / 0.35); outline-offset: 2px; border-radius: 12px; }

/* Title only renders in edit mode; sits LEFT of the action icons
   inside the chrome bar. Small uppercase chip for visual weight. */
.ls-cell-title {
  font-size: var(--ls-fs-9);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  padding-right: 8px;
  color: inherit;
  opacity: 0.7;
}
.ls-cell-actions { display: flex; gap: 4px; }
.ls-cell-btn {
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: transparent;
  color: inherit;
  cursor: pointer;
  border-radius: 4px;
  font-size: var(--ls-fs-13);
  line-height: 1;
  transition: color 120ms, background 120ms, transform 120ms;
}
.ls-cell-btn:hover {
  color: var(--ls-accent-hover);                                /* emerald-600 */
  background: rgb(var(--ls-accent-rgb) / 0.08);
  transform: translateY(-1px);
}
.dark .ls-cell-btn:hover { color: var(--ls-accent-hi); }     /* emerald-400 */
/* SVG icons inside .ls-cell-btn — sized to fit the 20×20 button
   with breathing room. Stroke uses currentColor so the hover
   colour swap on the parent button propagates to the icon. */
.ls-cell-btn svg {
  width: 14px;
  height: 14px;
  display: block;
}
.ls-cell-btn-expand svg { width: 13px; height: 13px; }
.ls-cell-btn-cog    svg { width: 14px; height: 14px; }

/* Edit-mode panel × — solid dark surface + white glyph so it reads as
   a deliberate "destroy this widget" affordance, mirroring the KPI
   chrome bar's dark colourway. The cog stays on its normal palette
   in edit mode so users can still tweak per-widget options. */
.ls-editing .ls-cell-btn-remove,
.ls-grid-stack.ls-editing .ls-cell-btn-remove {
  background: rgb(var(--ls-ink-rgb) / 0.88);                /* slate-900 / 88% */
  color: #e2e8f0;                                    /* slate-200 */
  font-weight: 700;
}
.ls-editing .ls-cell-btn-remove:hover {
  background: #0f172a;                               /* slate-900 solid */
  color: #ffffff;
  transform: translateY(-1px);
}

/* RESERVED SPACE — removed 2026-05-30. The old rule shoved 56px of
   right-padding onto every widget's first child to keep inline headers
   clear of the absolute cog. Since every panel now renders its title
   via the floating .ls-cell-title-pill on the OUTER .ls-cell (not in
   the body), bodies don't share row 1 with chrome any more. Top-row
   collision is handled per-widget with a small `pt-*` on the scroll
   container (top10 = pt-6, vidi/matches = pt-7) which lets tables use
   the full inline width. Widgets that still want inline header
   content on the right (Dream Team's "X.XX pts" badge) should add
   their own right-margin instead of relying on a global rule. */

/* Mobile (stack) — chrome always visible, sits on the same row as
   the widget header (still top-right but no opacity dance). */
.ls-grid-stack .ls-cell-chrome {
  opacity: 1;
  pointer-events: auto;
}

/* ── Drill-down modal ─────────────────────────────────────────────────
   Desktop: centered card, max ~960×80vh, scrolls inside. Backdrop dim
   covers viewport. Mobile: bottom sheet, full-width, max 90vh,
   slides up from the bottom. Same Alpine state drives both — CSS swaps
   the positioning. */
.ls-modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgb(var(--ls-ink-rgb) / 0.55);
  backdrop-filter: blur(2px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
  overflow: hidden;
}
.ls-modal-card {
  background: #fff;
  border-radius: 14px;
  box-shadow: 0 25px 60px rgba(0, 0, 0, 0.35);
  width: 100%;
  max-width: 960px;
  /* Fixed height (not max-height) so the body has a real size for inner
     widgets that use h-full / flex: 1; otherwise the partial collapses
     and the card shrinks to its header. */
  height: calc(100dvh - 4rem);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.dark .ls-modal-card { background: #0f172a; border: 1px solid rgb(var(--ls-surface-rgb) / 0.06); }
.ls-modal-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 18px;
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
  flex-shrink: 0;
}
.dark .ls-modal-header { border-bottom-color: rgb(var(--ls-surface-rgb) / 0.06); }
.ls-modal-title    { font-size: var(--ls-fs-16); font-weight: 700; letter-spacing: -0.01em; color: var(--ls-ink); }
.ls-modal-subtitle { font-size: var(--ls-fs-11); color: #94a3b8; margin-top: 2px; }
.ls-modal-close    {
  background: transparent;
  border: 0;
  color: #64748b;
  cursor: pointer;
  padding: 4px;
  border-radius: 8px;
  flex-shrink: 0;
}
.ls-modal-close:hover { background: rgb(var(--ls-ink-rgb) / 0.05); color: #0f172a; }
.dark .ls-modal-close:hover { background: rgb(var(--ls-surface-rgb) / 0.08); color: #f1f5f9; }
.ls-modal-body     {
  flex: 1;
  min-height: 0;
  overflow: auto;
  padding: 12px 14px;
}
/* Drop the inner widget's own border/shadow when it's inside a modal —
   the modal already owns the chrome. Also force h-full down the chain
   so the inner table fills the modal body (partials use h-full on
   their inner div which needs explicit height all the way up). */
.ls-modal-body .ls-widget          { height: 100%; display: flex; flex-direction: column; }
.ls-modal-body .ls-widget > div    { border: 0; box-shadow: none; flex: 1; min-height: 0; }

/* Phone: full-width bottom sheet. Card pins to bottom of viewport,
   slides up. Drag handle pill at top hints the gesture. */
@media (max-width: 767px) {
  .ls-modal-backdrop {
    padding: 0;
    align-items: flex-end;
  }
  .ls-modal-card {
    max-width: 100%;
    height: 90dvh;
    border-radius: 14px 14px 0 0;
  }
  .ls-modal-header { padding: 12px 14px; }
  .ls-modal-header::before {
    content: '';
    position: absolute;
    top: 4px;
    left: 50%;
    transform: translateX(-50%);
    width: 36px;
    height: 4px;
    border-radius: 999px;
    background: rgb(var(--ls-ink-muted-rgb) / 0.5);
  }
  .ls-modal-header { position: relative; padding-top: 16px; }
}

/* Expand button in the cell chrome — drives the drill-down. Always
   visible on phone (chrome is sticky there), hover-only on desktop. */
.ls-cell-btn.ls-cell-btn-expand { font-size: var(--ls-fs-11); }

/* Graph gridlines (rendered inline by dashboard/graphs.js with a light
   #e2e8f0 stroke + a .dark-grid hook). The hook had no rule, so the
   gridlines glared bright-white over the dark canvas. CSS stroke wins
   over the inline presentation attribute, so this dims them in dark. */
.dark .dark-grid { stroke: rgb(var(--ls-ink-soft-rgb) / 0.18); }

/* ── Phone: scoreboard = stacked full-width list (Xav 2026-07-04) ─────
   MUST live at the END of the file: the base `.ls-headline { flex:1;
   align-items:center }` (which centres cards on both axes for desktop)
   sits further up at equal specificity, so an earlier override loses the
   cascade tie and the cards stay vertically centred — overflowing a short
   frame above AND below. Placed last, this wins.

   On a phone each game is a full-width card stacked top-to-bottom, so the
   frame must HUG that list from the top: no min-height floor, no vertical
   centring, and the headline grows to its content (page scrolls). */
@media (max-width: 767px) {
  .ls-scoreboard-frame { min-height: 0; }
  .ls-headline {
    flex: 0 0 auto;
    align-items: stretch;
    justify-content: flex-start;
    min-height: 0;
  }
  .ls-headline-scoreboard { overflow: visible; }
}

/* ══ KPI escalation "volume" — quiet / live / hot (Xav 2026-07-04) ══════
   A KPI tile keeps its exact shape but earns a VOLUME from its per-widget
   thresholds (widgetCopy[id].volume, resolved by kpiVolume). Quiet = the
   neutral card with an ink number — most tiles, most of the time. .is-live
   leans green (something's happening); .is-hot goes orange + breathes (a
   real moment: a 2-HR night, a stack going off). The whole point is that
   quiet is the default, so a green/orange pop actually means something.
   One green (the accent), one orange (--ls-hot). */

/* LIVE — green, quiet confidence */
.ls-kpi-card.is-live {
  border-color: rgb(var(--ls-accent-rgb) / 0.34);
  box-shadow: 0 1px 2px rgb(var(--ls-ink-rgb) / 0.04),
              0 8px 22px -8px rgb(var(--ls-accent-rgb) / 0.26);
}
.dark .ls-kpi-card.is-live {
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35),
              0 8px 22px -8px rgb(var(--ls-accent-rgb) / 0.30);
}
.ls-kpi-card.is-live .ls-kpi-value        { color: var(--ls-accent); }
.dark .ls-kpi-card.is-live .ls-kpi-value  { color: var(--ls-accent-hi); }

/* HOT — orange, it grabs you across the whole strip */
.ls-kpi-card.is-hot {
  border-color: rgb(var(--ls-hot-rgb) / 0.55);
  box-shadow: 0 1px 2px rgb(var(--ls-ink-rgb) / 0.04),
              0 0 0 1px rgb(var(--ls-hot-rgb) / 0.35),
              0 10px 26px -6px rgb(var(--ls-hot-rgb) / 0.42);
  animation: ls-kpi-hot-breathe 2.4s ease-in-out infinite;
}
.dark .ls-kpi-card.is-hot {
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35),
              0 0 0 1px rgb(var(--ls-hot-rgb) / 0.40),
              0 10px 26px -6px rgb(var(--ls-hot-rgb) / 0.45);
}
/* Hero number stays ON-BRAND — emerald when the tile is hot, never orange
   (Xav 2026-07-10). The heat still reads loud via the card's orange border-
   glow + breathe below; the number just no longer turns orange, which felt
   off-brand (esp. exaggerated by the demo's constant scoring). */
.ls-kpi-card.is-hot .ls-kpi-value        { color: var(--ls-accent); }
.dark .ls-kpi-card.is-hot .ls-kpi-value  { color: var(--ls-accent-hi); }
/* the floating label pill flips to orange in the hot state */
.ls-kpi-card.is-hot .ls-kpi-label {
  background: linear-gradient(180deg, #fff6ed 0%, var(--ls-hot-hi) 100%);
  border-color: rgb(var(--ls-hot-rgb) / 0.55);
  color: #7c2d12;
}
.dark .ls-kpi-card.is-hot .ls-kpi-label {
  background: linear-gradient(180deg, rgb(var(--ls-hot-rgb) / 0.30) 0%, var(--ls-hot) 100%);
  color: #fff7ed;
}

@keyframes ls-kpi-hot-breathe {
  0%, 100% { box-shadow: 0 1px 2px rgb(var(--ls-ink-rgb) / 0.04),
                         0 0 0 1px rgb(var(--ls-hot-rgb) / 0.30),
                         0 8px 22px -8px rgb(var(--ls-hot-rgb) / 0.34); }
  50%      { box-shadow: 0 1px 2px rgb(var(--ls-ink-rgb) / 0.04),
                         0 0 0 1px rgb(var(--ls-hot-rgb) / 0.55),
                         0 12px 30px -4px rgb(var(--ls-hot-rgb) / 0.55); }
}
@media (prefers-reduced-motion: reduce) {
  .ls-kpi-card.is-hot { animation: none; }
}

/* ═══════════════════════════════════════════════════════════════════════
   Pre-Slate (pre-game hub) — premium chrome. Mirrors .ls-scoreboard-frame
   (flagship emerald frame) + rank-table list feel. Design tokens only.
   ═══════════════════════════════════════════════════════════════════════ */
.ls-pregame { padding-top: 2px; }
.ls-pregame-top { display:flex; flex-wrap:wrap; align-items:baseline; gap:5px 12px; margin-bottom:15px; }
.ls-pregame-eyebrow { font-size:11px; font-weight:800; letter-spacing:.14em; text-transform:uppercase;
  color:var(--ls-accent-deep); display:inline-flex; align-items:center; gap:6px; }
.dark .ls-pregame-eyebrow { color:var(--ls-accent-hi); }
.ls-pregame-sub { font-size:13px; color:var(--ls-ink-soft); }
.ls-pregame-sub b { color:var(--ls-ink); font-weight:700; }

/* featured deep card — flagship emerald frame */
.ls-pregame-feat { position:relative; border-radius:16px; overflow:hidden; margin:0 0 22px;
  background:linear-gradient(180deg, rgb(var(--ls-accent-rgb)/0.08), rgb(var(--ls-accent-rgb)/0.02));
  border:1.5px solid rgb(var(--ls-accent-rgb)/0.38);
  box-shadow:0 16px 44px -22px rgb(var(--ls-accent-rgb)/0.38); }
.dark .ls-pregame-feat { background:linear-gradient(180deg, rgb(var(--ls-accent-rgb)/0.15), rgb(2 6 23/0.45));
  border-color:rgb(var(--ls-accent-hi-rgb)/0.42); }
.ls-pregame-head { display:flex; align-items:center; gap:14px; padding:15px 20px;
  border-bottom:1px solid rgb(var(--ls-accent-rgb)/0.16); }
.ls-pregame-team { display:flex; align-items:center; gap:11px; min-width:0; }
.ls-pregame-crest { width:38px; height:38px; object-fit:contain; flex:none; }
.ls-pregame-team-nm { font-weight:800; font-size:16px; line-height:1.05; color:var(--ls-ink); }
.ls-pregame-team-sub { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--ls-ink-soft); }
.ls-pregame-at { font-weight:800; color:var(--ls-ink-soft); font-size:13px; }
.ls-pregame-when { margin-left:auto; text-align:right; flex:none; }
.ls-pregame-when-k { font-size:10px; font-weight:800; letter-spacing:.1em; text-transform:uppercase; color:var(--ls-ink-soft); }
.ls-pregame-when-t { font-size:24px; font-weight:800; line-height:1; color:var(--ls-accent-deep); font-variant-numeric:tabular-nums; }
.dark .ls-pregame-when-t { color:var(--ls-accent-hi); }
.ls-pregame-when-z { font-size:11px; color:var(--ls-ink-soft); font-variant-numeric:tabular-nums; }

.ls-pregame-pp { display:grid; grid-template-columns:1fr 1fr; }
.ls-pregame-pp-col { padding:14px 18px; }
.ls-pregame-pp-col + .ls-pregame-pp-col { border-left:1px solid rgb(var(--ls-accent-rgb)/0.12); }
.ls-pregame-pp-h { display:flex; align-items:center; gap:7px; margin-bottom:9px; }
.ls-pregame-pp-team { font-size:12px; font-weight:800; color:var(--ls-ink); }
.ls-pregame-pp-lbl { font-size:10px; font-weight:800; text-transform:uppercase; letter-spacing:.07em; color:var(--ls-ink-soft); }
.ls-pregame-sp { display:flex; align-items:center; gap:10px; padding:11px 13px; border-radius:12px;
  background:#fff; border:1px solid rgb(15 23 42/0.08); }
.dark .ls-pregame-sp { background:rgb(255 255 255/0.04); border-color:rgb(255 255 255/0.09); }
.ls-pregame-sp-badge { width:26px; height:26px; border-radius:50%; display:grid; place-items:center; flex:none;
  font-size:11px; font-weight:800; color:#fff; background:var(--ls-accent); }
.ls-pregame-sp-nm { font-weight:800; font-size:14px; color:var(--ls-ink); }
.ls-pregame-sp-tbd { font-size:14px; color:var(--ls-ink-soft); }

.ls-pregame-note { display:flex; align-items:center; gap:8px; padding:11px 20px;
  border-top:1px dashed rgb(var(--ls-accent-rgb)/0.22); font-size:12px; color:var(--ls-ink-soft); }
.ls-pregame-note-dot { width:6px; height:6px; border-radius:50%; background:#f5a524; flex:none; }

.ls-pregame-sec-h { font-size:11px; font-weight:800; text-transform:uppercase; letter-spacing:.12em;
  color:var(--ls-ink-soft); margin:0 2px 12px; }
.ls-pregame-grid { display:grid; gap:12px; grid-template-columns:repeat(auto-fill, minmax(232px, 1fr)); }
.ls-pregame-card { border-radius:13px; padding:12px 13px; background:#fff; border:1px solid rgb(15 23 42/0.08);
  transition:border-color .15s, transform .15s, background .15s; }
.dark .ls-pregame-card { background:rgb(255 255 255/0.03); border-color:rgb(255 255 255/0.08); }
.ls-pregame-card:hover { transform:translateY(-1px); border-color:rgb(var(--ls-accent-rgb)/0.42);
  background:rgb(var(--ls-accent-rgb)/0.05); }
.ls-pregame-card-top { display:flex; align-items:center; justify-content:space-between; margin-bottom:9px; }
.ls-pregame-card-mu { display:flex; align-items:center; gap:7px; font-weight:800; font-size:13.5px; color:var(--ls-ink); }
.ls-pregame-card-mu .at { color:var(--ls-ink-soft); font-size:11px; font-weight:700; }
.ls-pregame-card-time { font-size:11.5px; color:var(--ls-ink-soft); font-weight:700; font-variant-numeric:tabular-nums; }
.ls-pregame-card-sp { display:flex; flex-direction:column; gap:3px; }
.ls-pregame-card-sp-row { display:flex; align-items:center; gap:8px; font-size:12px; }
.ls-pregame-card-sp-team { color:var(--ls-ink-soft); font-weight:800; width:30px; text-transform:uppercase; font-size:10px; }
.ls-pregame-card-sp-nm { color:var(--ls-ink); font-weight:600; }
.ls-pregame-empty { text-align:center; color:var(--ls-ink-soft); font-size:14px; padding:40px 0; }

/* confirmed batting order (slice 1b) — lives under each probable pitcher */
.ls-pregame-bat { margin-top:12px; padding-top:11px; border-top:1px solid rgb(var(--ls-accent-rgb)/0.14); display:flex; flex-direction:column; gap:1px; }
.ls-pregame-bat-h { font-size:10px; font-weight:800; letter-spacing:.09em; text-transform:uppercase; color:var(--ls-accent-deep); margin-bottom:5px; }
.dark .ls-pregame-bat-h { color:var(--ls-accent-hi); }
.ls-pregame-bat-row { display:grid; grid-template-columns:20px 1fr auto; align-items:center; gap:9px; padding:5px 7px; border-radius:7px; transition:background .12s ease; }
.ls-pregame-bat-row:hover { background:rgb(var(--ls-accent-rgb)/0.06); }
.ls-pregame-bat-no { font-size:11px; font-weight:800; text-align:center; color:#fff; background:rgb(var(--ls-accent-rgb)/0.55); border-radius:5px; line-height:18px; font-variant-numeric:tabular-nums; }
.dark .ls-pregame-bat-no { background:rgb(var(--ls-accent-rgb)/0.4); }
.ls-pregame-bat-nm { font-size:13px; font-weight:600; color:var(--ls-ink); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.ls-pregame-bat-pos { font-size:10px; font-weight:800; letter-spacing:.04em; text-transform:uppercase; color:var(--ls-ink-soft); }
.ls-pregame-pending { margin-top:12px; display:flex; align-items:center; gap:8px; padding:9px 11px; border:1px dashed rgb(var(--ls-accent-rgb)/0.28); border-radius:10px; font-size:12px; color:var(--ls-ink-soft); }

/* weather cell (slice 3) — Open-Meteo forecast + statsapi roof */
.ls-pregame-wx { margin:14px 0 4px; padding:12px 14px; border-radius:12px; background:rgb(var(--ls-accent-rgb)/0.05); border:1px solid rgb(var(--ls-accent-rgb)/0.16); }
.dark .ls-pregame-wx { background:rgb(var(--ls-accent-rgb)/0.08); }
.ls-pregame-wx-live { display:flex; align-items:center; gap:14px; }
.ls-pregame-wx-temp { font-size:34px; font-weight:800; line-height:1; color:var(--ls-ink); font-variant-numeric:tabular-nums; }
.ls-pregame-wx-meta { display:flex; flex-direction:column; gap:3px; }
.ls-pregame-wx-cond { font-size:13px; font-weight:700; color:var(--ls-ink); }
.ls-pregame-wx-line { display:flex; align-items:center; gap:7px; font-size:12px; font-weight:600; color:var(--ls-ink-soft); }
.ls-pregame-wx-dot { opacity:.5; }
.ls-pregame-wx-rain-mid { color:#b45309; }          /* amber — meaningful rain */
.ls-pregame-wx-rain-hi  { color:#dc2626; font-weight:800; }  /* red — likely rain */
.dark .ls-pregame-wx-rain-mid { color:#fbbf24; }
.dark .ls-pregame-wx-rain-hi  { color:#f87171; }
.ls-pregame-wx-roof { padding:1px 7px; border-radius:999px; background:rgb(var(--ls-accent-rgb)/0.12); font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--ls-accent-deep); }
.dark .ls-pregame-wx-roof { color:var(--ls-accent-hi); }
.ls-pregame-wx-dome { display:flex; align-items:center; gap:10px; }
.ls-pregame-wx-badge { padding:3px 10px; border-radius:999px; background:rgb(var(--ls-accent-rgb)/0.16); color:var(--ls-accent-deep); font-size:11px; font-weight:800; text-transform:uppercase; letter-spacing:.06em; }
.dark .ls-pregame-wx-badge { color:var(--ls-accent-hi); }
.ls-pregame-wx-sub { font-size:12px; font-weight:600; color:var(--ls-ink-soft); }
.ls-pregame-wx-age { margin-left:auto; align-self:flex-start; font-size:10px; font-weight:600; letter-spacing:.02em; color:var(--ls-ink-soft); opacity:.7; white-space:nowrap; }
/* grid card compact weather */
.ls-pregame-card-wx { margin-top:9px; padding-top:8px; border-top:1px solid rgb(var(--ls-accent-rgb)/0.10); font-size:11px; font-weight:600; color:var(--ls-ink-soft); font-variant-numeric:tabular-nums; }
.ls-pregame-card-wx-rain { color:#b45309; font-weight:800; }
.dark .ls-pregame-card-wx-rain { color:#fbbf24; }
.ls-pregame-card-wx-dome { padding:1px 7px; border-radius:999px; background:rgb(var(--ls-accent-rgb)/0.12); font-size:10px; font-weight:800; text-transform:uppercase; letter-spacing:.04em; color:var(--ls-accent-deep); }
.dark .ls-pregame-card-wx-dome { color:var(--ls-accent-hi); }

@media (max-width:640px){
  .ls-pregame-pp { grid-template-columns:1fr; }
  .ls-pregame-pp-col + .ls-pregame-pp-col { border-left:0; border-top:1px solid rgb(var(--ls-accent-rgb)/0.12); }
}

/* ── THE SLATE — click-to-feature (both sports) ── */
.ls-pregame-card { cursor:pointer; }
.ls-pregame-card:focus-visible { outline:2px solid var(--ls-accent); outline-offset:2px; }
.ls-pregame-back { margin-left:auto; padding:4px 11px; border-radius:999px; font-size:11px; font-weight:700;
  color:var(--ls-accent-deep); background:rgb(var(--ls-accent-rgb)/0.10); border:1px solid rgb(var(--ls-accent-rgb)/0.30);
  cursor:pointer; transition:background .12s ease; }
.ls-pregame-back:hover { background:rgb(var(--ls-accent-rgb)/0.18); }
.dark .ls-pregame-back { color:var(--ls-accent-hi); }

/* ── THE SLATE, NFL variant — betting line / injury report / value strip ── */
.ls-pregame-line { display:flex; flex-wrap:wrap; gap:10px; padding:12px 20px 0; }
.ls-pregame-line-pill { display:inline-flex; align-items:center; gap:8px; padding:7px 13px; border-radius:999px;
  background:#fff; border:1px solid rgb(15 23 42/0.08); }
.dark .ls-pregame-line-pill { background:rgb(255 255 255/0.05); border-color:rgb(255 255 255/0.10); }
.ls-pregame-line-k { font-size:10px; font-weight:800; text-transform:uppercase; letter-spacing:.09em; color:var(--ls-ink-soft); }
.ls-pregame-line-v { font-size:14px; font-weight:800; color:var(--ls-ink); font-variant-numeric:tabular-nums; }
.ls-pregame-wx-wind-hi { color:#b45309; font-weight:800; }   /* amber — wind that moves totals */
.dark .ls-pregame-wx-wind-hi { color:#fbbf24; }

/* injury report (featured card) — the pool's DK O/Q/D flags */
.ls-pregame-pp .ls-pregame-inj-wrap { grid-column:1 / -1; padding:14px 18px; }
.ls-pregame-inj-cols { display:grid; grid-template-columns:repeat(auto-fit, minmax(210px, 1fr)); gap:6px 22px; }
.ls-pregame-inj-row { display:grid; grid-template-columns:42px 1fr auto; align-items:center; gap:9px; padding:5px 7px;
  border-radius:7px; transition:background .12s ease; }
.ls-pregame-inj-row:hover { background:rgb(var(--ls-accent-rgb)/0.06); }
.ls-pregame-inj-flag { font-size:9.5px; font-weight:800; letter-spacing:.05em; text-align:center; border-radius:5px;
  line-height:18px; text-transform:uppercase; }
.ls-pregame-inj-flag.is-OUT  { color:#fff; background:#dc2626; }
.ls-pregame-inj-flag.is-DOUB { color:#7c2d12; background:#fbbf24; }
.ls-pregame-inj-flag.is-QUES { color:#713f12; background:#fde047; }
.dark .ls-pregame-inj-flag.is-OUT  { background:#b91c1c; }
.ls-pregame-pp .ls-pregame-inj-clean { grid-column:1 / -1; display:flex; align-items:center; gap:8px;
  padding:12px 20px; font-size:12px; font-weight:600; color:var(--ls-ink-soft); }
.ls-pregame-inj-clean-dot { width:6px; height:6px; border-radius:50%; background:var(--ls-accent); flex:none; }

/* grid card: line + injury count */
.ls-pregame-card-line { margin-top:2px; font-size:11.5px; font-weight:700; color:var(--ls-ink);
  font-variant-numeric:tabular-nums; display:flex; align-items:center; gap:7px; }
.ls-pregame-card-inj { margin-top:8px; font-size:10.5px; font-weight:800; color:#b45309; }
.dark .ls-pregame-card-inj { color:#fbbf24; }

/* lineup alert — flagged players in MY lineups (shared by all sports).
   Amber urgency chrome: this is a warning, not brand emerald. */
.ls-pregame-alert { margin:0 0 18px; border-radius:14px; padding:13px 16px 11px;
  background:linear-gradient(180deg, rgb(245 158 11/0.10), rgb(245 158 11/0.03));
  border:1.5px solid rgb(245 158 11/0.45); }
.dark .ls-pregame-alert { background:linear-gradient(180deg, rgb(245 158 11/0.14), rgb(2 6 23/0.3));
  border-color:rgb(245 158 11/0.40); }
.ls-pregame-alert-h { display:flex; align-items:center; gap:8px; margin-bottom:8px;
  font-size:12px; font-weight:800; text-transform:uppercase; letter-spacing:.08em; color:#b45309; }
.dark .ls-pregame-alert-h { color:#fbbf24; }
.ls-pregame-alert-rows { display:grid; grid-template-columns:repeat(auto-fit, minmax(250px, 1fr)); gap:2px 20px; }
.ls-pregame-alert-row { display:grid; grid-template-columns:42px minmax(0,1fr) auto auto; align-items:center;
  gap:9px; padding:4px 6px; border-radius:7px; }
.ls-pregame-alert-row:hover { background:rgb(245 158 11/0.08); }
.ls-pregame-alert-n { font-size:11px; font-weight:800; color:#b45309; font-variant-numeric:tabular-nums; white-space:nowrap; }
.dark .ls-pregame-alert-n { color:#fbbf24; }
.ls-pregame-alert-foot { margin-top:8px; font-size:11px; color:var(--ls-ink-soft); }

/* ═══════════════════════════════════════════════════════════════════════
   Red Zone panel (NFL/CFB) — an ALERT surface. Quiet by default; red is
   the warning state here, not brand chrome, so emerald stays out of it.
   ═══════════════════════════════════════════════════════════════════════ */
.ls-rz { height: 100%; overflow-y: auto; }
.ls-rz-empty { height: 100%; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 5px; text-align: center; color: var(--ls-ink-soft); font-size: 13px; }
.ls-rz-empty-mark { width: 11px; height: 7px; border-radius: 50%; transform: rotate(-18deg);
  background: rgb(var(--ls-ink-rgb) / 0.18); }
.dark .ls-rz-empty-mark { background: rgb(255 255 255 / 0.18); }
.ls-rz-empty-sub { font-size: 11px; opacity: .7; max-width: 30ch; }

.ls-rz-list { display: flex; flex-direction: column; gap: 9px; padding: 2px; }
.ls-rz-card { border-radius: 12px; padding: 10px 12px; background: #fff;
  border: 1px solid rgb(15 23 42 / 0.08); }
.dark .ls-rz-card { background: rgb(255 255 255 / 0.03); border-color: rgb(255 255 255 / 0.08); }
/* A drive you have players in gets the red frame; one you don't stays neutral. */
.ls-rz-card.is-mine { border-color: rgb(239 68 68 / 0.45); background: rgb(239 68 68 / 0.05); }
.dark .ls-rz-card.is-mine { background: rgb(239 68 68 / 0.10); }

.ls-rz-head { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
.ls-rz-flag { font-size: 9px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  padding: 1px 6px; border-radius: 4px; color: #fff; background: #dc2626; }
@media (prefers-reduced-motion: no-preference) {
  .ls-rz-card.is-mine .ls-rz-flag { animation: ls-rz-pulse 1.6s ease-in-out infinite; }
}
@keyframes ls-rz-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
.ls-rz-mu { font-size: 12px; font-weight: 800; color: var(--ls-ink); }
.ls-rz-clock { margin-left: auto; font-size: 11px; font-weight: 700; color: var(--ls-ink-soft);
  font-variant-numeric: tabular-nums; }

.ls-rz-drive { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.ls-rz-att { font-size: 11px; font-weight: 800; letter-spacing: .04em; color: #dc2626; }
.dark .ls-rz-att { color: #fca5a5; }
.ls-rz-dd { font-size: 11.5px; font-weight: 600; color: var(--ls-ink-soft); font-variant-numeric: tabular-nums; }

.ls-rz-side + .ls-rz-side { margin-top: 8px; padding-top: 7px; border-top: 1px solid rgb(15 23 42 / 0.07); }
.dark .ls-rz-side + .ls-rz-side { border-top-color: rgb(255 255 255 / 0.07); }
.ls-rz-side-h { font-size: 9.5px; font-weight: 800; letter-spacing: .09em; text-transform: uppercase;
  color: var(--ls-ink-soft); margin-bottom: 4px; }
.ls-rz-row { display: grid; grid-template-columns: 32px 1fr auto; align-items: center; gap: 8px; padding: 3px 0; }
.ls-rz-pos { font-size: 9px; font-weight: 800; color: var(--ls-ink-soft); }
.ls-rz-nm { font-size: 13px; font-weight: 600; color: var(--ls-ink); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.ls-rz-pts { font-size: 12.5px; font-weight: 800; color: var(--ls-ink); font-variant-numeric: tabular-nums; }
.ls-rz-none { font-size: 11px; color: var(--ls-ink-soft); opacity: .75; }
.ls-rz-foot { padding: 4px 2px 0; font-size: 11px; font-weight: 700; color: #dc2626; }
.dark .ls-rz-foot { color: #fca5a5; }

/* ═══════════════════════════════════════════════════════════════════════
   NFL bespoke widgets — Bonus Watch / QB Room / D/ST Bracket / Game
   Script / Category Leaders. Shared idioms: a card per subject, a tight
   stat line, and amber ONLY where something is about to change.
   ═══════════════════════════════════════════════════════════════════════ */
.ls-bw, .ls-qbr, .ls-dstb, .ls-gs, .ls-cl { height: 100%; overflow-y: auto; }
.ls-bw-empty, .ls-qbr-empty, .ls-dstb-empty, .ls-gs-empty, .ls-cl-empty {
  height: 100%; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 4px; text-align: center;
  color: var(--ls-ink-soft); font-size: 13px; padding: 0 12px; }
.ls-bw-empty-sub { font-size: 11px; opacity: .7; }

/* shared card shell */
.ls-bw-row, .ls-qbr-card, .ls-dstb-card, .ls-gs-card, .ls-cl-row {
  border-radius: 11px; padding: 9px 11px; background: #fff;
  border: 1px solid rgb(15 23 42 / 0.08); }
.dark .ls-bw-row, .dark .ls-qbr-card, .dark .ls-dstb-card,
.dark .ls-gs-card, .dark .ls-cl-row {
  background: rgb(255 255 255 / 0.03); border-color: rgb(255 255 255 / 0.08); }
.ls-bw-list, .ls-qbr-list, .ls-dstb-list, .ls-gs-list, .ls-cl-list {
  display: flex; flex-direction: column; gap: 8px; padding: 2px; }

/* ── Bonus Watch ── */
.ls-bw-top { display: flex; align-items: baseline; gap: 7px; margin-bottom: 5px; }
.ls-bw-nm { font-size: 13px; font-weight: 700; color: var(--ls-ink); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.ls-bw-meta { font-size: 10px; font-weight: 700; color: var(--ls-ink-soft); text-transform: uppercase;
  letter-spacing: .04em; white-space: nowrap; }
.ls-bw-togo { margin-left: auto; font-size: 11.5px; font-weight: 800; color: var(--ls-ink-soft);
  font-variant-numeric: tabular-nums; white-space: nowrap; }
.ls-bw-togo.is-live { color: #b45309; }
.dark .ls-bw-togo.is-live { color: #fbbf24; }
.ls-bw-badge { margin-left: auto; font-size: 9px; font-weight: 800; letter-spacing: .06em;
  padding: 1px 6px; border-radius: 4px; color: var(--ls-accent-deep);
  background: rgb(var(--ls-accent-rgb) / 0.14); white-space: nowrap; }
.dark .ls-bw-badge { color: var(--ls-accent-hi); }
.ls-bw-bar { height: 5px; border-radius: 999px; background: rgb(15 23 42 / 0.08); overflow: hidden; }
.dark .ls-bw-bar { background: rgb(255 255 255 / 0.09); }
.ls-bw-fill { height: 100%; border-radius: 999px; background: var(--ls-accent); transition: width .4s ease; }
.ls-bw-row.is-earned .ls-bw-fill { background: var(--ls-accent-deep); }
.ls-bw-row.is-earned, .ls-bw-row.is-done { opacity: .6; }
.ls-bw-nums { margin-top: 3px; font-size: 10.5px; font-weight: 700; color: var(--ls-ink);
  font-variant-numeric: tabular-nums; }
.ls-bw-of { color: var(--ls-ink-soft); font-weight: 600; }
.ls-bw-foot, .ls-cl-foot { padding: 3px 2px 0; font-size: 11px; font-weight: 700; color: var(--ls-ink-soft); }
.ls-bw-dot { opacity: .5; margin: 0 5px; }

/* ── QB Room ── */
.ls-qbr-head { display: flex; align-items: baseline; gap: 7px; margin-bottom: 5px; }
.ls-qbr-nm { font-size: 13.5px; font-weight: 700; color: var(--ls-ink); }
.ls-qbr-tm { font-size: 10px; font-weight: 800; color: var(--ls-ink-soft); }
.ls-qbr-fpts { margin-left: auto; font-size: 14px; font-weight: 800; color: var(--ls-accent-deep);
  font-variant-numeric: tabular-nums; }
.dark .ls-qbr-fpts { color: var(--ls-accent-hi); }
.ls-qbr-line { display: flex; flex-wrap: wrap; gap: 10px; font-size: 11px; color: var(--ls-ink-soft); }
.ls-qbr-line b { color: var(--ls-ink); font-variant-numeric: tabular-nums; }
.ls-qbr-line .is-bad b { color: #dc2626; }
.dark .ls-qbr-line .is-bad b { color: #fca5a5; }
.ls-qbr-bonus { margin-top: 7px; }
.ls-qbr-bar { height: 4px; border-radius: 999px; background: rgb(15 23 42 / 0.08); overflow: hidden; }
.dark .ls-qbr-bar { background: rgb(255 255 255 / 0.09); }
.ls-qbr-fill { height: 100%; background: var(--ls-accent); transition: width .4s ease; }
.ls-qbr-b-lbl { display: block; margin-top: 3px; font-size: 10px; color: var(--ls-ink-soft); }
.ls-qbr-card.is-live { border-color: rgb(var(--ls-accent-rgb) / 0.38); }
.ls-qbr-card.is-done { opacity: .65; }

/* ── D/ST Bracket ── */
.ls-dstb-head { display: flex; align-items: baseline; gap: 7px; margin-bottom: 5px; }
.ls-dstb-nm { font-size: 13px; font-weight: 700; color: var(--ls-ink); }
.ls-dstb-fpts { margin-left: auto; font-size: 13.5px; font-weight: 800; color: var(--ls-ink);
  font-variant-numeric: tabular-nums; }
.ls-dstb-pre { font-size: 11px; color: var(--ls-ink-soft); }
.ls-dstb-pa { display: flex; align-items: baseline; gap: 6px; }
.ls-dstb-pa-n { font-size: 22px; font-weight: 800; color: var(--ls-ink); font-variant-numeric: tabular-nums; }
.ls-dstb-pa-l { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ls-ink-soft); }
.ls-dstb-tier { margin-left: auto; font-size: 13px; font-weight: 800; color: var(--ls-accent-deep);
  font-variant-numeric: tabular-nums; }
.dark .ls-dstb-tier { color: var(--ls-accent-hi); }
.ls-dstb-next { margin-top: 3px; font-size: 10.5px; font-weight: 700; color: var(--ls-ink-soft); }
.ls-dstb-next.is-edge { color: #b45309; }
.dark .ls-dstb-next.is-edge { color: #fbbf24; }
.ls-dstb-card.is-edge { border-color: rgb(245 158 11 / 0.5); background: rgb(245 158 11 / 0.06); }
.ls-dstb-card.is-done { opacity: .65; }
.ls-dstb-stats { margin-top: 5px; display: flex; gap: 10px; font-size: 10.5px; color: var(--ls-ink-soft); }
.ls-dstb-stats b { color: var(--ls-ink); font-variant-numeric: tabular-nums; }
.ls-dstb-stats .is-good b { color: var(--ls-accent-deep); }
.dark .ls-dstb-stats .is-good b { color: var(--ls-accent-hi); }

/* ── Game Script ── */
.ls-gs-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 5px; }
.ls-gs-mu { font-size: 12.5px; font-weight: 800; color: var(--ls-ink); }
.ls-gs-sc { font-size: 12px; font-weight: 700; color: var(--ls-ink-soft); font-variant-numeric: tabular-nums; }
.ls-gs-tag { margin-left: auto; font-size: 9px; font-weight: 800; letter-spacing: .07em;
  padding: 1px 6px; border-radius: 4px; color: var(--ls-ink-soft); background: rgb(15 23 42 / 0.07); }
.dark .ls-gs-tag { background: rgb(255 255 255 / 0.09); }
.ls-gs-card.is-blowout .ls-gs-tag { color: #b45309; background: rgb(245 158 11 / 0.16); }
.ls-gs-card.is-tight .ls-gs-tag { color: var(--ls-accent-deep); background: rgb(var(--ls-accent-rgb) / 0.14); }
.dark .ls-gs-card.is-tight .ls-gs-tag { color: var(--ls-accent-hi); }
.ls-gs-note { font-size: 10.5px; color: #b45309; margin-bottom: 5px; }
.dark .ls-gs-note { color: #fbbf24; }
.ls-gs-sides { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.ls-gs-side-h { font-size: 9.5px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase;
  color: var(--ls-ink-soft); margin-bottom: 3px; }
.ls-gs-row { display: grid; grid-template-columns: 28px 1fr auto; align-items: center; gap: 6px; padding: 2px 0; }
.ls-gs-pos { font-size: 9px; font-weight: 800; color: var(--ls-ink-soft); }
.ls-gs-nm { font-size: 12px; font-weight: 600; color: var(--ls-ink); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.ls-gs-pts { font-size: 11.5px; font-weight: 800; color: var(--ls-ink); font-variant-numeric: tabular-nums; }
.ls-gs-none, .ls-cl-mine--none { font-size: 10.5px; color: var(--ls-ink-soft); opacity: .75; }
@media (max-width: 640px) { .ls-gs-sides { grid-template-columns: 1fr; } }

/* ── Category Leaders ── */
.ls-cl-cat { font-size: 9.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ls-ink-soft); margin-bottom: 3px; }
.ls-cl-lead, .ls-cl-mine { display: flex; align-items: baseline; gap: 7px; }
.ls-cl-mine { margin-top: 3px; padding-top: 3px; border-top: 1px dashed rgb(15 23 42 / 0.09); }
.dark .ls-cl-mine { border-top-color: rgb(255 255 255 / 0.09); }
.ls-cl-nm { font-size: 12.5px; font-weight: 700; color: var(--ls-ink); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.ls-cl-mine .ls-cl-nm { font-weight: 600; font-size: 11.5px; color: var(--ls-ink-soft); }
.ls-cl-tm { font-size: 9.5px; font-weight: 800; color: var(--ls-ink-soft); }
.ls-cl-yours { font-size: 8.5px; font-weight: 800; letter-spacing: .08em; padding: 1px 5px;
  border-radius: 3px; color: #fff; background: var(--ls-accent); }
.ls-cl-v { margin-left: auto; font-size: 12.5px; font-weight: 800; color: var(--ls-ink);
  font-variant-numeric: tabular-nums; white-space: nowrap; }
.ls-cl-mine .ls-cl-v { font-size: 11px; color: var(--ls-ink-soft); }
.ls-cl-mine-lbl { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ls-ink-soft); }
.ls-cl-gap { font-size: 10.5px; font-weight: 800; color: #b45309; font-variant-numeric: tabular-nums; }
.dark .ls-cl-gap { color: #fbbf24; }
.ls-cl-row.is-mine { border-color: rgb(var(--ls-accent-rgb) / 0.4); background: rgb(var(--ls-accent-rgb) / 0.05); }

/* ── Panel chrome gutter ───────────────────────────────────────────────────
   .ls-cell-chrome is position:absolute at the panel's top-right, so any widget
   row that reaches the right edge runs underneath it — the rightmost filter
   pill disappears behind the ⓘ, the vidi lane's scope tag collides with it.

   Three widgets each carried their own `pr-9` for this, all commented
   "reserves space for the floating cog". 36px was right when the cog was the
   only button; the ⓘ was added beside it (2026-06-21) and every one of those
   reservations was left 20px short (Xav 2026-08-17).

   So the gutter is DERIVED from the buttons actually rendered rather than
   guessed per widget, and it shrinks now that the cog only appears where there
   is something to configure — most panels reserve 28px, not 52px.

   Geometry: .ls-cell-chrome sits at right:14px, buttons are 20px with a 4px
   gap, and the widget's own padding (~8px) is already inside that span. */
/* Recomputed now the ⓘ has moved onto the title pill (2026-08-21). Most panels
   have NO chrome at all, so they reserve NOTHING — which is the real fix for the
   collisions: not a bigger gutter, one fewer floating control. */
.ls-cell { --ls-chrome-gutter: 0px; }
.ls-cell:has(.ls-cell-btn-cog),
.ls-cell:has(.ls-cell-btn-expand)     { --ls-chrome-gutter: 28px; }
.ls-cell:has(.ls-cell-btn-cog):has(.ls-cell-btn-expand) {
  --ls-chrome-gutter: 52px;
}
/* Put this on any widget row that reaches the panel's right edge. Falls back
   to the two-button width where the variable is out of scope (e.g. a widget
   rendered outside .ls-cell, as the Studio preview does). */
.ls-topgutter { padding-right: var(--ls-chrome-gutter, 52px) !important; }

/* The vidi lane head's scope tag ("Slate-wide" / "My players") is right-
   aligned by a flexible rule, so it always lands under the chrome. */
.vidi-lane-head { padding-right: var(--ls-chrome-gutter, 52px); }


/* ── Title pill as the info trigger ────────────────────────────────────────
   Deliberately NO visual change to the pill. The point of moving the info onto
   the title was to REMOVE a control from every panel, so styling the pill to
   advertise itself would put the noise straight back (Xav 2026-08-21: "didnt
   really want to change the look and feel of titles").

   Only the cursor changes, and only on hover — it costs nothing at rest and
   still tells you the pill does something. Focus keeps a ring because a
   keyboard user has no cursor to go on. */
/* pointer-events:auto is the whole reason this works. The pill is
   pointer-events:NONE by default (see .is-renamable above) so it never blocks
   the panel beneath — which meant the hover handlers I wired to it could not
   fire at all, and the title looked inert (Xav 2026-08-21). Safe to re-enable:
   the cell is only :draggable in edit mode, so there is nothing underneath in
   view mode for the pill to swallow. */
.ls-cell-title-pill.has-info,
.ls-kpi-label.has-info { cursor: help; pointer-events: auto; }
/* .ls-kpi-label does NOT carry pointer-events:none the way the panel pill does,
   so `auto` above is a no-op for it today — kept deliberately, so the KPI hover
   cannot be broken later by someone adding `none` to the base rule for the same
   don't-block-the-tile reason the panel pill has. */
.ls-cell-title-pill.has-info:focus-visible,
.ls-kpi-label.has-info:focus-visible {
  outline: 2px solid var(--ls-accent);
  outline-offset: 2px;
}
