/* The props board.
 *
 * Density is the point: this is a research tool, and more rows on screen is
 * more useful than more whitespace. 52px rows, no zebra striping, thin
 * dividers, sticky header AND sticky first column so the player you are
 * reading stays anchored while you scroll sideways through twelve stat columns.
 */

/* The page column, capped and centred.
 *
 * Without a cap every page ran edge to edge, so on a maximised window the
 * header and the filter bar stretched to the full width while the board and
 * its rail -- both content-sized -- stopped where their content stopped. The
 * surplus piled up on the right and read as the rail having come unstuck.
 *
 * 1600 is from the arithmetic, not taste: the board's own columns come to
 * about 980 (70 rank + 400 player + 6 x 84), the gap is 16 and the rail takes
 * at most 420, which is 1416 inside 44px of padding. Everything on the page
 * now shares the same two edges at any width.
 *
 * It changes nothing below 1600, which is every laptop. */
.main {
  padding: var(--s4) var(--s5) var(--s7);
  min-width: 0;
  max-width: 1600px;
  margin-inline: auto;
  width: 100%;
}

.board-toolbar {
  position: sticky; top: var(--topbar-h); z-index: 30;
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2);
  padding: var(--s3) 0;
  background: var(--bg);
  box-shadow: 0 10px 12px -10px var(--bg);
}
.prop-count { font-size: var(--fs-sm); color: var(--dim); }

/* ---- The column legend --------------------------------------------------
 *
 * A "?" beside the heading rather than three lines of prose above the board.
 * The prose explained the columns a long way from the columns and cost a row
 * of screen on every visit after the first.
 *
 * Anchored here, outside .board-wrap, because that box scrolls in both axes
 * and would clip a popover anchored inside it.
 */
.legend-wrap { position: relative; display: inline-block; }
.legend-btn {
  width: 20px; height: 20px; border-radius: 50%;
  padding: 0; cursor: pointer; vertical-align: 3px;
  font-family: var(--font-num); font-size: 11px; font-weight: 700;
  line-height: 1;
  color: var(--dim); background: var(--panel-2);
  border: 1px solid var(--line-2);
}
.legend-btn:hover { color: var(--ink); border-color: var(--accent); }
.legend-btn[aria-expanded="true"] {
  color: var(--accent); border-color: var(--accent);
  background: var(--accent-weak);
}
.legend-pop {
  position: absolute; top: calc(100% + 8px); left: 0; z-index: 70;
  width: 340px; max-width: calc(100vw - 32px);
  padding: var(--s3);
  background: var(--panel-2);
  border: 1px solid var(--line-2); border-radius: var(--r-card);
  box-shadow: 0 12px 32px rgb(0 0 0 / 55%);
  /* The heading is display type; the legend is reading type. */
  font-family: var(--font); text-transform: none;
  letter-spacing: normal; font-weight: 400;
}
.legend-pop[hidden] { display: none; }
.legend-list { margin: 0; display: grid; gap: var(--s2); }
.legend-list > div { display: grid; grid-template-columns: 76px 1fr; gap: var(--s2); }
.legend-list dt {
  font-family: var(--font-display); text-transform: uppercase;
  letter-spacing: var(--track-lab); font-size: var(--fs-lab);
  color: var(--ink); font-weight: 700;
}
.legend-list dd {
  margin: 0; font-size: var(--fs-sm); color: var(--ink-2); line-height: 1.45;
}
.legend-foot {
  margin: var(--s3) 0 0; padding-top: var(--s2);
  border-top: 1px solid var(--line);
  font-size: var(--fs-micro); color: var(--faint);
}
@media (max-width: 860px) {
  /* Anchored to the VIEWPORT, not to the button.
   *
   * The popover hangs off .legend-wrap, which sits after the heading and its
   * prop count -- about 300px in on a phone. left:0 relative to that put a
   * 340px panel at x=300, so it ran 250px off the right edge and the page
   * grew a horizontal scrollbar. Paul: "extends to the right causing it to
   * overflow... should be covering the normal vertical screen."
   *
   * Fixed, pinned to both gutters, so it is a full-width sheet under the
   * heading and the page never scrolls sideways for it. */
  .legend-pop {
    position: fixed;
    left: var(--s3); right: var(--s3);
    top: auto; width: auto; max-width: none;
    max-height: 70vh; overflow-y: auto; overscroll-behavior: contain;
  }
  .legend-list > div { grid-template-columns: 76px 1fr; }
}

@media (max-width: 560px) {
  .legend-list > div { grid-template-columns: 1fr; gap: 2px; }
}

.board-wrap {
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  /* The table body scrolls inside here, in BOTH axes, with the header pinned to
   * the top of this container.
   *
   * This is not a style choice, it is forced: `overflow-x: auto` implicitly
   * makes overflow-y a scroll container too, which means `position: sticky` on
   * the header resolves against THIS box rather than the viewport. Sticking it
   * to the viewport while the box scrolls is not possible -- the header parked
   * 116px down inside the table. So the container owns the scrolling and the
   * header sticks to its top, which is also what every comparable board does.
   */
  overflow: auto;
  /* Set from JS, measured from where this box actually starts -- see
   * syncBoardHeight. The calc is the fallback for a first paint before the
   * module runs, and it is the one that was wrong: it knows about the topbar
   * and the toolbar but not about the page heading, the description or the
   * filter rows, so the box ran off the bottom of the screen and its scroll
   * ended early. */
  max-height: var(--board-max-h,
    calc(100vh - var(--topbar-h) - var(--toolbar-h, 60px) - var(--s6)));
  /* Keeps a wheel inside the board from running the page away underneath it.
   * Only safe while the box FITS: on a box that extends past the fold this is
   * what makes the last rows unreachable rather than merely awkward, because
   * the scroll stops dead instead of chaining to the page. syncBoardHeight
   * removes it in that case. */
  overscroll-behavior: contain;
}
/* Short screens: the page scrolls, the board does not. */
.board-wrap.board-wrap-flow {
  overscroll-behavior: auto;
  overflow-y: visible;
}
table.board {
  width: 100%; border-collapse: separate; border-spacing: 0;
  background: var(--panel);
  min-width: 1040px;
}

.board thead th {
  /* Offset by the toolbar's MEASURED height, not a guess. The toolbar wraps to
   * two rows at narrow widths, and a hardcoded offset then parks the header
   * on top of the first rows instead of above them. Set from JS. */
  /* Sticks to the top of .board-wrap, which is the scroll container. */
  position: sticky; top: 0; z-index: 20;
  background: var(--panel-2);
  border-bottom: 1px solid var(--line-2);
  padding: var(--s2) var(--s3);
  text-align: right; white-space: nowrap;
  font-size: var(--fs-lab); letter-spacing: var(--track-lab);
  text-transform: uppercase; color: var(--dim); font-weight: 600;
  cursor: pointer; user-select: none;
  transition: color var(--t-fast);
}
.board thead th:hover { color: var(--ink-2); }
.board thead th[aria-sort] { color: var(--ink); }

/* Sticky first column, so the player stays visible while scrolling sideways.
 * Width is fixed rather than fluid: left to the table layout it absorbed all
 * the slack and pushed the star button half a screen from the player name. */
.board th.c-line, .board td.c-line {
  position: sticky; left: 0; z-index: 10;
  width: 368px; min-width: 368px; max-width: 368px;
  text-align: left;
  background: var(--panel);
  border-right: 1px solid var(--line);
}
.board thead th.c-line { z-index: 25; background: var(--panel-2); }

.sort-rank {
  display: inline-grid; place-items: center;
  width: 15px; height: 15px; margin-left: 4px;
  border-radius: var(--r-chip);
  background: var(--accent); color: var(--accent-ink);
  font-family: var(--font-num); font-size: 9px; font-weight: 700;
}

.board tbody tr {
  height: var(--row-h);
  transition: background var(--t-fast);
}
.board tbody tr:hover, .board tbody tr:hover .c-line { background: var(--panel-3); }
.board td {
  padding: 0 var(--s3);
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-sm); text-align: right; white-space: nowrap;
  color: var(--ink-2);
}
.board tbody tr:last-child td { border-bottom: 0; }
:root[data-density="compact"] .board tbody tr { height: var(--row-h-compact); }

/* Identity cell: flex at every breakpoint, so the star never stacks below the
 * player and inflate the row. */
.board td.c-line {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s2); height: var(--row-h);
}
:root[data-density="compact"] .board td.c-line { height: var(--row-h-compact); }

.pcell { display: flex; align-items: center; gap: var(--s3); min-width: 0; }
.avatar {
  width: 32px; height: 32px; flex: none; border-radius: var(--r-pill);
  background: var(--panel-3);
  /* Team-coloured ring, set per row from data. */
  box-shadow: 0 0 0 2px var(--team, var(--line-2));
  display: grid; place-items: center;
  font-family: var(--font-num); font-size: var(--fs-micro);
  font-weight: 700; color: var(--ink-2);
}
.pmeta { min-width: 0; }
/* NEITHER the name nor the matchup is cut. The row wraps instead.
 *
 * This block has now been wrong in both directions. It began as one
 * overflow: hidden box with the matchup inside it, so the matchup -- being
 * last -- was what got cut: "Anthony EdwardsMIN", "VJ EdgecombePHI @ ".
 * The fix made the name the shrinking half, and on a phone that is not a
 * long-name problem, it is every name. Paul: "i just am not liking the
 * ... its almost everyplayer".
 *
 * There is no amount of width to divide here. At 390px, once the score
 * block and the photo have taken theirs, a full name and a matchup do not
 * both fit on one line for most players, so ellipsis on either half is just
 * a choice about which fact to destroy. Wrapping costs a row of height on
 * the narrow widths that need it and nothing at all on a desktop, and the
 * name is the thing you are scanning for.
 *
 * Nothing here ellipsizes. If this needs changing again, change the WIDTH,
 * not which half gets cut. */
.pname {
  display: flex; align-items: baseline; gap: 5px; flex-wrap: wrap;
  min-width: 0;
  /* Explicit, because nowrap is inherited from the cell. Without this the
     name cannot wrap and simply paints outside its box instead -- no
     ellipsis, but "Shai Gilgeous-Alexander" overlapping the matchup. */
  white-space: normal;
  font-weight: 600; font-size: var(--fs-sm); line-height: 1.2;
  color: var(--ink);
}
.pname .plink { min-width: 0; overflow-wrap: anywhere; }
.ppos, .pmatch {
  font-weight: 500; color: var(--dim); font-size: var(--fs-micro);
  text-transform: uppercase; letter-spacing: 0.03em;
}
.ppos { margin-left: 5px; }
.pmatch { flex: none; }
/* The second line carries the line value and stat, which is what the eye is
 * actually looking for. It gets its own row rather than competing with the
 * matchup for space. */
.psub {
  display: block; font-size: var(--fs-micro); line-height: 1.3; color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.psub .line-val { font-size: var(--fs-sm); }
.line-val { color: var(--accent); font-weight: 700; }

.book { display: inline-flex; align-items: baseline; gap: 4px; margin-right: var(--s2); }
.book-name {
  font-size: var(--fs-micro); color: var(--dim);
  text-transform: uppercase; letter-spacing: 0.04em;
}
.book.best .book-name { color: var(--accent); }
.more {
  padding: 2px 6px; border: 1px solid var(--line-2); border-radius: var(--r-chip);
  font-family: var(--font-num); font-size: var(--fs-micro);
  color: var(--ink-2); background: var(--panel-2);
}

.star {
  background: none; border: 0; padding: 3px; cursor: pointer;
  color: var(--faint); font-size: 14px; line-height: 1;
  transition: color var(--t-fast), transform var(--t-fast);
}
.star:hover { color: var(--accent); transform: scale(1.12); }
.star[aria-pressed="true"] { color: var(--accent); }

/* Inline context row, borrowed from Outlier: an injury warning that spans the
 * table costs no column and is impossible to miss. */
.board tr.context td {
  background: var(--panel-2);
  color: var(--neutral);
  font-size: var(--fs-micro);
  text-align: left;
  padding: var(--s2) var(--s3);
}

.mini { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.mini td { padding: var(--s2) 0; border-bottom: 1px solid var(--line); color: var(--ink-2); }
.mini tr:last-child td { border-bottom: 0; }
.mini td:first-child { color: var(--ink); }
.mini td:not(:first-child) { text-align: right; }

/* Skeleton rows while loading. */
.skel { display: block; height: 10px; border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--panel-2) 25%, var(--panel-3) 37%, var(--panel-2) 63%);
  background-size: 400% 100%; animation: shimmer 1.4s ease infinite; }
@keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }

/* ---- Mobile: card per row ------------------------------------------------ */
@media (max-width: 860px) {
  .main { padding: var(--s3) var(--s3) var(--s7); }
  .board-toolbar { top: 52px; }
  .board-wrap { border: 0; overflow: visible; max-height: none; }
  /* tfoot too. It was left out, so while every row became a block the Load
     more footer stayed a table-row sized to the table's natural width and
     hung 36px off the right edge -- the last thing still overflowing on the
     opportunity board after the min-width above was fixed. */
  .board, .board tbody, .board tfoot, .board tr, .board td {
    display: block; width: 100%;
  }
  /* table.board, not .board.
   *
   * The wide layout sets `table.board { min-width: 1040px }`, and an element
   * name beats a bare class however late the rule comes or what media query
   * it is in -- so this reset lost, every board stayed 1040px wide inside a
   * 362px card, and the opportunity board (whose wrapper does not scroll
   * here) spilled 666px off the right of the screen. Matching the same
   * selector shape is what wins it. */
  table.board { min-width: 0; }
  .board thead { display: none; }

  .board tbody tr {
    height: auto; background: var(--panel);
    border: 1px solid var(--line); border-radius: var(--r-card);
    margin-bottom: var(--s2); padding: var(--s3);
  }
  .board tbody tr:hover { background: var(--panel); }
  .board td.c-line {
    position: static; height: auto; border-right: 0;
    padding: 0 0 var(--s2); margin-bottom: var(--s2);
    border-bottom: 1px solid var(--line);
  }
  .board td:not(.c-line):not(.c-strip) { display: none; }
  /* Numbers collapse into one horizontally scrolling strip rather than
   * wrapping, so a card never grows taller than a thumb can reach. */
  .board td.c-strip {
    display: block; padding: 0; border-bottom: 0;
    overflow-x: auto; scrollbar-width: none;
  }
  .board td.c-strip::-webkit-scrollbar { display: none; }
  .strip { display: flex; gap: var(--s4); }
  .strip-item { flex: none; text-align: left; }
  .strip-item .lab { display: block; margin-bottom: 2px; }
  .star { font-size: 18px; padding: var(--s1); }
}


/* ---- Game strip ---------------------------------------------------------- */
.game-strip {
  display: flex; gap: var(--s2); overflow-x: auto;
  padding: var(--s3) 0 var(--s1);
  scrollbar-width: none;
}
.game-strip::-webkit-scrollbar { display: none; }
.game-chip {
  flex: none; text-align: left; cursor: pointer;
  padding: var(--s2) var(--s3);
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-chip);
  transition: background var(--t-fast), border-color var(--t-fast);
}
.game-chip:hover { background: var(--panel-2); border-color: var(--line-2); }
.game-chip[aria-pressed="true"] { background: var(--accent-weak); border-color: var(--accent); }
.game-chip .gm { display: block; font-size: var(--fs-sm); font-weight: 600; color: var(--ink); }
.game-chip .gt { display: block; font-size: var(--fs-micro); color: var(--dim); margin-top: 1px; }

/* ---- Pager ---------------------------------------------------------------
 * A floating bar under the board rather than a table footer, so it stays put
 * while the body scrolls inside its own container. */
.pager {
  display: flex; align-items: center; justify-content: center; gap: var(--s2);
  padding: var(--s4) 0 var(--s6);
  font-size: var(--fs-sm); color: var(--ink-2);
}

/* ---- Discovery board -----------------------------------------------------
 * Eight columns instead of twelve, and the four that matter most are hit-rate
 * windows. Odds live on the player page: the board answers "what should I look
 * at", and a price answers a question nobody is asking while scanning.
 */
.page-head { padding: var(--s5) 0 var(--s3); }
.page-head h1 { margin: 0 0 var(--s1); font-size: 34px; }
.page-sub { margin: 0; color: var(--ink-2); font-size: var(--fs-sm); max-width: 62ch; }

/* The page key.
 *
 * Replaces the paragraph that used to sit here. A paragraph under a heading is
 * the shape of an explanation, and nobody reads an explanation on a board they
 * came to scan -- so the terms are listed the way a box score or a racing form
 * lists them: term, then what it means, in a row you can skim sideways.
 *
 * Each term names a COLUMN that is visible on the same screen, which is the
 * rule for what earns a place here. Anything that is not a column is not a key
 * entry and belongs in the one plain sentence above it, or nowhere.
 */
.page-key {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s5);
  margin: var(--s3) 0 0; padding: 0;
}
.page-key > div { display: flex; align-items: baseline; gap: var(--s2); }
.page-key dt {
  font-family: var(--font-display); text-transform: uppercase;
  letter-spacing: var(--track-lab); font-size: var(--fs-lab);
  color: var(--ink); font-weight: 600; white-space: nowrap;
}
.page-key dd {
  margin: 0; color: var(--dim); font-size: var(--fs-sm);
}
/* A thin rule between entries, so the pairs read as a list and not as one
 * run-on sentence. Not on the first, and not on a wrapped row's first item. */
.page-key > div + div { border-left: 1px solid var(--line); padding-left: var(--s5); }

@media (max-width: 860px) {
  .page-key { gap: var(--s1) var(--s3); }
  .page-key > div + div { border-left: 0; padding-left: 0; }
  .page-key > div { flex: 1 1 100%; }
}

/* `display: flex` beats the browser's own [hidden] rule, so a bar switched
 * off with the attribute stayed on screen. The value page hides its compare
 * filters on the de-vig view, which has nothing to filter, and they sat there
 * looking live. The attribute has to win. */
.filterbar[hidden] { display: none; }
/* Busy, not broken. The controls stay enabled and their clicks are applied
   once the data lands -- this only says the wait is real. */
.filterbar[aria-busy="true"] { opacity: 0.72; cursor: progress; }
.filterbar[aria-busy="true"] .segb,
.filterbar[aria-busy="true"] .seg-select { cursor: progress; }
.filterbar {
  position: sticky; top: var(--topbar-h); z-index: 30;
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s5);
  padding: var(--s3) 0;
  background: var(--bg);
  box-shadow: 0 10px 12px -10px var(--bg);
}
.filter-group { display: flex; align-items: center; gap: var(--s2); min-width: 0; }
.filter-group.grow { flex: 1; min-width: 180px; }
.filter-group > .lab { flex: none; }

/* .seg and .segb live in app.css.
 *
 * They were here, and board.css is not loaded by the player page -- so the
 * Average/Median toggle on that page rendered as two pieces of plain text
 * with no pressed state at all. Paul: "when you push median the supporting
 * stat number changes but its not showing that thats whats highlited".
 * The number moved and nothing said which button had done it.
 *
 * One copy, in the file every page loads. The board-specific variants below
 * still override it: app.css is loaded first, so this file wins on a tie. */
/* What is applied, and the way out of it. Sits under the bar rather than in
 * it: the bar is the controls, this is the state they produced, and mixing
 * them is what makes a filter bar grow a third row. */
.active-filters {
  display: flex; flex-wrap: wrap; gap: var(--s2);
  padding: var(--s2) 0 0;
}
.afc {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px var(--s2);
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
  font-size: var(--fs-micro); font-weight: 600;
  cursor: pointer;
}
.afc:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }
/* "Clear all" is an action, not a state, so it does not wear the accent. */
.afc-clear {
  background: none; border-color: var(--line-2); color: var(--ink-2);
}
.afc-clear:hover { background: var(--panel-3); color: var(--ink); }

.filter-input {
  width: 100%; padding: 6px var(--s3);
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-chip);
  color: var(--ink); font: inherit; font-size: var(--fs-sm); outline: 0;
}
.filter-input:focus { border-color: var(--accent); }

.board.discover { min-width: 840px; }
.board.discover thead th.c-rank, .board.discover td.c-rank {
  position: sticky; left: 0; z-index: 10;
  width: 70px; min-width: 70px; text-align: center;
  background: var(--panel); border-right: 0;
}
.board.discover thead th.c-rank { z-index: 25; background: var(--panel-2); }
.board.discover thead th.c-prop, .board.discover td.c-prop {
  position: sticky; left: 70px; z-index: 10;
  width: 400px; min-width: 400px; max-width: 400px;
  text-align: left; background: var(--panel);
  border-right: 1px solid var(--line);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s2); height: var(--row-h);
}
/* The header keeps the body cell's `display: flex`.
 *
 * It used to be forced back to `table-cell` here, and that one word made the
 * header and the body obey two different layout algorithms. The table is
 * `table-layout: fixed` and wider than its declared columns, so the browser
 * hands the slack out proportionally: the header cell, being a real table
 * cell, grew with its column to 495px, while the body cell, being a flex
 * container, held the 400px max-width it was given. The Player header then
 * ran 70px into the MIN header and -- having the higher z-index -- painted
 * over it, so MIN was simply missing from the board, with a sliver of its
 * last letter showing. Both are flex now, both are 400px, and they line up. */
.board.discover thead th.c-prop { z-index: 25; background: var(--panel-2); }
/* The players board pins its first column to the LEFT EDGE, not to 70px.
 *
 * Every other board has a 70px score or edge column before the player, and
 * the sticky offset is set for them. The players index starts with the player
 * -- there is no rank column -- so a 70px offset slid the sticky cell 70px
 * right of its own column and it covered the MIN header. Which is why MIN was
 * missing on this board and on no other. */
.board.discover.players thead th.c-prop,
.board.discover.players td.c-prop { left: 0; }
.board.discover tbody tr:hover td.c-rank,
.board.discover tbody tr:hover td.c-prop { background: var(--panel-3); }

/* The score is the first thing read, so it is the largest thing in the row. */
.score {
  display: inline-grid; place-items: center;
  min-width: 38px; padding: 4px 6px;
  border-radius: var(--r-chip);
  font-family: var(--font-num); font-size: var(--fs-lg); font-weight: 600;
}
.score.heat-5 { color: var(--heat-5); background: color-mix(in srgb, var(--heat-5) calc(var(--heat-tint)*100%), transparent); }
.score.heat-4 { color: var(--heat-4); background: color-mix(in srgb, var(--heat-4) calc(var(--heat-tint)*100%), transparent); }
.score.heat-3 { color: var(--heat-3); background: color-mix(in srgb, var(--heat-3) calc(var(--heat-tint)*100%), transparent); }
.score.heat-2 { color: var(--heat-2); background: color-mix(in srgb, var(--heat-2) calc(var(--heat-tint)*100%), transparent); }
.score.heat-1 { color: var(--heat-1); background: color-mix(in srgb, var(--heat-1) calc(var(--heat-tint)*100%), transparent); }

.pstat { color: var(--ink); font-weight: 600; }
.psub .pill { margin-left: var(--s1); }

.empty-row { text-align: center; padding: var(--s7) var(--s4); }
.empty-row strong { display: block; margin-bottom: var(--s3); color: var(--ink); font-weight: 600; }

.demo-note {
  margin: var(--s4) 0 var(--s7);
  padding: var(--s3) var(--s4);
  border-left: 2px solid var(--neutral);
  background: var(--panel);
  border-radius: 0 var(--r-chip) var(--r-chip) 0;
  color: var(--ink-2); font-size: var(--fs-sm); max-width: 78ch;
}

@media (max-width: 860px) {
  .page-head h1 { font-size: 26px; }
  .filterbar { gap: var(--s3); padding: var(--s2) 0; }
  .filter-group { width: 100%; }
  .seg { overflow-x: auto; flex-wrap: nowrap; scrollbar-width: none; }
  .seg::-webkit-scrollbar { display: none; }
  .board.discover { min-width: 0; }
  .board.discover td.c-rank {
    position: static; display: inline-block; width: auto; min-width: 0;
    padding: 0 0 var(--s2);
  }
  .board.discover td.c-prop {
    position: static; width: auto; min-width: 0; max-width: none;
    height: auto; border-right: 0;
  }
}


/* ---- Discovery board, tightened -----------------------------------------
 * The hit-rate windows are a GROUP and must read as one. Left to the table
 * layout they stretched to fill the row and the eye had to travel the width of
 * the screen to compare L5 with L20, which is the comparison the board exists
 * to make.
 */
/* The min-width applies only on a wide screen. Declared here without a query
 * it sat AFTER the mobile block that zeroes it, won on source order, and left
 * a 760px table inside a 390px phone -- which is what made every board page
 * scroll sideways. On a phone the rows become cards and need no min-width at
 * all. */
@media (min-width: 861px) {
  /* Wide enough that the columns never have to compress.
   *
   * 760px was below every board's own arithmetic, and the first column is
   * PINNED: it is sticky at a declared 400px whatever the table does. So on a
   * table squeezed below its declared widths, `table-layout: fixed` shrank
   * the Player COLUMN while the sticky CELL stayed 400px, and the cell ran
   * over the next two columns -- which is what hid the MIN header on a
   * 1200px screen even after the header and body were brought back into
   * agreement.
   *
   * Each board gets the sum it actually needs: the pinned columns at their
   * declared widths, plus 84px per numeric column. Below that the board
   * scrolls sideways, which is honest, and the fade added earlier says so. */
  .board.discover { min-width: 890px; }    /* 70 rank + 400 prop + 5 x 84 */
  .board.discover.sim { min-width: 974px; }    /* 70 + 400 + 6 x 84 */
  .board.discover.players { min-width: 988px; }    /* 400 + 7 x 84 */
  .board.discover.disc { min-width: 890px; }    /* 70 + 400 + 5 x 84 */
}
/* The table sizes to its own columns, not to the card.
 *
 * `width: 100%` plus `table-layout: fixed` meant the browser had 294px of
 * slack to hand out on a 1462px screen -- and it gave it to the player
 * column, whose cell is `display: flex` with an explicit 400px. So the COLUMN
 * became 694px wide while the CELL stayed 400, and the difference showed as a
 * blank strip between the player and the first hit rate. The header and body
 * agreed with each other the whole time, which is why the earlier alignment
 * work did not catch it.
 *
 * `width: auto` gives the slack back: the table is exactly 70 + 400 + 5x84
 * and the columns sit against each other. min-width still forces the table
 * wider than a narrow screen, so the sideways scroll on a small laptop is
 * unaffected. */
/* Natural width, with the games rail taking the space beside it.
 *
 * `width: 100%` plus `table-layout: fixed` gave the browser slack to hand
 * out, and because the player cell is a flex box rather than a table cell it
 * went to a column that could not use it -- a hole in the middle, then a dead
 * strip down the right. Neither was a layout. The table is now exactly as
 * wide as its columns and .board-and-rail fills the rest with the slate. */
.board.discover { table-layout: fixed; width: auto; }
/* CENTRED, not right-aligned.
 *
 * The values in these columns are pills with a fixed min-width and their text
 * centred inside. Right-aligning the cell puts the pill's right EDGE at the
 * column edge, while the number sits centred within the pill -- so the number
 * ended up 14-19px left of its own header. Centring both puts the header over
 * the number rather than over the pill's edge. (The Score column was already
 * centred, and was already the only one that lined up.) */
.board.discover th:not(.c-rank):not(.c-prop),
.board.discover td:not(.c-rank):not(.c-prop) {
  width: 84px; padding: 0 var(--s1); text-align: center;
}
.board.discover th.c-sim, .board.discover td.c-sim { width: 84px; padding-left: var(--s3); }
/* A rule before the model column separates "what happened" from "what we think
 * will happen". */
.board.discover th.c-sim, .board.discover td.c-sim { border-left: 1px solid var(--line); }

/* The denominator is evidence, not a footnote: 5/5 and 13/20 are the whole
 * difference between a strong claim and a weak one. It was too dim to read. */
.heat-n { color: var(--ink-2); opacity: 0.85; font-weight: 500; }

.heat { padding: 2px 6px; min-width: 56px; text-align: center; }

/* The side, as a filled chip rather than coloured words.
 *
 * On the de-vig table the side was the only thing in the row that was plain
 * text, next to a column of hit-rate chips -- so the eye read the numbers and
 * skipped the one word that says which way the bet goes. Same shape as .heat,
 * same weight, so the row scans as one kind of thing. */
.ou-chip {
  display: inline-block;
  padding: 2px 6px; min-width: 72px;
  text-align: center; border-radius: var(--r-chip);
  font-family: var(--font-num); font-size: var(--fs-sm); font-weight: 700;
  letter-spacing: var(--track-lab);
}
.ou-chip.side-over {
  color: var(--over);
  background: color-mix(in srgb, var(--over) calc(var(--heat-tint)*100%), transparent);
}
.ou-chip.side-under {
  color: var(--under);
  background: color-mix(in srgb, var(--under) calc(var(--heat-tint)*100%), transparent);
}
.board.discover td .heat { display: inline-block; }

.rowend { display: inline-flex; align-items: center; gap: var(--s1); flex: none; }
.mod-dot {
  font-family: var(--font-num); font-size: var(--fs-micro); font-weight: 600;
  padding: 1px 5px; border-radius: var(--r-chip);
  border: 1px solid currentColor;
}
.mod-blowout {
  color: var(--neutral);
  border-color: color-mix(in srgb, var(--neutral) 55%, transparent);
  letter-spacing: var(--track-lab);
}
.mod-goblin { color: var(--goblin); }
.mod-demon { color: var(--demon); }
.src {
  font-size: var(--fs-micro); color: var(--faint);
  border: 1px solid var(--line-2); border-radius: 3px; padding: 0 3px; margin-left: 3px;
}

/* ESPN headshots are 600x436 with the player's head in the upper portion and a
 * lot of empty space below, so a default centre crop shows chest and
 * background. Crop toward the top and zoom slightly so the face fills the ring.
 */
.facewrap {
  position: relative;
  width: 34px; height: 34px; flex: none;
  border-radius: var(--r-pill);
  background: var(--panel-3);
  box-shadow: 0 0 0 2px var(--team, var(--line-2));
}
.facewrap img.avatar {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  display: block; object-fit: cover; object-position: 50% 14%;
  border-radius: var(--r-pill);
  box-shadow: none; background: none;
  transform: scale(1.3); transform-origin: 50% 20%;
  /* The ring must clip the zoom. */
  clip-path: circle(50% at 50% 50%);
}
/* Initials are a FALLBACK and must stay hidden while a photo is present.
 *
 * They were originally stacked under the photo so a failed image would reveal
 * them. That backfired: ESPN headshots are only 31-42% opaque, so the initials
 * showed straight through the transparent background and every player had
 * their own monogram printed across their face. Shown only when the image
 * actually fails. */
.facefallback {
  position: absolute; inset: 0;
  display: none; place-items: center;
  font-family: var(--font-num); font-size: var(--fs-micro);
  font-weight: 700; color: var(--ink-2);
  border-radius: var(--r-pill);
}
.facewrap.noface .facefallback { display: grid; }
.facewrap.noface img.avatar { display: none; }

/* Team crest, bottom-right, the way a broadcast graphic badges a player. */
.teamlogo {
  position: absolute; right: -3px; bottom: -3px;
  width: 15px; height: 15px;
  border-radius: var(--r-pill);
  background: var(--bg);
  box-shadow: 0 0 0 1.5px var(--bg);
  object-fit: contain;
}

/* Explains the score without a tooltip nobody hovers. */
.score-key {
  display: flex; align-items: center; gap: var(--s2);
  font-size: var(--fs-micro); color: var(--dim);
}

/* A board row opens the player. The name is a real link so it can be tabbed to
 * and opened in a new tab; the row click is a convenience on top of it. */
.board.discover tbody tr[data-href] { cursor: pointer; }
.board.discover .plink { color: inherit; }
.board.discover tbody tr:hover .plink { color: var(--accent); }

.page-head h1 .count {
  font-family: var(--font-num); font-size: var(--fs-sm);
  font-weight: 500; color: var(--dim);
  letter-spacing: 0; text-transform: none;
  margin-left: var(--s2);
}

/* A promotional free square. Amber rather than teal or coral: it is neither a
 * good nor a bad side, it is a different kind of thing. */
.mod-dot.mod-free {
  color: var(--neutral);
  border-color: color-mix(in srgb, var(--neutral) 45%, transparent);
  background: color-mix(in srgb, var(--neutral) 14%, transparent);
}

/* ---- Discrepancies ------------------------------------------------------ */
.disc .c-edge { width: 74px; }
.edge {
  display: inline-block; min-width: 46px; padding: 4px 8px;
  border-radius: var(--r-chip);
  font-family: var(--font-num); font-weight: 700; font-size: var(--fs-body);
  text-align: center;
}
.edge-over  { background: color-mix(in srgb, var(--over) 18%, transparent);
              color: var(--over); }
.edge-under { background: color-mix(in srgb, var(--under) 18%, transparent);
              color: var(--under); }
.c-consensus .micro { display: block; color: var(--faint); }
.push-tag {
  font-size: 9px; font-weight: 700; text-transform: uppercase;
  letter-spacing: var(--track-lab);
  padding: 1px 5px; border-radius: var(--r-pill);
  color: var(--neutral);
  border: 1px solid color-mix(in srgb, var(--neutral) 42%, transparent);
}
.agree    { color: var(--over); font-weight: 600; }
.disagree { color: var(--neutral); font-weight: 600; }

/* The per-book breakdown, tucked under each row. */
.books-row td { padding-top: 0; border-top: 0; }
.books { display: flex; flex-wrap: wrap; gap: var(--s2); }
.bk {
  display: inline-flex; align-items: baseline; gap: 5px;
  padding: 2px var(--s2);
  border: 1px solid var(--line); border-radius: var(--r-chip);
  background: var(--panel-2); font-size: var(--fs-micro);
}
.bk-name { color: var(--ink-2); }
.bk-line { color: var(--ink); font-weight: 600; }
.bk-price { color: var(--dim); }
.needs { display: block; color: var(--dim); font-weight: 600; }

/* ---- Player index ------------------------------------------------------- */
.players .hot-usage { color: var(--ink); font-weight: 600; }
.prop-count {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  background: var(--accent-weak);
}

/* ---- Sim Board ----------------------------------------------------------- */
.sim .sim-prob { font-weight: 700; color: var(--ink); }
.sim .gap { display: block; font-weight: 600; }
.sim .gap.pos { color: var(--over); }
.sim .gap.neg { color: var(--under); }

/* ---- Goblin / demon ladder ---------------------------------------------- */
/* The counts were a dead end for the whole life of the board: a row said
 * "4G 4D" and there was nowhere in the product those lines appeared. They are
 * buttons now, and this is what they open. */
button.mod-dot {
  background: none; cursor: pointer; font: inherit;
  font-family: var(--font-num); font-size: var(--fs-micro); font-weight: 600;
}
button.mod-dot:hover,
button.mod-dot[aria-expanded="true"] {
  background: color-mix(in srgb, currentColor 16%, transparent);
}
button.mod-dot:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.ladder-row > td { padding: 0; background: var(--panel-2); }
.ladder {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px;
  padding: 10px 14px;
  border-top: 1px solid var(--line-2);
  border-bottom: 1px solid var(--line-2);
}
.ladder-head { font-size: var(--fs-micro); color: var(--ink-2); font-weight: 600; }
.ladder-main {
  font-family: var(--font-num); color: var(--faint); margin-left: 6px; font-weight: 400;
}
.ladder-key { font-size: var(--fs-micro); color: var(--faint); margin-left: auto; }

.rungs { display: flex; flex-wrap: wrap; gap: 6px; list-style: none; margin: 0; padding: 0; }
.rung {
  display: inline-flex; align-items: baseline; gap: 6px;
  padding: 3px 8px; border-radius: var(--r-chip);
  border: 1px solid var(--line-2); background: var(--panel);
  font-family: var(--font-num); font-size: var(--fs-micro);
}
.rung-kind { font-weight: 700; }
.rung-line { font-weight: 700; font-size: var(--fs-body); }
.rung-price { color: var(--faint); }
.rung-ours { color: var(--ink); font-weight: 600; }
.rung-edge { font-weight: 700; }

/* Our chance against what the price needs. Green is only earned by a real
 * gap -- anything inside two points is flat, because the model's own band gate
 * allows up to five. */
.rung-good { border-color: color-mix(in srgb, var(--over) 55%, transparent); }
.rung-good .rung-edge { color: var(--over); }
.rung-bad { border-color: color-mix(in srgb, var(--under) 45%, transparent); }
.rung-bad .rung-edge { color: var(--under); }
.rung-flat .rung-edge { color: var(--faint); }

@media (max-width: 700px) {
  .ladder { gap: 8px 10px; padding: 9px 12px; }
  .ladder-key { display: none; }
}

/* On a phone every row becomes a card and `.board td:not(.c-line):not(.c-strip)`
 * hides everything else -- which hid the ladder's cell too, so tapping a badge
 * opened a 26px empty strip. A control that visibly does nothing is the exact
 * complaint the ladder was built to answer.
 *
 * The selector below is deliberately long, and the length is load-bearing.
 * Specificity counts the classes inside :not(), so there are two rules to beat
 * and both are scored higher than they look:
 *
 *   .board td:not(.c-line):not(.c-strip)                 -> 3 classes, hides it
 *   .board.discover td:not(.c-rank):not(.c-prop)         -> 4 classes, 84px wide
 *
 * `.board td.c-ladder` loses to both and `.board tr.ladder-row td.c-ladder`
 * still loses the width to the second, which is how this cell came back
 * visible at 84px with the rungs stacked in a column. Four classes, and the
 * `.discover` scope, are the minimum that wins. */
@media (max-width: 860px) {
  /* `.board tr { display: block }` in the block above also beats the browser's
   * own `[hidden] { display: none }` -- an author rule outranks the user agent
   * whatever the specificity -- so every ladder on the page rendered OPEN on a
   * phone, fifty of them, and the badges toggled an attribute with no visible
   * effect.
   *
   * Missed when the mobile layout was fixed, because the check only ever
   * measured the ONE ladder it had just opened. "What I opened is visible" and
   * "what I did not open is not" are two assertions; only the first was made. */
  .board tr.ladder-row[hidden] { display: none; }

  .board.discover tr.ladder-row td.c-ladder {
    display: block; padding: 0; width: 100%; text-align: left;
  }
  .board tbody tr.ladder-row {
    margin-top: calc(var(--s2) * -1);
    padding: 0;
    border-top: 0;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
  }
  .ladder-row > td { background: transparent; }
  .ladder { border: 0; padding: 10px 12px; }
  /* The card above squares off so the two read as one object. */
  .board tbody tr:has(+ .ladder-row:not([hidden])) {
    margin-bottom: 0;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
  }
}

/* ---- Live model record --------------------------------------------------- */
/* Closing line value, coloured only where it is earned. A CLV sitting at zero
 * is the expected result for a model that is an honest mirror of the market
 * rather than an edge over it, and painting that green would be the page
 * flattering itself. */
.clv-good { color: var(--over); font-weight: 700; }
.clv-bad { color: var(--under); font-weight: 700; }
.clv-flat { color: var(--dim); }
.model-table tfoot td {
  border-top: 1px solid var(--line-2);
  padding-top: var(--s2);
  font-weight: 600;
}

/* ---- Sortable headers ---------------------------------------------------- */
/* These have carried `cursor: pointer` since the first commit without doing
 * anything. Now they sort, and they say which way. */
.board thead th[data-sort] { position: relative; }
.board thead th[data-sort]:hover { color: var(--ink); }
.board thead th[data-sort]:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px;
}
.board thead th.is-sorted { color: var(--ink); }
.board thead th.is-sorted::after {
  content: "\25BC";           /* filled triangle, down = highest first */
  font-size: 8px;
  margin-left: 4px;
  color: var(--accent);
  display: inline-block;
}
.board thead th.is-sorted[data-dir="asc"]::after { content: "\25B2"; }

/* ---- Load more ----------------------------------------------------------- */
/* The board held 50 rows of 451 and the players index 60 of 596, and neither
 * ever asked for the rest: scrolling stopped with hundreds of names behind it
 * and nothing on screen admitting it. */
.board tfoot .more-cell {
  padding: var(--s3);
  background: var(--panel);
  border-top: 1px solid var(--line-2);
  text-align: center;
}
.board tfoot .btn { max-width: 320px; margin: 0 auto; }

/* ---- Opportunity board: the move columns -------------------------------
 *
 * A move is signed and coloured only when it is big enough to mean something.
 * Below the threshold it is drawn flat, because a 0.2-minute drift painted
 * green invites a read the data cannot support, and a board where every row
 * is coloured is a board where no colour means anything.
 */
.mv {
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.mv-up   { color: var(--over); }
.mv-down { color: var(--under); }
.mv-flat { color: var(--faint); font-weight: 400; }

/* A sortable column says so before it is clicked. */
.board th[data-sort] { cursor: pointer; user-select: none; }
.board th[data-sort]:hover { color: var(--ink); }
.board th[aria-sort="descending"]::after { content: " \25BC"; font-size: 8px; }
.board th[aria-sort="ascending"]::after  { content: " \25B2"; font-size: 8px; }

.f-input {
  background: var(--panel-2); color: var(--ink);
  border: 1px solid var(--line-2); border-radius: var(--r-chip);
  padding: 6px var(--s3); font-size: var(--fs-sm); width: 150px;
}
.more-row { padding: var(--s4) 0; background: var(--panel); }
.board tfoot .more-row { border-top: 1px solid var(--line); }
.more-anchor {
  position: sticky; left: 0;
  display: flex; justify-content: center; width: 100vw; max-width: 100%;
}

/* ---- Opportunity board on a phone --------------------------------------
 *
 * The props board turns each row into a card and hides every cell except the
 * line and the number strip. This table has neither, so those rules emptied
 * it completely -- rows rendered as blank bars. It opts out and lays itself
 * out instead: the player across the top, the figures in a wrapping grid
 * underneath, each one labelled, because a column header that has been
 * display:none is a label the reader no longer has.
 */
@media (max-width: 860px) {
  .board.opportunity td:not(.c-line):not(.c-strip) { display: block; }
  .board.opportunity tbody tr {
    display: grid;
    /* minmax(0, 1fr) rather than 1fr: a bare 1fr has an automatic MINIMUM of
       its content's width, so a long number pushed the row wider than the
       card and the last column fell off the right of the screen. Two columns
       because five figures in three leaves a ragged orphan. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s2) var(--s3); align-items: start;
    box-sizing: border-box; max-width: 100%;
  }
  .board.opportunity td.c-prop { grid-column: 1 / -1; padding: 0 0 var(--s2); }
  .board.opportunity td.num {
    text-align: left; padding: 0; border-bottom: 0;
  }
  /* The move has its own sortable column on a wide screen. Here it rides
     inside the value cell instead, so the separate column is redundant.
     The :not() pair is not decoration: specificity counts the classes INSIDE
     :not(), so the display:block rule above scores higher than a plain
     .board.opportunity td.mv-col and the hidden column kept showing as a
     row of unlabelled numbers. Matching the chain is what wins it. */
  .board.opportunity td.mv-col:not(.c-line):not(.c-strip) { display: none; }
  .board.opportunity td[data-lab]::before {
    content: attr(data-lab); display: block;
    font-family: var(--font-display); text-transform: uppercase;
    letter-spacing: var(--track-lab); font-size: var(--fs-micro);
    color: var(--dim); margin-bottom: 1px;
  }
}

/* On a wide screen the move lives in its own column, so the copy carried
   inside the value cell would print it twice. */
@media (min-width: 861px) {
  .board.opportunity td[data-lab] .mv { display: none; }
}

/* Shown rather than hidden. Last season's numbers for a player nobody has
   signed are still real numbers, and quietly dropping him would leave a hole
   with no explanation; saying so costs one word. */
.off-roster {
  color: var(--neutral); font-size: var(--fs-micro);
  text-transform: uppercase; letter-spacing: var(--track-lab);
}

/* A payout we know the DIRECTION of but not the size.
 *
 * The feed assigns every goblin and demon a flat placeholder price, so the
 * only honest thing to print is which way it moves. Set in the modifier's own
 * colour so the row still reads at a glance, and quieter than a real number
 * so it does not look like one. */
.pay-less, .pay-more {
  font-family: var(--font); font-size: var(--fs-micro);
  text-transform: uppercase; letter-spacing: var(--track-lab);
  cursor: help;
}
.pay-less { color: var(--goblin); }
.pay-more { color: var(--mod-demon, var(--demon)); }

/* ======================================================================
 * Phones held upright
 * ======================================================================
 *
 * Measured rather than guessed. On /players the seven numeric columns are
 * 84px each -- 588px of columns before the name is drawn at all -- inside a
 * 390px screen. So every board ran about 250px off the right edge, and only
 * the props board pinned anything, which meant that on Players, Sim Board and
 * Discrepancies the name scrolled away too and left a grid of numbers
 * belonging to nobody. In landscape it all fits, which is exactly why it
 * looked perfect sideways and broken upright.
 *
 * These rules come AFTER the 860px block above, which deliberately unpins the
 * props board for its own card layout. Equal specificity, later source: that
 * is the whole reason they are at the end of the file rather than merged into
 * it. Reordering this file will break them silently.
 *
 * The widths below were chosen by probing the real table at a 390px container
 * until the overflow reached zero. 116 + 5x52 = 376 in a 388px box.
 */
@media (max-width: 860px) {
  .board.players, .board.sim, .board.disc {
    min-width: 0; table-layout: fixed; width: 100%;
  }

  /* 1. PIN THE IDENTITY. This is the fix; the rest is polish. */
  .board.sim thead th.c-edge, .board.sim td.c-edge,
  .board.disc thead th.c-edge, .board.disc td.c-edge {
    position: sticky; left: 0; z-index: 10;
    width: 54px; min-width: 54px; max-width: 54px;
    padding: 0 2px; background: var(--panel);
  }
  .board.players thead th.c-prop, .board.players td.c-prop {
    position: sticky; left: 0; z-index: 10;
  }
  .board.sim thead th.c-prop, .board.sim td.c-prop,
  .board.disc thead th.c-prop, .board.disc td.c-prop {
    position: sticky; left: 54px; z-index: 10;
  }
  .board.players thead th.c-prop, .board.players td.c-prop,
  .board.sim thead th.c-prop, .board.sim td.c-prop,
  .board.disc thead th.c-prop, .board.disc td.c-prop {
    width: 116px; min-width: 116px; max-width: 116px;
    background: var(--panel); border-right: 1px solid var(--line-2);
    text-align: left;
  }
  /* The header crosses both axes, so it needs the higher stacking order and
     an opaque surface of its own or rows scroll through it. */
  .board.players thead th.c-prop,
  .board.sim thead th.c-edge, .board.sim thead th.c-prop,
  .board.disc thead th.c-edge, .board.disc thead th.c-prop {
    z-index: 25; background: var(--panel-2);
  }
  /* A pinned cell paints over the row behind it, so it has to repeat the
     hover colour or it stays dark while the rest of the row lights up. */
  .board.players tbody tr:hover td.c-prop,
  .board.sim tbody tr:hover td.c-edge, .board.sim tbody tr:hover td.c-prop,
  .board.disc tbody tr:hover td.c-edge, .board.disc tbody tr:hover td.c-prop {
    background: var(--panel-3);
  }

  .board.players th.num, .board.players td.num,
  .board.sim th.num, .board.sim td.num,
  .board.disc th.num, .board.disc td.num {
    width: 52px; padding: 0 2px; text-align: center;
  }

  /* 2. SAY THAT IT SCROLLS -- see app.css. The rule lives there because the
     player page does not load this stylesheet and its prop tabs scroll too. */
}

/* 3. CARRY LESS. Dropped on what survives being read one-handed, not on what
 * is cheapest to hide: the leading score, who it is, and the numbers the board
 * is sorted by. Everything hidden here is still on the player page. */
@media (max-width: 640px) {
  .board.players th:nth-child(4), .board.players td:nth-child(4),   /* FP/MIN */
  .board.players th:nth-child(5), .board.players td:nth-child(5),   /* FP */
  .board.sim th:nth-child(5), .board.sim td:nth-child(5),           /* Band */
  .board.sim th:nth-child(6), .board.sim td:nth-child(6),           /* Min */
  .board.disc th:nth-child(7), .board.disc td:nth-child(7) {        /* Model */
    display: none;
  }
}

/* ======================================================================
 * The props card on a phone: show the numbers
 * ======================================================================
 *
 * The card showed the score and the player and NOTHING else. Every hit-rate
 * window and the model's own number were display:none -- not by intent but by
 * accident: `.board td:not(.c-line):not(.c-strip)` hides anything that is not
 * one of two classes, and no board has rendered either of those for a long
 * time. The rule outranks the discover block that follows it (four classes
 * against three, because specificity counts what is inside :not), so the
 * cells stayed hidden and the card became a name with a number beside it.
 *
 * They come back as a row of small boxes under the player, which is the whole
 * reason to open the board on a phone: five numbers, read at a glance,
 * without scrolling sideways to find them.
 *
 * The :not() chain is repeated here deliberately. A plainer selector loses to
 * the rule above it and this stops working with no visible error.
 */
@media (max-width: 860px) {
  .board.discover td.num:not(.c-line):not(.c-strip) {
    display: inline-flex; flex-direction: column; align-items: center;
    gap: 1px;
    width: auto; min-width: 54px; flex: 1 1 54px;
    padding: var(--s1) 2px;
    border-radius: var(--r-chip);
    background: var(--panel-2);
  }
  /* A GRID, not a wrapping flex row.
   *
   * Flex gave three ragged bands -- the score orphaned on its own line
   * hugging the left, then the player, then the numbers -- with nothing
   * sharing an edge. Five equal columns put the score directly above the L5
   * box at the same width, the player across the remaining four, and the five
   * windows one per column underneath. Everything lines up because it is the
   * same grid, not because the paddings were tuned until it looked close. */
  .board.discover tbody tr {
    display: grid;
    /* minmax(0, 1fr), not a bare 1fr. A bare 1fr takes its CONTENT's width as
       an automatic minimum, and the player cell spans columns 2 to 5 with a
       name, a matchup, a promo tag and a star in it -- so those four tracks
       were sized to fit that, the grid came out wider than the phone, and the
       SIM box fell off the right. The page then inherited the overflow, which
       is why the subhead ran off the edge as well.
       The same mistake is documented on the opportunity board above; this is
       the board it was never applied to. */
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: var(--s1); align-items: center;
  }
  .board.discover td.c-rank:not(.c-line):not(.c-strip) {
    grid-column: 1; grid-row: 1;
    display: flex; align-items: center;
    position: static; width: auto; min-width: 0; padding: 0; margin: 0;
  }
  .board.discover td.c-prop:not(.c-line):not(.c-strip) {
    grid-column: 2 / -1; grid-row: 1;
    position: static; width: auto; min-width: 0; max-width: none;
    height: auto; border-right: 0; padding: 0;
  }
  /* :not(.c-rank) is load-bearing. The score cell is <td class="c-rank num">
     -- it carries BOTH classes -- so a plain td.num rule claimed it for row
     two, which put the score back beside the windows instead of beside the
     player AND pushed the model's number into a sixth, implicit column 54px
     wide. One exclusion fixes both. */
  .board.discover td.num:not(.c-rank):not(.c-line):not(.c-strip) {
    grid-row: 2;
  }

  /* The label the hidden table header used to supply. Without it five
     percentages in a row are five numbers with no idea which window each
     one is. */
  .board.discover td.num[data-lab]::before {
    content: attr(data-lab);
    font-family: var(--font-display); text-transform: uppercase;
    letter-spacing: var(--track-lab); font-size: 9px;
    color: var(--dim); line-height: 1;
  }
  /* THE BOX IS TINTED, not just the number.
   *
   * The percentage was coloured and the box was not, so the denominator
   * underneath sat on flat panel grey and could not be read -- and that is
   * the half that makes the number mean anything, because 5/5 and 18/20 are
   * different claims. Same ramp the wide board already puts on the cell; it
   * had simply never been applied here. */
  .board.discover td.num:has(.heat-5) { background: color-mix(in srgb, var(--heat-5) 15%, var(--panel-2)); }
  .board.discover td.num:has(.heat-4) { background: color-mix(in srgb, var(--heat-4) 15%, var(--panel-2)); }
  .board.discover td.num:has(.heat-3) { background: color-mix(in srgb, var(--heat-3) 15%, var(--panel-2)); }
  .board.discover td.num:has(.heat-2) { background: color-mix(in srgb, var(--heat-2) 15%, var(--panel-2)); }
  .board.discover td.num:has(.heat-1) { background: color-mix(in srgb, var(--heat-1) 15%, var(--panel-2)); }

  .board.discover td.num .heat {
    padding: 0; min-width: 0; background: none; font-size: var(--fs-body);
  }
  /* Evidence, not a footnote. Lifted off --faint, which was unreadable on a
     tinted box, and sized to be read at arm's length. */
  .board.discover td.num .heat-n {
    font-size: 10px; color: var(--ink-2); font-weight: 500;
  }
  /* The model's own number. A rule rather than a wash: the brown tint read as
     muddy rather than as "this one is different", and all it has to do is
     separate what happened from what we think will happen. */
  .board.discover td.c-sim:not(.c-line):not(.c-strip) {
    border-left: 2px solid var(--line-2);
    border-radius: 0 var(--r-chip) var(--r-chip) 0;
    background: var(--panel-2);
  }
}

/* ---- Depth chart --------------------------------------------------------
 * A roster, not a board: no numbers, so nothing needs aligning in columns and
 * the layout can simply flow. Teams side by side on a wide screen, one per
 * row on a phone.
 */
/* The page is now two SECTIONS -- tonight, then everybody else -- so the
   outer element stacks and each section carries its own grid. It was one
   grid of thirty equal cards, which is exactly the wall of text this
   replaced. */
.dc-teams { display: grid; gap: var(--s6); }
.dc-section-head {
  display: flex; align-items: baseline; gap: var(--s2);
  margin-bottom: var(--s3);
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--line);
}
.dc-section-head h2 {
  margin: 0; font-size: var(--fs-lg);
  letter-spacing: var(--track-display); text-transform: uppercase;
}
.dc-section-head .micro { margin-left: auto; }

/* A team playing tonight holds two columns of names, so it needs roughly
   twice the width a single roster did before it will sit beside another. */
.dc-section > .dc-tonight { margin-bottom: var(--s4); }
.dc-section:has(> .dc-tonight) {
  display: grid; gap: var(--s4);
}
.dc-section-head { grid-column: 1 / -1; }
@media (min-width: 1100px) {
  .dc-section:has(> .dc-tonight) {
    grid-template-columns: repeat(auto-fill, minmax(560px, 1fr));
    align-items: start;
  }
  .dc-section > .dc-tonight { margin-bottom: 0; }
}

/* ---- In and Out, side by side ------------------------------------------
 *
 * The answer the page is opened for. Two columns at every width above a
 * phone: on a phone they stack, because two columns of names in 180px each
 * is worse than one column of names.
 */
.dc-cols {
  display: grid; gap: var(--s3);
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: start;
}
.dc-col {
  border: 1px solid var(--line); border-radius: var(--r-card);
  overflow: hidden; min-width: 0;
}
.dc-col-head {
  display: flex; align-items: center; gap: var(--s2);
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--line);
  background: var(--panel-2);
}
.dc-col-lab {
  font-family: var(--font-display); text-transform: uppercase;
  letter-spacing: var(--track-lab); font-size: var(--fs-lab);
  font-weight: 700;
}
.dc-col-n { margin-left: auto; color: var(--ink-2); font-size: var(--fs-sm); }
.dc-col-in .dc-col-lab { color: var(--over); }
.dc-col-in .dc-col-head {
  box-shadow: inset 0 -2px 0 color-mix(in srgb, var(--over) 45%, transparent);
}
.dc-col-out .dc-col-lab { color: var(--under); }
.dc-col-out .dc-col-head {
  box-shadow: inset 0 -2px 0 color-mix(in srgb, var(--under) 45%, transparent);
}
.dc-col-empty {
  margin: 0; padding: var(--s3);
  font-size: var(--fs-sm); color: var(--faint);
}
/* The columns are narrower than the full-width list was, so the rows give
   back the padding they no longer have room for. */
.dc-cols .dc-link { padding-left: var(--s3); padding-right: var(--s3); }
.dc-gtd-note {
  margin: var(--s3) 0 0; font-size: var(--fs-micro); color: var(--faint);
}

/* ---- The teams that are not playing -------------------------------------
 *
 * Shut by default and one line each, so twenty-six of them cost a screen
 * rather than the page. The summary keeps the counts, because "3 out" is
 * most of what anybody wants from a team they are not betting tonight.
 */
.dc-rest-list {
  display: grid; gap: var(--s2);
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  align-items: start;
}
.dc-rest[open] { grid-column: 1 / -1; }
.dc-summary {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s2) var(--s3); min-height: 44px;
  cursor: pointer; list-style: none;
}
.dc-summary::-webkit-details-marker { display: none; }
.dc-summary:hover { background: var(--panel-2); }
.dc-sum-abbr {
  font-family: var(--font-display); font-weight: 700;
  font-size: var(--fs-sm); letter-spacing: var(--track-lab);
  min-width: 34px; color: var(--ink);
}
.dc-sum-name {
  font-size: var(--fs-sm); color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dc-rest .micro { flex: none; }
.dc-rest[open] .dc-summary {
  border-bottom: 1px solid var(--line); background: var(--panel-2);
}
.dc-match {
  font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--ink-2);
}
.dc-team .card-head { gap: var(--s2); flex-wrap: wrap; }
.dc-flags { margin-left: auto; display: flex; gap: var(--s1); }
.dc-flag {
  font-size: var(--fs-micro); font-weight: 700;
  text-transform: uppercase; letter-spacing: var(--track-lab);
  padding: 2px 6px; border-radius: var(--r-chip);
}
.dc-flag.dc-out { color: var(--under); background: color-mix(in srgb, var(--under) 16%, transparent); }
.dc-flag.dc-questionable { color: var(--neutral); background: color-mix(in srgb, var(--neutral) 16%, transparent); }

.dc-list { list-style: none; margin: 0; padding: 0; }
.dc-player + .dc-player { border-top: 1px solid var(--line); }
.dc-link {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s2) var(--s4); min-height: 48px;
  color: inherit; text-decoration: none;
}
.dc-link:hover { background: var(--panel-3); }
.dc-face {
  width: 34px; height: 34px; border-radius: var(--r-pill); flex: none;
  object-fit: cover; background: var(--panel-2);
}
.dc-noface {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--fs-micro); font-weight: 700; color: var(--dim);
}
.dc-meta { display: grid; gap: 1px; min-width: 0; }
.dc-name {
  font-weight: 600; color: var(--ink); font-size: var(--fs-sm);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dc-sub {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  font-size: var(--fs-micro); color: var(--dim);
}
.dc-pos {
  font-family: var(--font-display); text-transform: uppercase;
  letter-spacing: var(--track-lab); color: var(--ink-2); font-weight: 600;
}
.dc-jersey { font-family: var(--font-num); }
.dc-note { color: var(--ink-2); }

.dc-badge {
  margin-left: auto; flex: none;
  font-size: var(--fs-micro); font-weight: 700;
  text-transform: uppercase; letter-spacing: var(--track-lab);
  padding: 3px 7px; border-radius: var(--r-chip);
}
/* The row is tinted as well as badged. A badge alone is easy to miss in a
   list of twenty, and roughly one man in twelve cannot separate the colours,
   so both carry the message. */
.dc-player.dc-out .dc-badge {
  color: var(--under); background: color-mix(in srgb, var(--under) 18%, transparent);
}
.dc-player.dc-out .dc-name { color: var(--ink-2); }
.dc-player.dc-out .dc-link { background: color-mix(in srgb, var(--under) 7%, transparent); }
.dc-player.dc-questionable .dc-badge {
  color: var(--neutral); background: color-mix(in srgb, var(--neutral) 18%, transparent);
}
.dc-player.dc-questionable .dc-link { background: color-mix(in srgb, var(--neutral) 6%, transparent); }

.dc-only {
  display: inline-flex; align-items: center; gap: var(--s2);
  font-size: var(--fs-sm); color: var(--ink-2); cursor: pointer;
}
/* Said out loud, because a status feed that stopped looks exactly like a
   league with nobody hurt. */
.dc-stale { color: var(--neutral); font-weight: 600; }

@media (max-width: 860px) {
  /* Two columns of names in 180px each is worse than one column of names,
     so In and Out stack -- In first, because most of a roster is in it and
     scrolling past the out list to reach it would be backwards. */
  .dc-cols { grid-template-columns: minmax(0, 1fr); }
  .dc-rest-list { grid-template-columns: minmax(0, 1fr); }
  .dc-section-head h2 { font-size: var(--fs-md); }
}

/* ---- Filters on a phone -------------------------------------------------
 *
 * Thirteen prop chips and sixteen team chips is 32 controls, wrapping into
 * rows that ate the screen before a single prop was visible. On a wide screen
 * the chips are faster than any menu and nothing changes. On a narrow one the
 * four markets people actually open keep their buttons and the rest move into
 * the menu beside them; teams drop to the menu entirely, because sixteen of
 * them is a list pretending to be buttons.
 *
 * Both controls set the same filter and each shows what the other chose, so
 * there is no state here -- only which of the two is on screen.
 */
.seg-select {
  display: none;
  background: var(--panel-2); color: var(--ink);
  border: 1px solid var(--line-2); border-radius: var(--r-chip);
  padding: 6px var(--s3); font: inherit; font-size: var(--fs-sm);
  min-height: 34px; max-width: 100%;
}
.seg-select:focus { border-color: var(--accent); outline: 0; }

/* A group with no primary chips is a MENU, at every width.
 *
 * This was scoped to phones, on the reasoning that chips are faster than a
 * menu when there is room. Thirty of them is not faster than anything -- the
 * team filter wrapped into two rows of chips that pushed the board most of a
 * screen down, and the comment in props.html already said sixteen teams is
 * "a list pretending to be buttons". It is just as true at 1500px.
 *
 * Only groups that declare no primary chips are affected. Night, Prop and
 * Lines keep theirs, because those are few and genuinely faster to hit. */
.seg-select { display: inline-block; }
.seg[data-chips="none"] .segb { display: none; }

/* A group whose chips already show EVERY option does not also need a menu.
 *
 * Night rendered "All | Tonight" as buttons with a dropdown beside it
 * offering All and Tonight again -- two controls for one filter, which is
 * how it reached Paul as "Tonight appears twice". They stay in sync and
 * always did (press() writes both), so this was never a state bug, only two
 * ways to say the same thing at once.
 *
 * Set from JS, not guessed here, because only the JS knows how many options
 * a group has: Prop caps at six chips out of seventeen markets, so its menu
 * is the ONLY way to reach the other eleven and must stay. The flag is set
 * when the chips cover everything and none of them is a secondary that a
 * phone hides -- see seg() in props.html. */
.seg[data-chips="all"] .seg-select { display: none; }

@media (max-width: 860px) {
  /* On a phone the secondary chips go too, leaving the four primaries and
     the menu beside them. */
  .seg .segb-extra { display: none; }
  /* The row no longer needs to scroll sideways: what is left fits. */
  .seg { overflow-x: visible; flex-wrap: wrap; }
}

/* The split between the projected five and everybody else. A band rather than
   a heading: it groups the rows under it without competing with the team name
   above, which is the same device the matchup panel uses. */
.dc-group {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s2); padding: var(--s1) var(--s4);
  background: var(--panel-2); border-top: 1px solid var(--line);
  font-family: var(--font-display); text-transform: uppercase;
  letter-spacing: var(--track-lab); font-size: var(--fs-lab);
  color: var(--ink-2);
}
.dc-group-note {
  text-transform: none; letter-spacing: 0; font-family: var(--font);
  font-size: var(--fs-micro); color: var(--dim);
}
.dc-group + .dc-player { border-top: 0; }
/* How often he has started, beside the name rather than under it: it is
   evidence for the grouping, not a stat about the player. */
.dc-starts {
  margin-left: auto; flex: none;
  font-family: var(--font-num); font-size: var(--fs-micro); color: var(--dim);
}
.dc-badge + .dc-starts, .dc-starts + .dc-badge { margin-left: var(--s2); }

/* ---- Filters on a phone, aligned ----------------------------------------
 *
 * Four rows that each began at a different x: PROP put its label to the left
 * and wrapped its menu onto a second line below the chips, TEAM was a lone
 * menu beside a label, LINES was chips, PLAYER was a wide input. Nothing
 * shared a left edge, and the labels ate seventy pixels of a three-hundred-
 * and-ninety pixel screen.
 *
 * The label becomes a caption ABOVE its control. Every control then starts at
 * the same edge and runs the full width, the rows read as a stack rather than
 * a ragged table, and the width the labels were taking goes back to the
 * controls that need it.
 */
@media (max-width: 860px) {
  /* TWO groups per row, not one.
   *
   * Measured on the props board in a true 390x844 viewport: one group per
   * row made the open block 384px tall -- 45% of the screen -- and pushed
   * the first prop row to y=887. Opening the filters meant you could not
   * see a single row of what you were filtering, which is the same failure
   * the collapse button was added to fix, just one tap further in.
   *
   * The cost per group is a 21px caption plus its control, and six captions
   * is 126px of nothing but labels. Halving the ROW COUNT is what pays for
   * raising the controls to a 44px target: a taller control on half as many
   * rows is still shorter than what it replaces. Grid rather than a wrapping
   * flex row because auto-placement gives every page the same two columns
   * whatever its groups are, and a full-width group only has to say so. */
  .filterbar {
    display: grid; grid-template-columns: 1fr 1fr;
    align-items: start; gap: var(--s2);
    /* NOT sticky here.
     *
     * Sticky is right for a one-line bar on a desktop. Open on a phone it
     * pins 300px over the board, so the rows you just filtered to can never
     * be scrolled up to -- the filters follow them down the screen. Static
     * lets the block scroll away once you are done with it. The FILTERS
     * button scrolls away with it, which it already did when the bar was
     * sticky, so nothing is lost: the way back is where it always was. */
    position: static; box-shadow: none;
  }
  .filter-group, .filter-group.grow {
    width: auto; flex-direction: column; align-items: stretch;
    gap: var(--s1); min-width: 0;
  }
  /* The ones that cannot share a row: a checkbox has its whole sentence of
     label to put somewhere, and the applied-filters line is a sentence too.
     The player search is NOT one of them -- made full width it stranded
     Team alone on the row above it, which cost a whole row to gain width on
     an input whose placeholder is two words. */
  .filterbar > label,
  .filterbar > .f-games,
  .filterbar > .applied { grid-column: 1 / -1; }
  .filter-group > .lab {
    font-family: var(--font-display); text-transform: uppercase;
    letter-spacing: var(--track-lab); font-size: var(--fs-lab);
    color: var(--dim);
  }
  /* Chips and menu on ONE line, and that line SCROLLS.
   *
   * Wrapping was right at full width; in half a column it is what would
   * give the saved rows straight back -- Prop alone has seven chips. The
   * row keeps its height and overflows sideways instead. */
  .seg {
    display: flex; flex-wrap: nowrap; gap: var(--s1); align-items: stretch;
    overflow-x: auto; scrollbar-width: none;
  }
  .seg::-webkit-scrollbar { display: none; }
  .seg .segb { flex: 0 0 auto; }
  .seg .seg-select { flex: 1 0 96px; min-width: 96px; }
  /* 44px, the thumb target. Everything here was 32 or 34. */
  .seg .segb, .seg .seg-select,
  .filterbar .f-input, .filterbar .filter-input { min-height: 44px; }
  .filters-toggle { min-height: 44px; }
}

/* A lone menu should not stretch the full width of the screen -- that reads
   as an input rather than a choice. */
.seg[data-chips="none"] .seg-select { flex: 0 1 220px; min-width: 160px; }

/* The 404's way back. Buttons rather than a list of links: on a phone a link
   you have mistyped your way to should be a thumb target, not four words. */
.nf-links { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s4); }
.nf-links .btn { flex: 0 1 auto; min-height: 42px; padding-inline: var(--s4); }

/* A promotional line, marked wherever it appears.
 *
 * These are real PrizePicks picks and they stay on the board -- but the line
 * is set far below the player's real number, so the hit rate reads 100% and
 * means nothing about the player. The badge is the difference between a
 * member reading that as a lock we found and reading it as a promotion.
 * Amber, not green: it is neither a good nor a bad side, it is a different
 * KIND of thing. */
.promo-tag {
  font-size: var(--fs-micro); font-weight: 700;
  text-transform: uppercase; letter-spacing: var(--track-lab);
  padding: 2px 6px; border-radius: var(--r-chip); cursor: help;
  color: var(--neutral);
  background: color-mix(in srgb, var(--neutral) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--neutral) 45%, transparent);
}

/* A book that is shown but was not counted toward the consensus: an exchange
 * resting order, or a line with no usable price. Dimmed rather than removed,
 * because seeing the wild number is useful and only believing it was wrong. */
.bk-out { opacity: 0.45; cursor: help; }
.bk-out .bk-line { text-decoration: line-through; }

/* ---- The sim board on a phone -------------------------------------------
 *
 * The shared mobile card puts every numeric cell on ONE row, which is right
 * for the props board: it has five, and five 1fr columns hold them. The sim
 * board has six -- Model, Proj, Band, Min, L10 and History -- so the sixth
 * landed in an implicit seventh column and hung 41px past the right edge of
 * a 390px screen, on every row. That is the clipping: not a card too wide,
 * a card with one cell more than the grid was built for.
 *
 * Three columns and let them flow onto two rows. Six across would fit in
 * 344px by arithmetic and be unreadable in practice -- "100.0%" and "18-34"
 * do not belong in a 54px box.
 */
@media (max-width: 860px) {
  .board.discover.sim tbody tr { grid-template-columns: repeat(3, 1fr); }
  /* auto, not 2. The shared rule pins every number to row two; here they are
     meant to wrap, and the browser places them. */
  .board.discover.sim td.num:not(.c-rank):not(.c-line):not(.c-strip) {
    grid-row: auto;
  }
  /* No special case for the verdict. Six cells in three columns is exactly
     two full rows -- Model, Proj, Band above Min, L10, History -- and giving
     History the full width left a hole in the row above it. */
}

/* ---- The players board on a phone ---------------------------------------
 *
 * Same family of bug as the sim board, different cause. The shared card grid
 * reserves column 1 for the leading score and spans the player across 2/-1.
 * This board has no score column -- it starts with the player -- so column 1
 * sat empty on row one while seven numeric cells flowed from column 1 of row
 * two into implicit columns 6 and 7, and the row ran 41px off the right edge
 * of a 390px screen.
 */
@media (max-width: 860px) {
  /* The whole width, because there is no rank cell beside it. */
  .board.discover.players td.c-prop:not(.c-line):not(.c-strip) {
    grid-column: 1 / -1;
  }
  /* Let them wrap onto as many rows as they need instead of being pinned to
     row two and overflowing to the right. */
  .board.discover.players td.num:not(.c-rank):not(.c-line):not(.c-strip) {
    grid-row: auto;
  }
}

/* The orange count on a player row is the number of props on the board for
 * him. It carries a tooltip, so say so with the cursor and let a keyboard
 * reach it -- a hover-only explanation is no explanation on a phone, and the
 * aria-label is what a screen reader reads in place of a bare digit. */
.prop-count { cursor: help; }
.prop-count:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

/* A player who has been ruled out. Red, because unlike PROMO this is not a
 * different kind of pick -- it is a line that is not going to settle, and the
 * hit rates beside it were earned by a man who is not playing tonight. The
 * row is dimmed as well as badged: roughly one man in twelve cannot separate
 * the colours, so the message is carried twice, the same way the depth chart
 * marks an absence. */
.mod-dot.mod-out {
  color: var(--under);
  border-color: color-mix(in srgb, var(--under) 50%, transparent);
  background: color-mix(in srgb, var(--under) 16%, transparent);
}
.board.discover tbody tr:has(.mod-out) .c-prop,
.board.discover tbody tr:has(.mod-out) td.num { opacity: 0.55; }

/* The filter block, collapsed behind a button on a phone. See
 * wireCollapsibleFilters in shell.js for why: 73% of a 390px screen was gone
 * before the first row, and the block is sticky so scrolling never gave it
 * back. The button is not rendered at all on a wide screen. */
.filters-toggle {
  display: none;
  align-items: center; gap: var(--s2);
  margin: var(--s2) 0 0;
  padding: 8px 14px; min-height: 40px;
  font-family: var(--font-display); text-transform: uppercase;
  letter-spacing: var(--track-lab); font-size: var(--fs-lab);
  color: var(--ink-2); background: var(--panel-2);
  border: 1px solid var(--line); border-radius: var(--r-chip);
  cursor: pointer;
}
.filters-toggle[aria-expanded="true"] { color: var(--ink); border-color: var(--line-2); }
.filters-toggle .f-count {
  display: inline-grid; place-items: center;
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: var(--r-pill);
  background: var(--accent); color: var(--on-accent, #16120c);
  font-size: 10px; font-weight: 700;
}
/* 1479px, matching FILTER_BREAKPOINT in shell.js, and the same number as
 * the rail breakpoint below: the bar is inline exactly while the board and
 * the slate are side by side.
 *
 * These two numbers have to be the same number. They were 860 here and 1024
 * there, so between 861px and 1023px the JS marked the bar collapsed and the
 * CSS carried on displaying it -- a toggle that set an attribute nothing
 * read. Measured at 880px: data-collapsed present, 266px of filters still on
 * screen. */
@media (max-width: 1479px) {
  .filters-toggle { display: inline-flex; }
  .filterbar[data-collapsed] { display: none; }
}
/* [hidden] has to BEAT the rule above, and a bare `display` beats [hidden].
 *
 * This is the second time in one stylesheet. `.filterbar { display: flex }`
 * did it first and the de-vig view shipped with two buttons labelled
 * "Filters"; the fix set `btn.hidden` in JS, which then lost to
 * `.filters-toggle { display: inline-flex }` the moment a third filter bar
 * was added, and the same two buttons came back.
 *
 * The attribute selector carries more specificity than the class alone, so
 * this wins wherever it sits in the file, which a plain `display: none`
 * would not. Any new `display` on .filterbar or .filters-toggle needs the
 * matching [hidden] rule beside it. */
.filters-toggle[hidden] { display: none; }

/* ---- Boards on a narrow desktop (861px to 1023px) -----------------------
 *
 * The band where the table layout applies but there is not room for it. The
 * widest board declares 988px of columns and the sidebar takes 232px, so at
 * 880px the table has 648px to live in and scrolls either way.
 *
 * The player column is what to give back: at 400px it was most of the visible
 * width, so the name was enormous and every number was off-screen. Narrower
 * here means more of the row is reachable without scrolling, and the scroll
 * fade (app.css) now says the rest is there.
 */
@media (min-width: 861px) and (max-width: 1023px) {
  .board.discover thead th.c-prop,
  .board.discover td.c-prop {
    width: 240px; min-width: 240px; max-width: 240px;
  }
  .board.discover { min-width: 828px; }        /* 240 + 7 x 84 */
  .board.discover.sim { min-width: 814px; }    /* 70 + 240 + 6 x 84 */
  .board.discover.players { min-width: 828px; }
  .board.discover.disc { min-width: 730px; }   /* 70 + 240 + 5 x 84 */
}

/* ---- The board and the slate, side by side ------------------------------
 *
 * The board used to stretch to fill the card, and because the player cell is
 * a flex box rather than a table cell the browser handed the slack to a
 * column that could not use it -- first as a hole in the middle, then as a
 * dead strip down the right. Neither was a layout; both were a table being
 * asked to be wider than it wanted to be.
 *
 * So the board goes back to its natural width and the space beside it earns
 * its keep. Only the board scrolls: the games stay put while you read down.
 */
.board-and-rail {
  display: grid;
  /* The BOARD gets exactly what it needs and the rail takes what is left,
     within bounds. A fixed rail width was taking its 320px first and leaving
     the board 24px short, which clipped the Sim column -- the one thing on
     the row the page is sorted by. The board's own min-width still forces a
     sideways scroll if even max-content cannot fit, so nothing is ever
     silently cut. */
  /* The board's floor is its own declared width -- 70 rank + 400 player +
     5 x 84 hit-rate columns. `max-content` alone let it collapse, because
     .board-wrap scrolls and a scrolling box has a min-content width of zero,
     so the rail took its 340px first and clipped the Sim column: the one
     thing on the row the board is sorted by. */
  /* The rail takes whatever the board does not.
   *
   * It was capped at 340, so on a wide window the two tracks stopped short of
   * the column they sit in and the surplus became a hole on the right. The
   * board cannot absorb it -- its player cell is a flex box with a hard 400px
   * and the header matches it, so stretching the table puts the two back on
   * different layout algorithms (see the c-prop note above). The rail has no
   * such constraint: it is cards in a grid and it simply gets roomier.
   *
   * `1fr` rather than a bigger fixed cap, so this needs no number to be kept
   * in step with the board's own width. The page column's max-width is what
   * stops the rail growing without end. */
  grid-template-columns: minmax(890px, max-content) minmax(260px, 1fr);
  gap: var(--s4);
  /* `start` so the RAIL keeps its own height and its sticky behaviour; the
     board opts back into stretching below, because a short board beside a
     tall slate left the same hole from the other direction. */
  align-items: start;
  /* CENTRED, so surplus width is a margin and not a hole.
   *
   * Both tracks are content-sized, so the grid is only as wide as it needs to
   * be and every spare pixel used to pile up on the right. At a normal window
   * there are almost no spare pixels and it looked perfect; maximised there
   * were hundreds and it looked like the rail had come unstuck.
   *
   * This also makes the layout independent of how a browser sizes
   * `max-content` for a track holding a scroll container, which Firefox and
   * Chrome do not agree on -- the reason the same window looked right in one
   * and gapped in the other. Whatever width the board resolves to, the
   * leftover is now split evenly instead of landing in one place.
   *
   * Not centred: the page column above already shares its two edges with the
   * header and the filter bar, and centring this grid inside it would inset
   * the board from both of them -- trading one misalignment for another. */
}
/* The board fills the row rather than stopping at its last row.
 *
 * With a filter on, the board can be three rows beside a slate of eleven
 * games, and `align-items: start` left it floating at the top of a column
 * that carried on without it. Stretching makes the two panels end level
 * whichever of them is shorter; the shared max-height above keeps them level
 * when it is the board that is longer. */
.board-and-rail > .board-wrap { align-self: stretch; }

/* Between the strip breakpoint and the card one, the board is the only thing
 * in its column and the column is the full page width -- but the discover
 * table is content-sized (`width: auto`, which it must be: stretching it puts
 * the flex player cell and its header on different layout algorithms). So the
 * card stretched and the table did not, and the difference showed as empty
 * panel inside the card's own right edge.
 *
 * fit-content, so the card is exactly as wide as the board it holds. The
 * space beside it is then page background, which is what it is -- the same
 * as the note below the board, which has never stretched either. */
@media (min-width: 861px) and (max-width: 1479px) {
  .board-and-rail > .board-wrap { width: fit-content; max-width: 100%; }
}

/* The value board carries eleven columns and is the widest table here, so it
 * gets room rather than the board's 84px default.
 *
 * Why fixed widths and not `width: 100%`: the Leg cell is a flex box pinned
 * at its declared width, header and body both. Stretching the TABLE grows the
 * column without growing the cell, which puts a hole inside the Leg column --
 * the bug the c-prop note above describes. Widening the declared columns
 * moves the whole table instead, so nothing can come apart.
 *
 * The page column is capped at 1600, so above that the space available is
 * constant and these add up to fill it. Below it the wrap scrolls, which it
 * already did. */
@media (min-width: 1240px) {
  .board.value th:not(.c-rank):not(.c-prop),
  .board.value td:not(.c-rank):not(.c-prop) { width: 108px; }
  .board.value thead th.c-prop, .board.value td.c-prop {
    width: 430px; min-width: 430px; max-width: 430px;
  }
  /* The de-vig view trades name width for book columns: six of them, and the
     prop is a name and a market rather than a full leg description. */
  #view-devig .board.value thead th.c-prop,
  #view-devig .board.value td.c-prop {
    width: 290px; min-width: 290px; max-width: 290px;
    /* flex-start, not space-between. The leg cell spreads its contents to
       fill the column, which is right when it carries a side, a line, a
       stat and a price -- and here it carries a name and a market, so the
       spreading became a hole in front of the next column. */
    justify-content: flex-start;
  }
  /* The number everything is sorted by should read as the headline it is. */
  #view-devig .board.value td.c-rank .heat {
    font-size: var(--fs-body); font-weight: 700; padding: 5px var(--s2);
  }
  #view-devig .board.value thead th.c-rank,
  #view-devig .board.value td.c-rank { width: 86px; }
}

/* The edge after the model's own measured bias on that market. Shown beside
 * the raw figure rather than instead of it: the samples behind some markets
 * are thin, and a reader deciding what to trust should see both. */
.edge-adj { display: block; color: var(--under); line-height: 1; }

/* Each sportsbook's line and price, beside the de-vigged number they produce.
 * A book at a DIFFERENT line is shown and greyed: it is useful to see, and it
 * did not count toward the probability. */
/* One column per book: line above, prices below, same place on every row so
 * the eye can run down a column instead of hunting through a bag of chips. */
.board.value th.c-book, .board.value td.c-book {
  width: 76px; padding: var(--s1) 4px; text-align: center;
}
/* The price, which is the figure being compared across the row. */
.c-book .bk-p { display: block; color: var(--ink); font-weight: 600; }
/* A book that disagrees about the LINE says so above its price. Only drawn
 * when it differs, because when it matches it is the line already in its own
 * column two cells to the left. */
.c-book .bk-l {
  display: block; color: var(--faint); font-size: var(--fs-micro);
  line-height: 1;
}
.c-book .bk-none { color: var(--line-2); }
/* Posted, but at a different number, so it did not count. Dimmed rather than
 * hidden: seeing that a book disagrees about the LINE is worth something. */
.c-book.book-off { cursor: help; }
/* A book at a DIFFERENT line is marked by showing that line, not by dimming
   its price into illegibility.
   The price was --faint, and on a phone those cells read as broken: Paul saw
   them as "+105 odds ... in grey" and asked for white to match the rest. The
   plus prices were not a separate case -- a book quoting a different line is
   usually quoting a longer one, so off-line and plus-priced mostly coincide.
   The LINE above it keeps the dim treatment, which is the part that says
   "this is not the number the row is about". */
.c-book.book-off .bk-l { color: var(--faint); font-weight: 400; }
.c-book.book-off .bk-p { color: var(--ink-2); }

/* The verdict, in words, above the tables that support it. Three dense
 * tables are a reference; this is the answer. */
.verdict { margin: 0; padding: 0 0 0 var(--s4); list-style: none; }
.verdict li {
  position: relative; padding: var(--s2) 0;
  color: var(--ink-2); font-size: var(--fs-sm); line-height: 1.55;
  max-width: 82ch;
}
.verdict li + li { border-top: 1px solid var(--line); }
.verdict li strong:first-child {
  display: block; color: var(--ink); font-family: var(--font-display);
  letter-spacing: var(--track-lab); margin-bottom: 2px;
}
.verdict li strong { color: var(--ink); }

/* Both signals agreeing. Rare, independent, and the reason the value and
 * discrepancies pages became one. */
.sig-both {
  display: inline-flex; align-items: center; cursor: help;
  padding: 1px 6px; border-radius: var(--r-pill);
  font-size: var(--fs-micro); font-weight: 700;
  letter-spacing: var(--track-lab);
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 50%, transparent);
}

/* The value board on a phone.
 *
 * It inherits the props board's card layout, which lays a row out as FIVE
 * equal columns -- tuned for the five cells that board has. This one has
 * eleven. The result was the worst kind of broken: nothing overflowed the
 * screen, so it looked deliberate, while the player's name was clipped to
 * "Cad...", the three leading figures ran together as "69 57 +1" under
 * crushed labels, and the market cell printed on top of L5.
 *
 * So it opts out and lays itself out: the EV and the leg across the top,
 * then the nine figures as labelled boxes four to a row. Same idea the
 * opportunity board uses, and for the same reason -- a column header that
 * has been display:none is a label the reader no longer has.
 */
@media (max-width: 860px) {
  .board.value tbody tr {
    display: grid;
    /* minmax(0, 1fr), never a bare 1fr: a bare one takes its content's width
       as a minimum, which is what pushed a cell off the row on the props
       board and on the opportunity board before it. */
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--s2) var(--s1);
    align-items: start;
    padding: var(--s2) 0;
  }
  /* The EV is the headline, so it leads, and the leg takes the rest. */
  .board.value td.c-rank:not(.c-line):not(.c-strip) {
    grid-column: 1; grid-row: 1;
    display: flex; align-items: center;
    position: static; width: auto; min-width: 0; padding: 0; margin: 0;
  }
  .board.value td.c-prop:not(.c-line):not(.c-strip) {
    grid-column: 2 / -1; grid-row: 1;
    position: static; width: auto; min-width: 0; max-width: none;
    height: auto; border-right: 0; padding: 0;
  }
  /* Everything else is a labelled box, flowing four to a row. The :not chain
     matches the one the props board uses, because specificity counts the
     classes inside :not() and a plainer selector loses to the rule that
     hides non-card cells. */
  .board.value td.num:not(.c-rank):not(.c-line):not(.c-strip) {
    /* grid-row: auto, undoing the props board's `grid-row: 2`.
       That rule pins every figure to the second row, which is right for its
       five and packed all NINE of these into one row of implicit columns --
       so the four-column template above was being ignored and the labels
       crushed to fit. Letting them flow is the whole fix. */
    grid-row: auto;
    display: inline-flex; flex-direction: column; align-items: center;
    gap: 1px; width: auto; min-width: 0; padding: var(--s1) 2px;
    border-radius: var(--r-chip); background: var(--panel-2);
    border-bottom: 0;
  }
  /* Four to a row groups them exactly right -- the price (model, bar, edge,
     market), then the history (L5 to H2H), then the matchup. The matchup is
     one cell on a row of four, so it takes the width instead of sitting as
     an orphan beside three gaps. */
  .board.value td.num[data-lab="Def"] {
    grid-column: 1 / -1; flex-direction: row; justify-content: center;
    gap: var(--s2);
  }
  .board.value td.num[data-lab]::before {
    content: attr(data-lab);
    font-family: var(--font-display); text-transform: uppercase;
    letter-spacing: var(--track-lab); font-size: 9px;
    color: var(--dim); line-height: 1;
  }
  /* The name has the whole width of the row to itself now, so it should use
     it rather than clipping to four characters. */
  .board.value .pname { min-width: 0; }
  .board.value .pcell { width: 100%; }

  /* The de-vig view carries six book columns, and on a phone each one
     becomes its own labelled box -- so a leg that two books price turned
     into five rows of cards, four of them a dot under a book's initials.
     A book with no price at this line says nothing; the BOOKS count
     already says how many there are. Drop the empties and the card is the
     two or three books that actually quoted it.

     :has() rather than a class because the markup already distinguishes
     them -- bk-none is what devigRow emits for a book that is not there. */
  #view-devig .board.value td.c-book:has(.bk-none) { display: none; }
  /* Off-line books keep their line, so they need both lines of text; the
     counted ones are a single price and read better slightly larger. */
  #view-devig .board.value td.c-book .bk-p { font-size: var(--fs-body); }
}

/* A board with no rail beside it, at any width above the card layout. Same
 * reasoning as the rule above: the table is content-sized on purpose, so the
 * card has to be too or the difference reads as a hole in the panel. */
@media (min-width: 861px) {
  .board-wrap-hug { width: fit-content; max-width: 100%; }
}

.rail {
  position: sticky;
  /* Clear of the header, which is sticky too. */
  top: calc(var(--topbar-h) + var(--s3));
  /* The SAME height the board gets, not its own arithmetic.
   *
   * The two boxes sit in one grid row and were measured differently: the
   * board from where it actually starts on the page (syncBoardHeight, which
   * knows about the heading and the filter rows above it), the rail from the
   * top of the viewport. The heading and filters are about 400px, so the
   * rail's box came out that much taller than the board's. The row took the
   * rail's height, `align-items: start` held the board at the top, and the
   * difference was a hole under the board -- which is what it looked like:
   * the board cutting off early with the slate carrying on beside it.
   *
   * --board-max-h is already measured and already re-measured on resize and
   * when the filters wrap, so sharing it keeps the two bottoms level without
   * a second measurement to keep in step. The calc is the same fallback the
   * board uses, for the first paint before the module runs. */
  max-height: var(--board-max-h,
    calc(100vh - var(--topbar-h) - var(--toolbar-h, 60px) - var(--s6)));
  overflow-y: auto;
  overscroll-behavior: contain;
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  background: var(--panel);
  padding: var(--s3);
}
.rail-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s2); margin-bottom: var(--s3);
}
.rail-head h2 { font-size: var(--fs-h3); margin: 0; }
.rail-list { display: grid; gap: var(--s2); }

.rail-game {
  display: grid; gap: 4px;
  width: 100%; text-align: left;
  padding: var(--s2) var(--s3);
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--r-chip);
  color: inherit; font: inherit; cursor: pointer;
}
.rail-game:hover { background: var(--panel-3); border-color: var(--line-2); }
/* A placeholder is not a button: no hover, no pointer, and room for the two
   bars that stand in for the teams and the line. */
.rail-game.rail-skel {
  display: grid; gap: var(--s2); cursor: default; pointer-events: none;
}
.rail-game.rail-skel:hover { background: var(--panel-2); border-color: var(--line); }
.rail-game.is-on {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, var(--panel-2));
}
.rg-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s2); }
.rg-teams { font-weight: 600; font-size: var(--fs-sm); color: var(--ink); }
.rg-at { color: var(--dim); font-weight: 400; }
.rg-tip { font-size: var(--fs-micro); color: var(--dim); }
.rg-lines { display: flex; align-items: baseline; gap: var(--s3); }
.rg-spread { font-size: var(--fs-sm); color: var(--ink-2); }
.rg-total { font-size: var(--fs-micro); color: var(--dim); }
.rg-none { font-size: var(--fs-micro); color: var(--faint); }
/* Amber, not red: a lopsided game is a reason to read the minutes column, not
   a warning that something is broken. */
.rg-flag {
  justify-self: start;
  font-size: 9px; font-weight: 700; text-transform: uppercase;
  letter-spacing: var(--track-lab);
  padding: 2px 6px; border-radius: var(--r-chip);
  color: var(--neutral);
  background: color-mix(in srgb, var(--neutral) 16%, transparent);
}
/* Both moneylines, under the spread and total. */
.rg-mls {
  display: flex; gap: var(--s3); align-items: baseline;
  margin-top: 2px; font-size: var(--fs-micro); color: var(--ink-3);
}
.rg-ml { display: inline-flex; gap: 5px; align-items: baseline; }
.rg-ml-team { letter-spacing: var(--track-lab); }
/* The favourite, marked once rather than left to be worked out from two
 * negative numbers. */
.rg-ml.is-fav { color: var(--ink); font-weight: 600; }

.rail-foot {
  margin: var(--s3) 0 0; font-size: var(--fs-micro); color: var(--faint);
}

/* Under 1280px there is not room for a column beside the board, so the slate
   becomes a strip above it that scrolls sideways. Same cards, same click. */
/* 1480, from arithmetic rather than taste: the board needs 890, the gap 16
   and the narrowest useful rail 260, which with the sidebar and the page
   padding comes to just under 1480. Below that there is no honest way to put
   them side by side, so the slate becomes a strip above the board. */
@media (max-width: 1479px) {
  .board-and-rail { grid-template-columns: minmax(0, 1fr); }
  .rail {
    position: static; max-height: none; overflow: visible;
    order: -1; margin-bottom: var(--s3);
  }
  .rail-list {
    grid-auto-flow: column; grid-auto-columns: minmax(190px, 1fr);
    overflow-x: auto; padding-bottom: var(--s2);
    scroll-snap-type: x proximity;
  }
  .rail-game { scroll-snap-align: start; }
  .rail-foot { display: none; }
}

/* Team crests and colours on the rail.
 *
 * Grey three-letter codes on a grey card gave a reader nothing to aim at --
 * on a slate of ten games you want to find your team without reading. The
 * stripe does that work; the crest confirms it; the letters are the actual
 * label and survive the CDN failing. */
.rg-team {
  display: inline-flex; align-items: center; gap: 5px;
  padding-left: 6px;
  border-left: 3px solid var(--tc, var(--line-2));
}
.rg-logo { object-fit: contain; flex: none; }
.rg-abbr { font-weight: 700; letter-spacing: 0.01em; }
.rg-teams { display: inline-flex; align-items: center; gap: var(--s2); }
.rg-flags { display: flex; flex-wrap: wrap; gap: 4px; }
/* Pace is a different kind of claim from blowout risk -- it is a prediction
   about the game rather than a warning about minutes -- so it reads in the
   product's information colour rather than its caution one. */
.rg-flag.rg-fast {
  color: var(--over);
  background: color-mix(in srgb, var(--over) 15%, transparent);
}
.rg-flag.rg-slow {
  color: var(--accent-2, var(--dim));
  background: color-mix(in srgb, var(--dim) 18%, transparent);
}
