/* ═══════════════════════════════════════════════════════════════════════
   v4.css — /dashboard-next only. Loads AFTER v3.css and overrides it.

   Scope: the SHELL. Header chrome, the new search + league rail + board,
   and the sticky run bar. Every result section below the board is left
   entirely to v3.css so the redesign can't change how results look.

   The legacy rail/hero/today-board are hidden here rather than deleted —
   v3.js still binds to their ids, and some of them (#matchupTitle) are
   written to unguarded. Hiding keeps that machinery intact.
   ══════════════════════════════════════════════════════════════════════ */

/* ── 1. Header ──────────────────────────────────────────────────────────
   v3.css owns the complete header contract. Keeping dashboard chrome here
   used to leak global overrides into /today and league pages. */

/* This page shrank its header logo, so match the footer mark to it here rather
   than to v3.css's larger default. */
/* margin-left cancels logo.png's baked-in transparent padding (0.219 x the
   rendered height) so the mark lines up with the footer links below it. */
.home-foot-logo                { height: 60px; max-width: 246px; margin-left: -13.1px; }
@media (min-width: 480px)  { .home-foot-logo { height: 63px; max-width: 262px; margin-left: -13.8px; } }
@media (min-width: 720px)  { .home-foot-logo { height: 71px; max-width: 298px; margin-left: -15.5px; } }
@media (min-width: 1020px) { .home-foot-logo { height: 76px; max-width: 317px; margin-left: -16.6px; } }

/* Dev-comfort chrome, not user value. The board carries freshness instead. */
.status-pill,
#cacheHint { display: none !important; }

.btn-login {
  padding: 7px 12px;
  font-size: 13px;
}

/* 15% down from the login button's size: it's the upgrade CTA, and next to a
   bigger logo it was competing with the brand for the top-right corner. */
.btn-upgrade-pro {
  padding: 6px 10px;
  font-size: 11px;
}

/* ── 1b. Sticky backgrounds must reach the screen edges ─────────────────
   .app is `width: min(100% - 24px, …); margin: 0 auto`, so every child stops
   12px short of each edge. A sticky element's background stopped there too,
   leaving two uncovered gutters that scrolling tiles showed through.

   Fixed with a bleeding pseudo-element rather than negative margins on the
   elements themselves: widening a sticky child re-introduces the horizontal
   overflow this file already fights, and rather than clip-path, which would
   also clip the absolutely-positioned settings dropdown anchored inside
   .top.

   The offset is exactly .app's gutter (it is `width: min(100% - 24px)`
   centred, so 12px a side). It used to be 20px, which overhung the viewport
   by 8px each side and pushed document.scrollWidth past the window - enough
   to stretch the fixed .api-progress bar and make the whole page read as
   too wide. Reach the edge, never past it.

   No negative z-index here, for the same reason as .top::before in v3.css:
   mobile Safari composites a sticky element separately while scrolling and
   a negative-z pseudo can trail it by a frame. The bar's own children are
   lifted above the backdrop instead (they're grid items, so z-index applies
   without position). */
.nx-sticky::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -12px;
  right: -12px;
  background: var(--page);
  pointer-events: none;
}

.nx-sticky > * { z-index: 1; }

/* ── 1c. Nothing may widen the page ──────────────────────────────────────
   Result sections carry tables with `min-width: 720px` (readable on desktop,
   scrolled inside their own `.data-table { overflow-x: auto }` wrapper). Any
   that ISN'T inside such a wrapper widens the layout, and because .app is
   `width: min(100% - 24px, …)` measured against that, every board tile grows
   with it — which is why a single wide table in a breakdown makes the whole
   board overflow the screen.

   Rather than chase each offender, clamp the containers: .canvas clips
   horizontally, so a stray wide table can never push the layout out. Wrappers
   that already scroll keep scrolling — this only catches the ones that don't.
   Cheap insurance; nothing legitimate on this page needs to exceed it. */
/* Match the landing page's root overflow contract. `hidden` creates a scroll
   container on iOS Safari and prevents position:sticky from reaching the
   viewport; `clip` contains wide results without breaking the shared header. */
html,
body {
  width: 100%;
  max-width: 100%;
  overflow-x: clip;
  overscroll-behavior-x: none;
}

.app,
.layout,
.nx,
.canvas { max-width: 100%; min-width: 0; }

/* The board and everything in it. A tile's min-content width is driven by its
   footer row — a nowrap gap chip next to the CTA — and without min-width: 0 a
   grid item takes that as its automatic minimum size. The tile then renders
   WIDER than the column it sits in: on desktop it spills across the breakdown,
   and on mobile it pushes past the screen edge. Cap every level, and let the
   footer wrap rather than demand the width. */
.nx-board-wrap,
.nx-board,
.nx-group,
.nx-tile,
.nx-open-pin { min-width: 0; max-width: 100%; }

/* WebKit uses an implicit `auto` column when a grid has no explicit template.
   Its width is then allowed to follow the widest min-content child even when
   the grid itself is capped at 100%. CFL is the one board that currently puts
   three labels in the win-probability legend (away / draw / home); on iOS the
   combined intrinsic width of that row can widen both nested grid tracks.
   Every sibling assigned to the track — including Latest sim — inherits the
   oversized width. A zero-minimum explicit track keeps content sizing inside
   the app column. */
.nx-board,
.nx-group,
.nx-wp { grid-template-columns: minmax(0, 1fr); }

.nx-tile-foot { flex-wrap: wrap; row-gap: 6px; }
.nx-tile-teams,
.nx-tile-team { min-width: 0; }

body.nx-game-open .canvas { overflow-x: clip; }

/* The box score is a grid of team blocks with no scroll wrapper of its own. */
.box-table,
.team-box { min-width: 0; max-width: 100%; }
.box-table { overflow-x: auto; }

/* ── 2. Hide the legacy shell (kept in DOM for v3.js) ────────────────── */

.rail,
.hero,
#todayBoardSection { display: none !important; }

/* ...except the hero, which comes back the moment a game is chosen. It carries
   the matchup lockup, the market counts and the run button, exactly as the
   current dashboard does — there's no reason to rebuild any of that. It stays
   hidden while browsing because "Select a matchup" above an empty canvas is
   noise. */
body.nx-game-open .hero { display: grid !important; }

/* The whole results column goes with it. A breakdown belongs to one game, so
   when that game isn't in the list on screen (different league, or filtered
   out by a search) its hero, market snapshot and result sections must not be
   sitting underneath a slate they have nothing to do with. */
body:not(.nx-game-open) .canvas { display: none !important; }

/* On phones the hero's run button is redundant with the pinned bottom bar, so
   the hero is header-only there. Hiding it is safe: the bar proxies clicks by
   calling .click() on this button, which works on a non-rendered element. */
@media (max-width: 1079px) {
  body.nx-game-open .hero-controls,
  body.nx-game-open .run-meta { display: none !important; }

  /* Once a game has been SIMULATED the breakdown is the page: un-pin the
     search + league rail and the latest-sim pin so they scroll away with the
     board rather than following you down. Merely opening a game doesn't do
     this — you're still choosing, and the navigation should stay put.
     Scroll up and it's all still there. */
  body.nx-sim-open .nx-sticky,
  body.nx-sim-open .nx-open-pin { position: static; }
  /* The rail's edge-bleed pseudo is absolutely positioned; once the parent is
     static it resolves against a different ancestor and paints over the board
     (the same failure the desktop split hit). Nothing to bleed when nothing
     is pinned. */
  body.nx-sim-open .nx-sticky::before { display: none; }
}

/* .hero-team-name carries `overflow: hidden` (for the ellipsis on long club
   names) inside a title set at line-height 1.04. That line box is shorter than
   the font's full descent, so the tails of g / y / p / j were being clipped.
   Give the name its own taller line box — the ellipsis still works. */
body.nx-game-open .hero-team-name {
  line-height: 1.22;
  padding-bottom: 0.06em;
}

/* The kicker above the matchup ("Props loaded" / "No market loaded") is
   loading chatter, not information — the matchup and its counts below already
   say the market arrived. Hidden rather than removed: v3.js writes to this
   element unguarded. */
#marketState { display: none !important; }

/* Matchup lockup 15% down from v3.css's sizes, crests included so the two
   scale together. v3.css: title clamp(30,4.6vw,50) desktop / clamp(22,7.5vw,32)
   under 640px, and .team-logo.xl 56px (44px under 720px). */
body.nx-game-open .hero-title { font-size: clamp(25.5px, 3.9vw, 42.5px); }
@media (max-width: 640px) {
  body.nx-game-open .hero-title { font-size: clamp(18.7px, 6.4vw, 27.2px); }
}

body.nx-game-open .hero-team .team-logo.xl {
  width: 47.6px;
  height: 47.6px;
  font-size: 11px;
}
@media (max-width: 720px) {
  body.nx-game-open .hero-team .team-logo.xl { width: 37.4px; height: 37.4px; }
}

.layout { grid-template-columns: minmax(0, 1fr) !important; gap: 18px; }

/* ── 3. Sticky search + league rail ──────────────────────────────────── */

/* minmax(0,1fr) caps the implicit column at the container width. Without it
   the full-bleed league rail below widens .nx, and because every child
   stretches to the grid column, the tiles inherit that extra width and hang
   off the right edge of the screen. */
.nx {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 14px;
}

.nx > * { min-width: 0; }

.nx-sticky {
  position: sticky;
  top: var(--header-h);
  z-index: 50;
  background: var(--page);
  padding: 8px 0 6px;
  display: grid;
  /* Same cap as .nx — the full-bleed league rail is a child of THIS grid, so
     without an explicit minmax(0,1fr) column it widens the sticky block,
     which widens the board, which pushes the tiles off the right edge. */
  grid-template-columns: minmax(0, 1fr);
  gap: 8px;
  min-width: 0;
}

.nx-sticky > * { min-width: 0; }

.nx-searchrow {
  display: flex;
  align-items: stretch;
  gap: 8px;
  min-width: 0;
}

.nx-search {
  position: relative;
  display: flex;
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;
}

/* ── "Live games" switch ─────────────────────────────────────────────────
   Off by default. Started games can't be simulated, so out of the box the
   board only shows what you can actually run; the counts on the league rail
   follow the same set, so a chip's number always matches its contents. */

.nx-live {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 0 11px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface);
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

/* Visually hidden but still focusable, so the switch is keyboard-operable. */
.nx-live input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}

.nx-live-track {
  position: relative;
  flex: 0 0 auto;
  width: 32px;
  height: 18px;
  border-radius: 999px;
  background: var(--line-strong);
  transition: background 160ms ease;
}

.nx-live-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
  transition: transform 160ms ease;
}

.nx-live input:checked + .nx-live-track { background: var(--positive); }
.nx-live input:checked + .nx-live-track .nx-live-thumb { transform: translateX(14px); }
.nx-live input:focus-visible + .nx-live-track { outline: 2px solid var(--accent); outline-offset: 2px; }

.nx-live-text {
  font-size: 9.5px;
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}

.nx-live input:checked ~ .nx-live-text { color: var(--positive); }

.nx-search-icon {
  position: absolute;
  left: 12px;
  color: var(--muted);
  pointer-events: none;
}

.nx-search-input {
  width: 100%;
  height: 44px;
  padding: 0 38px 0 38px;
  border: 1px solid var(--line-strong);
  border-radius: 12px;
  background: var(--surface);
  /* 16px minimum or iOS Safari zooms the viewport on focus. */
  font-size: 16px;
  color: var(--ink);
  -webkit-appearance: none;
  appearance: none;
}

.nx-search-input::-webkit-search-cancel-button { display: none; }
.nx-search-input::placeholder { color: var(--faint); }
.nx-search-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.nx-search-clear {
  position: absolute;
  right: 8px;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: var(--surface-3);
  color: var(--ink-2);
}

.nx-leagues {
  display: flex;
  gap: 2px;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
  /* Aligned to the content column rather than bleeding to the screen edge —
     the full-bleed version ran the tiles right up against the phone's bezel. */
  width: 100%;
  margin: 0;
  padding: 2px 0 4px;
}

.nx-leagues::-webkit-scrollbar { display: none; }


/* ── League rail scroll affordance ───────────────────────────────────────
   The rail has always been scrollable, but the hidden scrollbar left mouse
   users with no way to reach the leagues past the right edge on a narrow
   window. Arrows appear only when there's actually somewhere to scroll, and
   only for fine pointers — on touch you just swipe. */

.nx-leagues-wrap { position: relative; min-width: 0; }

.nx-rail-arrow {
  position: absolute;
  top: 0;
  bottom: 22px;
  z-index: 3;
  width: 34px;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 0;
  color: var(--ink);
  cursor: pointer;
}

.nx-rail-arrow.is-prev {
  left: 0;
  background: linear-gradient(to right, var(--page) 55%, rgba(247, 246, 241, 0));
  justify-content: flex-start;
}

.nx-rail-arrow.is-next {
  right: 0;
  background: linear-gradient(to left, var(--page) 55%, rgba(247, 246, 241, 0));
  justify-content: flex-end;
}

.nx-rail-arrow:hover { color: var(--accent-deep); }

/* Pointer-based input only, and only when JS says there's overflow. */
@media (pointer: fine) {
  .nx-rail-arrow:not([hidden]) { display: flex; }
}

/* App-drawer style: rounded icon tile with the label underneath. */
.nx-league {
  flex: 0 0 auto;
  scroll-snap-align: start;
  /* Wide enough for "All Sports" / "Serie A" at the label size below. */
  width: 54px;
  display: grid;
  justify-items: center;
  gap: 5px;
  padding: 0;
  border: 0;
  background: none;
}

.nx-league-icon {
  width: 39px;
  height: 39px;
  display: grid;
  place-items: center;
  border-radius: 11px;
  border: 1px solid var(--line);
  background: var(--surface);
  font-size: 19px;
  line-height: 1;
  transition: border-color 150ms ease, background 150ms ease;
}

.nx-league-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0;
  color: var(--ink-2);
  white-space: nowrap;
  max-width: 54px;
  overflow: hidden;
  text-overflow: ellipsis;
}

.nx-league-count {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--muted);
  margin-top: -3px;
}

.nx-league.is-active .nx-league-icon {
  border-color: var(--ink);
  box-shadow: 0 0 0 2px var(--ink);
  background: var(--surface);
}
.nx-league.is-active .nx-league-label { color: var(--ink); }

/* Out of season / no slate today: visible so the rail still advertises the
   full coverage, but clearly inert. */
.nx-league.is-empty { cursor: default; }
.nx-league.is-empty .nx-league-icon {
  filter: grayscale(1);
  opacity: 0.4;
  background: var(--surface-2);
}
.nx-league.is-empty .nx-league-label,
.nx-league.is-empty .nx-league-count { color: var(--faint); }

.nx-league-divider {
  flex: 0 0 auto;
  width: 1px;
  align-self: stretch;
  margin: 4px 6px 18px;
  background: var(--line-strong);
}

/* ── Mobile sizing pass ──────────────────────────────────────────────────
   Measured baselines were: search 44px tall, league icon 39px, and a 24px
   gap between the header and the search box (16px .app grid gap + 8px
   .nx-sticky padding). Desktop keeps the larger sizes. */
@media (max-width: 1079px) {
  /* 44 -> 35 (-20%). font-size stays 16px: anything smaller makes iOS Safari
     zoom the viewport on focus, which is far worse than the height saved. */
  .nx-search-input {
    height: 35px;
    border-radius: 10px;
  }
  .nx-search-clear { width: 24px; height: 24px; }

  /* 39 -> 35 (-10%). Tile width is unchanged so "All Sports" still fits. */
  .nx-league-icon {
    width: 35px;
    height: 35px;
    border-radius: 10px;
    font-size: 17px;
  }

  /* 24 -> 20 (-15%), taken off the sticky block's top padding. */
  .nx-sticky { padding-top: 4px; }

  /* Tighten the league rail down to the first league header. Three separate
     gaps stacked up here — the sticky block's bottom padding, .nx's grid gap,
     and the group header's own top margin. */
  .nx-sticky { padding-bottom: 2px; }
  .nx { gap: 6px; }
  .nx-group-head { margin: 12px 0 6px; }
  .nx-group-head:first-child { margin-top: 0; }
}


/* ── 5. Board + tiles ────────────────────────────────────────────────── */

.nx-board { display: grid; gap: 8px; }

/* ── Sport grouping ──────────────────────────────────────────────────
   Each league keeps an accent colour, used on the header chip and as a
   left edge on its tiles, so scrolling a mixed board makes the sport
   change obvious without reading the label. */

.nx-board { --lg: var(--muted); }

.nx-group-head[data-league="mlb"],  .nx-group[data-league="mlb"]  { --lg: #1c5d8c; }
.nx-group-head[data-league="nba"],  .nx-group[data-league="nba"]  { --lg: #b4531f; }
.nx-group-head[data-league="nhl"],  .nx-group[data-league="nhl"]  { --lg: #4a4a52; }
.nx-group-head[data-league="wnba"], .nx-group[data-league="wnba"] { --lg: #a3432f; }
.nx-group-head[data-league="cfl"],  .nx-group[data-league="cfl"]  { --lg: #2c6a4a; }
.nx-group-head[data-league="nfl"],  .nx-group[data-league="nfl"]  { --lg: #3b3f8f; }
.nx-group-head[data-league="mls"],  .nx-group[data-league="mls"]  { --lg: #7a3e8c; }

.nx-group-head {
  /* NOT sticky. It used to pin under the search block, which meant the league
     label floated over the tiles as they scrolled past — and once the
     latest-sim pin became sticky at nearly the same offset the two overlapped
     each other. It's a section label; it can scroll with its section. */
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 20px 0 8px;
  padding: 7px 2px;
  background: var(--page);
}
.nx-group-head:first-child { margin-top: 4px; }

.nx-group-name {
  display: inline-flex;
  align-items: center;
  padding: 4px 9px;
  border-radius: 6px;
  background: var(--lg);
  color: #fff;
  font-size: 11.5px;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.nx-group-rule {
  flex: 1;
  height: 1px;
  background: var(--line-strong);
}

.nx-group-count { font-family: var(--font-mono); font-size: 11px; color: var(--muted); }

.nx-group { display: grid; gap: 8px; }

.nx-tile {
  width: 100%;
  display: grid;
  gap: 8px;
  padding: 12px 13px;
  border: 1px solid var(--line-soft);
  border-radius: 12px;
  background: var(--surface);
  text-align: left;
  /* Comfortably above the 44px touch-target floor. */
  min-height: 76px;
}

.nx-tile:active { background: var(--surface-2); }

/* Started game: the lines it would simulate from are stale, so the tile is
   inert. The one exception is the game you already simulated — that stays
   openable so the run you spent a credit on is still readable. */
.nx-tile.is-locked {
  background: var(--surface-2);
  border-style: dashed;
  cursor: default;
}
.nx-tile.is-locked .nx-tile-team,
.nx-tile.is-locked .nx-tile-stats { color: var(--muted); }
.nx-tile.is-locked .nx-crest { opacity: 0.55; }
.nx-tile.is-locked .nx-wp-seg.is-away { background: var(--faint); }

.nx-tile-cta.is-locked {
  color: var(--muted);
  font-weight: 600;
}

.nx-tile-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.nx-tile-league {
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

.nx-tile-time {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--muted);
}

/* Starting soon: the one moment urgency is real and honest. */
.nx-tile-time.is-soon {
  color: var(--accent-deep);
  font-weight: 700;
}

.nx-tile-time.is-live {
  color: var(--negative);
  font-weight: 800;
  letter-spacing: 0.08em;
}

.nx-tile-time.is-final {
  color: var(--muted);
  font-weight: 750;
  letter-spacing: 0.08em;
}

.nx-live-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--negative);
  animation: nx-pulse 1.8s ease-in-out infinite;
}

@keyframes nx-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}

@media (prefers-reduced-motion: reduce) {
  .nx-live-dot { animation: none; }
}

/* Stat line: no "median" label — the numbers carry it. */
.nx-tile-stats {
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--muted);
}
.nx-tile-stats strong { color: var(--ink-2); font-weight: 700; }

/* League/hub tiles only — the dashboard tile renders no stats line at all.
   The 80% band is the whole line now; the median total was dropped because a
   single number reads as a prediction of the final total, which is not the
   claim. Label stays quiet so the numbers carry the emphasis. */
.nx-tile-range-label {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--muted);
  text-transform: uppercase;
}
.nx-tile-range {
  color: var(--ink);
  font-size: 12.5px;
  font-weight: 700;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--accent-soft);
  white-space: nowrap;
}

/* Tap affordance + depth teaser in one line. */
.nx-tile-cta {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  font-weight: 650;
  color: var(--ink-2);
}
.nx-tile-arrow { color: var(--accent); font-weight: 800; }

/* Locked gap indicator. Never used to sort the board. */
.nx-gap {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface-2);
  font-size: 11px;
  font-weight: 700;
  color: var(--muted);
  white-space: nowrap;
}

/* Pro: unlocked. Green rather than the gold used for locked/upsell states, so
   the difference reads at a glance across a whole board. */
.nx-gap.is-open {
  color: var(--positive);
  background: var(--positive-soft);
  border-color: var(--positive);
}

/* Pro tiles put the one action on the totals line rather than in a separate
   row below a divider — no second row, no rule across the tile. */
/* .nx-tile-statsrow is gone with the dashboard tile's stats line: it existed
   only to sit the Pro gap chip beside the total, and there is no total now. */
.nx-gap.is-open .nx-gap-arrow { color: var(--positive); }

.nx-gap-arrow { color: var(--accent-deep); font-weight: 800; }

/* Redacted value: reads as "there is a number here" without asserting what it
   is, which is the point — the tease costs nothing in accuracy. */
.nx-gap-redacted {
  width: 24px;
  height: 9px;
  border-radius: 3px;
  background: repeating-linear-gradient(
    -45deg,
    var(--line-strong) 0 3px,
    var(--surface-3) 3px 6px
  );
}

.nx-gap-lock { color: var(--muted); flex: 0 0 auto; }

.nx-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.nx-tile-teams { display: grid; gap: 2px; }

.nx-tile-team {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 15px;
  font-weight: 650;
  color: var(--ink);
  line-height: 1.25;
  min-width: 0;
}

.nx-tile-team > span:last-child {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Crest markup comes from v3.js teamLogo(): a .team-logo span wrapping an
   <img> with an initials <span> behind it as the fallback. Size it here. */
.nx-crest {
  width: 22px;
  height: 22px;
  flex: 0 0 22px;
  font-size: 9px;
}
.nx-crest img { width: 100%; height: 100%; object-fit: contain; }

/* Currently-open game, and the way back to it when a filter hides the tile. */
/* A real 2px border rather than 1px border + 1px box-shadow ring. The shadow
   version renders identically on mobile, but inside the desktop column
   (.nx-board-wrap has overflow-y: auto, which makes overflow-x compute to auto
   too) the ring is clipped on the left and right while the top and bottom
   survive — so the outline looked thinner on two sides. box-sizing is
   border-box, so the extra pixel costs nothing in layout. */
.nx-tile.is-open {
  border: 2px solid var(--accent);
}

.nx-open-pin {
  /* Rides just under the search + league block so the way back to your run is
     always reachable while browsing. Goes static once that run is the page
     you're looking at (see the nx-sim-open rules). */
  position: sticky;
  /* Flush against the sticky block above — any gap is a window tiles scroll
     through. The box-shadow paints page colour into the strip above and below
     the rounded rect for the same reason, so nothing shows past its corners. */
  top: calc(var(--header-h) + var(--nx-sticky-h, 96px));
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  margin-bottom: 4px;
  padding: 10px 12px;
  border: 1px solid var(--accent);
  border-radius: 10px;
  background: var(--accent-soft);
  box-shadow: 0 -7px 0 var(--page), 0 7px 0 var(--page);
  text-align: left;
}

.nx-open-pin-label {
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent-deep);
}

/* Single line, always. Truncating a long matchup is preferable to the pin
   growing to two rows above the board. */
.nx-open-pin { flex-wrap: nowrap; }

.nx-open-pin-teams {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  font-weight: 650;
  color: var(--ink);
}

.nx-open-pin-arrow { color: var(--accent-deep); font-weight: 800; }

.nx-tile-score { font-family: var(--font-mono); font-size: 14px; color: var(--ink-2); }

.nx-tile-foot {
  display: flex;
  align-items: center;
  gap: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--line-soft);
  font-size: 12px;
  color: var(--muted);
}

/* Simulate sits LEFT and Open breakdown RIGHT on every tile — dashboard and
   league hubs alike. The auto margin lives on the CTA, not on the chip: the
   chip comes first in DOM order, so pushing the trailing element right is what
   splits the row. Both renderers emit that order (dash-next.js tileHtml and
   board_tiles.tile), so this is the base rule rather than a modifier. */
.nx-tile-foot .nx-tile-cta { margin-left: auto; }

/* ...but ONLY the two-action row swaps. A Pro tile has no "Open breakdown" —
   the chip is the whole footer (dash-next.js marks that foot `is-solo`) — and
   it stays right-aligned exactly as it always was. Without this the chip falls
   back to flex-start and Pro's tile silently changes for a reason that has
   nothing to do with it. */
.nx-tile-foot.is-solo .nx-gap { margin-left: auto; }

/* Win-probability bar, only rendered when the toggle is on. */
.nx-wp { display: grid; gap: 4px; }
.nx-wp-track {
  display: flex;
  height: 5px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}
.nx-wp-seg { height: 100%; }
.nx-wp-seg.is-away { background: var(--ink); }
.nx-wp-seg.is-draw { background: var(--faint); }
.nx-wp-seg.is-home { background: var(--line-strong); }
.nx-wp-draw { color: var(--faint); }
.nx-wp-legend {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--muted);
}

.nx-wp-legend > span {
  min-width: 0;
  white-space: nowrap;
}

.nx-wp-legend > span:first-child,
.nx-wp-legend > span:last-child {
  overflow: hidden;
  text-overflow: ellipsis;
}

.nx-wp-legend > span:last-child { text-align: right; }

.nx-empty {
  padding: 22px 14px;
  border: 1px dashed var(--line-strong);
  border-radius: 12px;
  background: var(--surface-2);
  color: var(--muted);
  text-align: center;
  font-size: 13.5px;
}

.nx-empty strong { color: var(--ink); font-weight: 700; }

/* End-of-day board: the cached slate retains aggregate-safe context after the
   final pregame tile leaves, so a late visitor sees proof of the day's model
   coverage instead of a generic dead end. */
.nx-day-recap {
  position: relative;
  overflow: hidden;
  display: grid;
  justify-items: start;
  gap: 10px;
  padding: 24px 20px;
  border: 1px solid var(--line-strong);
  border-radius: 14px;
  background:
    radial-gradient(circle at 100% 0, var(--accent-soft) 0, transparent 46%),
    var(--surface);
}

.nx-recap-kicker {
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--accent-deep);
}

.nx-day-recap h2 {
  margin: 0;
  font-family: var(--font-serif);
  font-size: clamp(23px, 6vw, 32px);
  font-weight: 500;
  line-height: 1.08;
  letter-spacing: -0.025em;
}

.nx-day-recap > p {
  max-width: 620px;
  margin: 0;
  color: var(--muted);
  line-height: 1.55;
}

.nx-recap-metrics {
  width: 100%;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  margin: 4px 0;
}

.nx-recap-metric {
  min-width: 0;
  display: grid;
  align-content: center;
  gap: 2px;
  padding: 12px 10px;
  border: 1px solid var(--line-soft);
  border-radius: 10px;
  background: var(--surface-2);
}

.nx-recap-metric strong {
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: var(--font-mono);
  font-size: clamp(17px, 4.8vw, 23px);
  line-height: 1;
  color: var(--ink);
}

.nx-recap-metric span {
  font-size: 9.5px;
  font-weight: 700;
  line-height: 1.2;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}

.nx-day-recap .nx-recap-note { font-size: 12px; color: var(--faint); }

.nx-recap-signin {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  min-height: 42px;
  margin-top: 4px;
  padding: 9px 15px;
  border: 1px solid var(--ink);
  border-radius: 9px;
  background: var(--ink);
  color: #fff;
  font-weight: 700;
}

.nx-recap-google {
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: #fff;
  color: #4285f4;
  font-family: Arial, sans-serif;
  font-size: 12px;
  font-weight: 800;
}

.nx-recap-signin:hover { background: #000; }

@media (max-width: 380px) {
  .nx-day-recap { padding: 20px 16px; }
  .nx-recap-metrics { grid-template-columns: minmax(0, 1fr); }
  .nx-recap-metric { grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 10px; }
}

/* Shown only when v3.js's helpers never arrive — i.e. a cached v3.js from
   before they existed. Loud on purpose: the old failure was silent. */
.nx-stale {
  border-style: solid;
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--ink-2);
}

.nx-stale-btn {
  margin-top: 10px;
  padding: 8px 16px;
  border: 1px solid var(--ink);
  border-radius: 9px;
  background: var(--ink);
  color: #fff;
  font-size: 13px;
  font-weight: 700;
}

/* ── 6. Sticky run bar (thumb zone) ──────────────────────────────────── */

.nx-runbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 70;
  padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
  /* Solid, not a gradient. The caption sits in the bar's upper band, where a
     fade is still transparent — page content read straight through it. */
  background: var(--page);
  box-shadow: 0 -10px 24px -12px rgba(0, 0, 0, 0.22);
  display: flex;
  justify-content: center;
  pointer-events: none;
}

.nx-runbar-inner {
  pointer-events: none;
  width: min(100%, 460px);
  display: grid;
  justify-items: center;
  gap: 5px;
}

/* Context for the button above it, so a first-time visitor knows what pressing
   it costs before they press it. */
.nx-runbar-note {
  pointer-events: auto;
  margin: 0;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--muted);
  text-align: center;
}

.nx-runbar-btn {
  pointer-events: auto;
  width: 100%;
  min-height: 52px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  border: 1px solid var(--ink);
  border-radius: 14px;
  background: var(--ink);
  color: #fff;
  font-size: 15.5px;
  font-weight: 800;
  letter-spacing: -0.01em;
  box-shadow: 0 10px 30px -12px rgba(0, 0, 0, 0.55);
}

.nx-runbar-btn:hover { border-color: var(--ink); background: #000; }

.nx-runbar-btn[disabled] {
  background: var(--surface-3);
  border-color: var(--line-strong);
  color: var(--muted);
  box-shadow: none;
}

/* Out of credit. Gold rather than the neutral ink, because this is now an
   offer, not the action it replaced — and emphatically not greyed out. */
.nx-runbar-btn.is-upgrade,
.run-btn.is-upgrade {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.nx-runbar-btn.is-upgrade:hover,
.run-btn.is-upgrade:hover {
  background: var(--accent-deep);
  border-color: var(--accent-deep);
}

/* Reserve space for the fixed bar on the whole page, not just .canvas — the
   footer is a sibling of .layout, so padding the canvas left the footer's last
   lines sitting underneath the bar and unreadable. --nx-runbar-h is measured
   at runtime (dash-next.js), so the caption growing can't reintroduce this. */
body.nx-runbar-on.nx-game-open .app {
  padding-bottom: calc(var(--nx-runbar-h, 96px) + 24px);
}

/* The bar acts on the open game, so it has to leave with it. Otherwise it sits
   over a WNBA board still wired to simulate the MLB game you left behind. */
body:not(.nx-game-open) .nx-runbar { display: none !important; }

/* ── 6c. Market snapshot on phones ───────────────────────────────────────
   v3.css only gives the market card its three-column layout at ≥760px; below
   that the two teams and the total stack into a tall vertical column. Bring
   the desktop shape down to mobile — away | total | home, side by side — and
   shrink the contents to fit ~366px.

   Inside each ~145px card the Win / spread pair can't sit side by side any
   more, so that strip stacks instead; the team identity centres under its
   crest for the same reason. */

@media (max-width: 759px) {
  .consensus-teams {
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    align-items: stretch;
    gap: 7px;
  }

  .consensus-team { padding: 10px 8px; gap: 8px; }

  .team-role { font-size: 8.5px; letter-spacing: 0.1em; }

  .team-identity {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    text-align: center;
    gap: 5px;
  }
  .team-identity .team-logo { width: 24px; height: 24px; font-size: 9px; }
  .team-identity strong {
    font-size: 13px;
    line-height: 1.15;
    white-space: normal;      /* wrap rather than ellipsis in a narrow card */
    overflow: visible;
  }

  .team-stat-strip {
    grid-template-columns: minmax(0, 1fr);
    gap: 7px;
    padding-top: 8px;
    text-align: center;
  }
  .team-stat-strip > span + span {
    border-left: 0;
    padding-left: 0;
    border-top: 1px solid var(--line-soft);
    padding-top: 7px;
  }
  .team-stat-strip small { font-size: 8.5px; letter-spacing: 0.05em; }
  .team-stat-strip strong { font-size: 13px; }

  .consensus-center {
    min-width: 0;
    padding: 10px 6px;
  }
  .consensus-center .label-sm { font-size: 8.5px; letter-spacing: 0.08em; }
}

/* ── 7. Desktop ───────────────────────────────────────────────────────
   Two distinct states:

   BROWSING  — the board owns the full width and tiles flow into as many
               columns as fit.
   GAME OPEN — collapses to the classic split: a single-column list of that
               sport's games on the left, the breakdown on the right.

   The columns go on .nx-group, not .nx-board: the board's direct children are
   the league headers and per-league group wrappers, so columns there would lay
   out whole leagues side by side instead of games. */

@media (min-width: 900px) {
  .nx-group { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
  /* The bar spans the full browser width at every size — the button centres
     inside it, but the surface behind it runs edge to edge. */
  .nx-runbar { padding-bottom: 16px; }
}

@media (min-width: 1180px) {
  .nx-group { grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); }
}

@media (min-width: 1080px) {
  /* `display: contents` dissolves .nx so its children become grid items of
     .layout directly. That's what lets the search + league rail stay FULL
     WIDTH across the top while only the game list narrows into a column —
     they're nested inside .nx in the markup, so without this they'd be
     trapped in the narrow column and resize whenever a game opens. */
  body.nx-game-open .nx { display: contents; }

  body.nx-game-open .layout {
    grid-template-columns: 323px minmax(0, 1fr) !important;
    gap: 0 30px;
    align-items: start;
  }

  /* Full-width row above the split. Stays sticky to the page, and keeps its
     edge-bleed pseudo, because it's still a full-width sticky bar. */
  body.nx-game-open .nx-sticky {
    grid-column: 1 / -1;
    margin-bottom: 14px;
  }

  /* Left column: the slate, one game per row.
     No max-height and no internal scroll. Capping it to the viewport left only
     ~3 tiles visible on a laptop, with a hairline scrollbar most people never
     found — the rest of the slate was simply unreachable. Letting the column
     flow means the page scroll reaches every game, and it also stops the
     container from clipping a selected tile's outline. */
  body.nx-game-open .nx-board-wrap { grid-column: 1; }

  body.nx-game-open .nx-group { grid-template-columns: minmax(0, 1fr); }

  /* Group headers sit inside that column; pinning them there fights the page's
     own scroll. */
  body.nx-game-open .nx-group-head { position: static; }

  body.nx-game-open .canvas { grid-column: 2; }

  /* No pinned bottom bar on desktop — it's a thumb-zone device with no job on
     a pointer. The hero's own run button, at the top of the results column,
     is the action instead. */
  body.nx-game-open .nx-runbar { display: none !important; }

  body.nx-game-open .app { padding-bottom: 64px; }
}
