/* ══ FANTASY — PHASE 1 (+ PHASE 2 additions below) ═══════════════════════════
   js/11e-fantasy.js (moved out of js/11b-verticals.js in PHASE 2, once the
   port outgrew that file's 800-line budget — see that file's own header for
   why) is the only builder of `.vhf-*`. Everything genuinely generic (a
   labelled band, an honest empty sentence, a KPI tile, the modal shell
   itself, a data table) reuses the existing `.vh-*`/`.vgm-*` systems
   (19-verticals.css / 22-modals.css) instead of a fantasy copy of each — this
   file exists only for the shapes those systems do not have: a matchup, a
   draft check-in band, a lineup slot, a roster row, a trade offer, a weekly
   points chart.

   Same scoping as 19-verticals.css: `#screen-vertical` carries `.bs`
   (index.html), and so does every openModal() panel (11d-modals.js's
   `el('div', 'vgm-panel bs')`) — one selector prefix covers the landing AND
   every modal built on top of it (My team / Players / Wire / League, all
   PHASE 2). */

.bs .vhf-btn-primary {
  font-family: var(--bs-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--bs-sheet);
  background: var(--bs-ink);
  border: 1px solid var(--bs-ink);
  border-radius: var(--radius);
  padding: 10px 18px;
  cursor: pointer;
  transition: background .12s ease;
}
.bs .vhf-btn-primary:hover { background: var(--bs-ink-2); }
.bs .vhf-btn-primary:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 2px; }
/* A disabled primary keeps its shape and loses its fill — fantasy.html's own
   rule, carried over verbatim: "a solid ink button at 40% opacity is still a
   big muted slab, which reads as a surface rather than as an unavailable
   control." */
.bs .vhf-btn-primary:disabled {
  cursor: not-allowed;
  background: transparent;
  color: var(--bs-ink-3);
  border-color: var(--bs-rule);
  border-style: dashed;
}

/* Row-level actions — Change/Fill/Drop/Bench. Solid ink stays reserved for
   the ONE primary action per screen (Save lineup, Claim my seat); a list of
   nine solid buttons down a lineup is a wall of colour that also misstates
   hierarchy, and Drop, being destructive, has no business wearing the
   primary style at all. Same reasoning fantasy.html's own `.btn.quiet`
   already carries. */
.bs .vhf-btn-sm {
  font-family: var(--bs-ui);
  font-size: 12px;
  font-weight: 600;
  color: var(--bs-blue);
  background: transparent;
  border: 1px solid var(--border-hi);
  border-radius: var(--radius);
  padding: 5px 11px;
  cursor: pointer;
  white-space: nowrap;
  transition: background .12s ease, border-color .12s ease;
}
.bs .vhf-btn-sm:hover { background: var(--gold-bg); border-color: var(--bs-camel-deep); }
.bs .vhf-btn-sm:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 2px; }
.bs .vhf-btn-sm:disabled { cursor: not-allowed; color: var(--bs-ink-3); border-style: dashed; background: none; }
.bs .vhf-btn-sm.is-danger { color: var(--bs-pen); }
.bs .vhf-btn-sm.is-danger:hover { background: rgba(194, 38, 31, .07); border-color: var(--bs-pen); }

/* ── the draft check-in band ── */
.bs .vhf-draftband {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-top: 20px;
  padding: 16px 20px;
  background: var(--bs-ink);
  border: 1px solid var(--bs-ink);
  border-radius: var(--radius);
}
.bs .vhf-draftband-text { display: flex; flex-direction: column; gap: 4px; }
.bs .vhf-draftband-kicker {
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--bs-camel-on-ink);
}
.bs .vhf-draftband-line { font-family: var(--bs-ui); font-size: 14px; color: var(--bs-sheet); }
.bs .vhf-draftband .vhf-btn-primary {
  background: var(--bs-camel-deep);
  border-color: var(--bs-camel-deep);
  color: var(--bs-ink);
  flex-shrink: 0;
}
.bs .vhf-draftband .vhf-btn-primary:hover { background: var(--bs-camel); }

/* ── the anchor: this week's matchup ── */
.bs .vhf-anchor {
  padding: 18px 20px;
  border: 1px solid var(--bs-ink);
  border-radius: var(--radius);
  background: var(--bs-sheet);
}
.bs .vhf-matchup { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 12px; }
.bs .vhf-matchup-side { display: flex; flex-direction: column; gap: 4px; }
.bs .vhf-matchup-side.is-mine { border-left: 2px solid var(--bs-blue); padding-left: 12px; }
.bs .vhf-matchup-side:not(.is-mine) { border-left: 2px solid transparent; padding-left: 12px; }
.bs .vhf-matchup-name {
  font-family: var(--bs-ui);
  font-size: 13.5px;
  font-weight: 600;
  color: var(--bs-ink-2);
}
.bs .vhf-matchup-side.is-mine .vhf-matchup-name { color: var(--bs-ink); }
.bs .vhf-matchup-pts {
  font-family: var(--bs-ed);
  font-variation-settings: 'WONK' 1;
  font-weight: 900;
  font-size: 30px;
  line-height: 1;
  letter-spacing: -.02em;
  color: var(--bs-ink-3);
  font-variant-numeric: tabular-nums;
}
.bs .vhf-matchup-side.is-mine .vhf-matchup-pts { color: var(--bs-blue); }

/* The projected final under the banked figure on a scoreboard row. Same
   relationship the box-score header already draws with `.vhf-box-proj`: the
   big number is what is ON THE BOARD, this is the forecast, and it is quieter
   so the two are not read as equals. Declared BELOW `.vhf-matchup-pts` because
   equal specificity is settled by source order. */
.bs .vhf-matchup-proj {
  display: block;
  font-size: 0.78rem;
  letter-spacing: 0.02em;
  color: var(--bs-ink-3);
}
.bs .vhf-matchup-proj .is-proj { font-variant-numeric: tabular-nums; }

/* The per-fixture drill-in, sitting in the same tag row as FINAL/LIVE so it
   reads as part of the row's status furniture rather than a floating control. */
.bs .vhf-box-open { margin-left: auto; }
.bs .vhf-anchor .vh-empty { margin-top: 12px; }
@media (max-width: 480px) {
  .bs .vhf-matchup { grid-template-columns: 1fr; gap: 10px; }
}

/* ── join a league ── */
/* Capped to the same column as .vhf-rules above it. Measured 2026-09-06: the
   rules list ends at 862 and this form was running to 996, so the pre-league
   screen had two different right edges stacked on each other. The kickers still
   span full width — a heading rule spanning its section is the existing
   `.vh-kicker` behaviour across every vertical, and it reads as a header rather
   than as a fourth ragged edge. */
.bs .vhf-join {
  display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 12px;
  max-width: calc(9rem + 18px + var(--read-measure));
}
.bs .vhf-join-select,
.bs .vhf-join-input,
.bs .vhf-search-input {
  font-family: var(--bs-ui);
  font-size: 13.5px;
  padding: 9px 11px;
  border: 1px solid var(--bs-rule);
  border-radius: var(--radius);
  background: #fff;
  color: var(--bs-ink);
}
.bs .vhf-join-input { min-width: 220px; flex: 1; }
/* PHASE 2 reuses this exact input face for the Players search box and the
   waiver bid amount — a text field is a text field regardless of which form
   it sits in, and a second declaration block for the same three properties
   would be the "one system, drifting" defect this namespace exists to avoid. */
.bs .vhf-search-input { width: 100%; margin-bottom: 10px; }

/* ── commissioner desk ──
   Controls use their own compact input classes. Reusing .vhf-join-input here
   would import its 220px minimum into a week field and make a 375px viewport
   wider than the page. Every grid/flex child is therefore explicitly allowed
   to shrink. */
.bs .vhf-commish-intro { max-width: 68ch; }
.bs .vhf-commish-rows {
  border-top: 1px solid var(--bs-ink);
}
.bs .vhf-commish-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 16px;
  align-items: center;
  padding: 14px 0;
  border-bottom: 1px solid var(--bs-rule);
  min-width: 0;
}
.bs .vhf-commish-copy { min-width: 0; }
.bs .vhf-commish-title {
  display: block;
  font-family: var(--bs-ed);
  font-size: 16px;
  color: var(--bs-ink);
}
.bs .vhf-commish-description,
.bs .vhf-commish-modal-lede {
  margin: 3px 0 0;
  font-family: var(--bs-ui);
  font-size: 13px;
  line-height: 1.5;
  color: var(--bs-ink-2);
}
.bs .vhf-commish-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  min-width: 0;
  max-width: 100%;
}
.bs .vhf-commish-week-label,
.bs .vhf-commish-field-label,
.bs .vhf-commish-target-label {
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
}
.bs .vhf-commish-week,
.bs .vhf-commish-input,
.bs .vhf-commish-league-select {
  box-sizing: border-box;
  min-width: 0;
  max-width: 100%;
  padding: 9px 10px;
  border: 1px solid var(--bs-rule);
  border-radius: var(--radius);
  background: var(--bs-sheet);
  color: var(--bs-ink);
  font-family: var(--bs-ui);
  font-size: 13px;
}
.bs .vhf-commish-week { width: 4.5rem; }
.bs .vhf-commish-target {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  justify-content: space-between;
  gap: 8px 16px;
  padding: 14px 0;
  border-bottom: 1px solid var(--bs-rule);
  min-width: 0;
}
.bs .vhf-commish-target-label,
.bs .vhf-commish-field {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}
.bs .vhf-commish-league-select { width: min(22rem, 100%); }
.bs .vhf-commish-target-note {
  font-family: var(--bs-ui);
  font-size: 12px;
  color: var(--bs-ink-3);
}
.bs .vhf-commish-fields {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
  margin-top: 16px;
}
.bs .vhf-commish-field.is-wide { grid-column: 1 / -1; }
.bs textarea.vhf-commish-input {
  width: 100%;
  min-height: 5.5rem;
  resize: vertical;
}
.bs .vhf-commish-warning {
  margin: 16px 0 0;
  padding: 10px 12px;
  border-left: 3px solid var(--bs-pen);
  background: var(--red-bg);
  font-family: var(--bs-ui);
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--bs-ink-2);
}
.bs .vhf-commish-status {
  min-height: 1.5em;
  margin: 12px 0 0;
  font-family: var(--bs-ui);
  font-size: 12.5px;
  color: var(--bs-ink-3);
}
.bs .vhf-commish-status.is-error { color: var(--bs-pen); }

@media (max-width: 560px) {
  .bs .vhf-commish-row { grid-template-columns: minmax(0, 1fr); gap: 10px; }
  .bs .vhf-commish-controls { justify-content: flex-start; width: 100%; }
  .bs .vhf-commish-row > .vhf-btn-primary { width: 100%; }
  .bs .vhf-commish-controls .vhf-btn-primary { flex: 1 1 9rem; }
  .bs .vhf-commish-fields { grid-template-columns: minmax(0, 1fr); }
  .bs .vhf-commish-target-label { width: 100%; }
  .bs .vhf-commish-league-select { width: 100%; }
}

/* ── the "My team" modal body ── */
.bs .vhf-team-head { padding-bottom: 14px; border-bottom: 1px solid var(--bs-rule); }
.bs .vhf-team-name {
  font-family: var(--bs-ed);
  font-variation-settings: 'WONK' 1;
  font-weight: 900;
  font-size: 24px;
  letter-spacing: -.01em;
  color: var(--bs-ink);
}
.bs .vhf-team-sub { font-family: var(--bs-mono); font-size: 11px; color: var(--bs-ink-3); margin-top: 3px; }

.bs .vhf-banner {
  margin-top: 12px;
  padding: 10px 12px;
  border-radius: var(--radius);
  border: 1px solid var(--bs-ok);
  background: var(--green-bg);
}
.bs .vhf-banner b { font-family: var(--bs-ui); font-size: 13.5px; color: var(--bs-ink); }
.bs .vhf-banner.is-warn { border-color: var(--bs-pen); background: var(--red-bg); }
/* A THIRD STATE, because two were not enough. The base rule above IS the OK
   state — green border, green wash — and `.is-warn` is the alarm, so a banner
   with no modifier reads as SUCCESS. Before the auction settles a roster is
   neither: nothing is wrong and nothing has gone right yet. Painting "No roster
   yet" in the success green claims a result that does not exist, which is the
   same false-positive the red banner was making from the other direction.
   Found by reading the computed border off the painted box — the class list
   alone said "no modifier", which looked neutral and was not. */
.bs .vhf-banner--pending { border-color: var(--bs-rule); background: var(--bs-stock-2); }
.bs .vhf-banner .vh-empty { margin-top: 4px; border-left: none; padding-left: 0; }

.bs .vhf-counts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }

.bs .vhf-tag {
  display: inline-block;
  margin-left: 5px;
  font-family: var(--bs-mono);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: .06em;
  padding: 1px 5px;
  border-radius: var(--radius);
  border: 1px solid var(--bs-camel-deep);
  color: var(--bs-blue);
  background: rgba(193, 154, 91, .2);
  white-space: nowrap;
}
/* NOT `:first-child`, and this was a real defect for as long as it shipped.
   `:first-child` matches on ELEMENT children only, and every chip in this arm
   is appended to a span whose player name is a bare TEXT node — so the first
   chip on a line was always the first element child, always matched, and
   always lost its 5px. Measured in a browser 2026-09-09 on the auction board:
   `marginLeft: "0px"` and a gap of exactly 0 between "Ja'Marr Chase" and its
   QUES tag, on every row with a tag. The same rule was doing the same thing to
   the DFS slot rows, the monitor's UNCLAIMED, the box score's YOU and every
   R/TE/LOCKED badge in a lineup.

   What it was FOR is `.vhf-counts`, the player-detail badge row — the one
   container where a chip really is first with no text before it. That row is a
   flex with `gap: 6px`, so it never wanted the margin at all: the margin was
   adding to the gap on every badge but the first, and `:first-child` was
   levelling them back up. Zeroing it there lets the gap do the whole job. */
.bs .vhf-counts .vhf-tag { margin-left: 0; }
.bs .vhf-tag.is-te { border-color: var(--bs-ink-3); color: var(--bs-ink-2); background: var(--bs-stock-2); }
.bs .vhf-tag.is-inj { border-color: var(--bs-pen); color: var(--bs-pen); background: rgba(194, 38, 31, .07); }
.bs .vhf-tag.is-ok { border-color: var(--bs-ok); color: var(--bs-ok); background: var(--green-bg); }

.bs .vhf-weeknav { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.bs .vhf-weeknav-lab {
  font-family: var(--bs-ed);
  font-weight: 700;
  font-size: 15px;
  min-width: 60px;
  text-align: center;
  color: var(--bs-ink);
}

.bs .vhf-lineup-list { border-radius: var(--radius); overflow: hidden; margin-top: 10px; }
.bs .vhf-lineup-row {
  display: grid;
  grid-template-columns: 62px minmax(0, 1fr) auto;
  gap: 10px;
  align-items: center;
  padding: 8px 0;
  border-bottom: 1px solid var(--bs-rule);
}
.bs .vhf-lineup-row:last-child { border-bottom: none; }
.bs .vhf-slot-badge {
  font-family: var(--bs-mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .06em;
  padding: 3px 5px;
  border-radius: var(--radius);
  text-align: center;
  border: 1px solid var(--border-hi);
  color: var(--bs-ink-2);
  background: var(--bs-stock-2);
}
.bs .vhf-slot-badge.is-flex { border-color: var(--bs-camel-deep); color: var(--bs-blue); background: rgba(193, 154, 91, .14); }
.bs .vhf-slot-mid { min-width: 0; }
.bs .vhf-slot-name {
  font-family: var(--bs-ed);
  font-weight: 600;
  font-size: 14.5px;
  display: block;
  line-height: 1.25;
  color: var(--bs-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bs .vhf-slot-mid .vh-empty { margin-top: 2px; border-left: none; padding-left: 0; font-size: 12px; }

/* ── player rows: roster list + slot picker ── */
.bs .vhf-prow-list { border-radius: var(--radius); overflow: hidden; margin-top: 10px; }
.bs .vhf-prow {
  display: grid;
  grid-template-columns: 32px minmax(0, 1fr) auto;
  gap: 9px;
  align-items: center;
  padding: 7px 9px;
  border-bottom: 1px solid var(--bs-rule);
  background: var(--bs-sheet);
}
.bs .vhf-prow:last-child { border-bottom: none; }
/* A row with no headshot. Two children, so the 32px media column must go —
   otherwise the NAME lands in it and is clipped to 32px, which is what "Your
   bids" was doing: fourteen rows at 32px against a natural 79-160px.

   BELOW `.bs .vhf-prow`, AND THAT IS THE WHOLE POINT. Both selectors are
   (0,2,0), so the cascade decides on source order and the later one wins.
   Declared above the base rule this override was INERT — it shipped, the
   commit message said the clipping was fixed, and a browser found every row
   still 32px wide. Equal specificity is not an override; position is. */
.bs .vhf-prow--nomedia { grid-template-columns: minmax(0, 1fr) auto; }
.bs .vhf-prow-img {
  width: 32px; height: 32px;
  object-fit: cover;
  object-position: top center;
  border: 1px solid var(--bs-rule);
  border-radius: var(--radius);
  background: var(--bs-stock-2);
}
.bs .vhf-prow-name { min-width: 0; }
.bs .vhf-prow-nm {
  font-family: var(--bs-ed);
  font-weight: 600;
  font-size: 14.5px;
  display: block;
  line-height: 1.25;
  color: var(--bs-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bs .vhf-prow-sub {
  font-family: var(--bs-mono);
  font-size: 9.5px;
  color: var(--bs-ink-3);
  letter-spacing: .04em;
  text-transform: uppercase;
}
.bs .vhf-prow-act { display: flex; align-items: center; gap: 8px; }
.bs .vhf-prow-num {
  font-family: var(--bs-mono);
  font-size: 12px;
  color: var(--bs-ink-3);
  white-space: nowrap;
  /* MIGRATED IN WAVE 2. This was the one number rule in the file with no
     tabular figures, which is why a column of "13.9 proj / 9.4 proj / 12.8
     proj" down the auction pool did not line up on the decimal point. The
     primitives header says the older rules move onto .vhf-num surface by
     surface as each wave touches one; the auction pool is where this column is
     read most, so it moves now. Kept as its own selector rather than swapped
     for .vhf-num--agate because the ' proj' / ' pts' suffix rides inside the
     same span, and the primitive is for bare figures. */
  font-variant-numeric: tabular-nums;
}
/* Every row drills into the player detail nested step now (js/11e-fantasy.js's
   _vhFantasyPlayerRow, `opts.onOpen`) — fantasy.html wired the same universal
   row click; phase 1 deferred it because there was nothing to open yet. */
.bs .vhf-prow.is-clickable { cursor: pointer; transition: background .12s ease; }
.bs .vhf-prow.is-clickable:hover { background: var(--gold-bg); }
.bs .vhf-prow.is-clickable:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: -2px; }

/* ══ PHASE 2 additions ═══════════════════════════════════════════════════════
   Waivers, trades, players/free-agents, the wire, the league table, and the
   player detail drill-through. Same reconciliation phase 1 committed every
   later phase to — nothing here invents a new modal, toast, or fetch
   wrapper; it is all api()/showError()/openModal()/.vh-*, plus the fantasy-
   shaped remainder below. */

/* ── a generic row: name (+ meta line) · a value · an action ──
   Waiver claims, recent waiver runs. Not a fixed-column grid like the
   lineup/prow rows above — those have a badge or headshot at a known width;
   this one doesn't, so flex lets the value/action column size to content. */
.bs .vhf-list-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--bs-rule); }
.bs .vhf-list-row:last-child { border-bottom: none; }
.bs .vhf-list-main { flex: 1; min-width: 0; }
.bs .vhf-list-name {
  font-family: var(--bs-ed);
  font-weight: 600;
  font-size: 14.5px;
  color: var(--bs-ink);
  display: block;
  line-height: 1.25;
}
.bs .vhf-list-val {
  font-family: var(--bs-mono);
  font-size: 15px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--bs-ink);
  white-space: nowrap;
}
/* A small mono caption — bio vitals, a claim's processing note, a news
   item's byline. One generic reader rather than a fresh class per surface
   that happens to also want a quiet grey mono line under something. */
.bs .vhf-meta { font-family: var(--bs-mono); font-size: 10.5px; color: var(--bs-ink-3); line-height: 1.6; margin-top: 3px; }

/* ── trade offers (view) ── */
.bs .vhf-trade-card {
  border: 1px solid var(--bs-rule);
  border-radius: var(--radius);
  padding: 10px;
  margin-top: 10px;
  background: var(--bs-sheet);
}
.bs .vhf-trade-card.is-incoming { border-color: var(--bs-camel-deep); background: var(--gold-bg); }
.bs .vhf-trade-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin-top: 8px; }
.bs .vhf-trade-col { border: 1px solid var(--bs-rule); border-radius: var(--radius); padding: 7px; background: var(--bs-sheet); }
.bs .vhf-trade-col-names {
  font-family: var(--bs-ed);
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.35;
  margin-top: 2px;
  color: var(--bs-ink);
}
.bs .vhf-trade-actions { display: flex; gap: 8px; margin-top: 9px; }

/* ── trade builder (propose) — a nested step off a League roster card ── */
.bs .vhf-trade-builder { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 10px; }
.bs .vhf-trade-builder-col { min-width: 0; }
.bs .vhf-trade-check-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 0;
  border-bottom: 1px solid var(--bs-rule);
  font-family: var(--bs-ui);
  font-size: 13.5px;
  color: var(--bs-ink);
  cursor: pointer;
}
.bs .vhf-trade-check-row input { width: auto; }
@media (max-width: 480px) {
  .bs .vhf-trade-builder, .bs .vhf-trade-grid { grid-template-columns: 1fr; }
}

/* ── the waiver bid form ── */
.bs .vhf-bid-form { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; max-width: 260px; }
.bs .vhf-bid-form label {
  font-family: var(--bs-mono);
  font-size: 9.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  font-weight: 600;
}

/* ── position filter pills (Players) ── */
.bs .vhf-filter-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.bs .vhf-filter-btn {
  font-family: var(--bs-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--bs-ink-2);
  background: transparent;
  border: 1px solid var(--border-hi);
  border-radius: var(--radius);
  padding: 5px 10px;
  cursor: pointer;
}
.bs .vhf-filter-btn:hover { background: var(--bs-stock-2); }
.bs .vhf-filter-btn.is-active { background: var(--bs-blue); color: var(--bs-sheet); border-color: var(--bs-blue); }

/* The auction board's paging row. Centred under the list so "Load more" reads
   as the end of the board rather than as another row in it. */
.bs .vhf-more-row { display: flex; justify-content: center; padding: 10px 0 2px; }
.bs .vhf-more-row .vh-empty { margin: 0; font-size: 12px; }

/* Sort sits at the end of the filter row and pushes to the right edge, so the
   position buttons stay left-aligned as a group whatever the row's width. */
.bs .vhf-sort-select {
  margin-left: auto;
  font-family: var(--bs-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--bs-ink-2);
  background: transparent;
  border: 1px solid var(--border-hi);
  border-radius: var(--radius);
  padding: 5px 8px;
  cursor: pointer;
}
.bs .vhf-sort-select:hover { background: var(--bs-stock-2); }
.bs .vhf-sort-select:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 2px; }

/* ── the wire (news list) ── */
.bs .vhf-news-item {
  display: grid;
  grid-template-columns: 38px minmax(0, 1fr);
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--bs-rule);
}
.bs .vhf-news-item:last-child { border-bottom: none; }
.bs .vhf-news-img {
  width: 38px; height: 38px;
  object-fit: cover;
  object-position: top center;
  border: 1px solid var(--bs-rule);
  border-radius: var(--radius);
  background: var(--bs-stock-2);
}
.bs .vhf-news-body { min-width: 0; }
.bs .vhf-news-item.is-clickable .vhf-news-img,
.bs .vhf-news-item.is-clickable .vhf-news-title { cursor: pointer; }
/* The player-detail step's own "The wire" section has no `n.player` sub-object
   to show a headshot for (GET /players/{id} already knows whose page this
   is) — one column instead of the image-plus-text grid. */
.bs .vhf-news-item.no-img { grid-template-columns: 1fr; }
.bs .vhf-news-title { font-family: var(--bs-ed); font-size: 15px; font-weight: 700; line-height: 1.3; color: var(--bs-ink); }
.bs .vhf-news-desc { font-size: 13px; color: var(--bs-ink-2); line-height: 1.5; margin-top: 3px; }
.bs .vhf-news-analysis {
  font-size: 12.5px;
  color: var(--bs-ink-2);
  line-height: 1.5;
  margin-top: 5px;
  padding-left: 9px;
  border-left: 2px solid var(--bs-camel);
}

/* ── the league table: standings gets three fantasy-only accents on top of
   the generic .vh-standings (19-verticals.css) — my row, the qualification
   cutoff, the advancing accent bar. Everything else about the table (th/td,
   right-align, the emphasised numeric cell) is the shared system. ── */
.bs .vh-standings tr.vhf-row-mine td { background: var(--gold-bg); }
.bs .vh-standings tr.vhf-row-cutoff td { border-top: 2px solid var(--bs-camel-deep); }
.bs .vh-standings td.vhf-cell-advancing {
  box-shadow: inset 3px 0 0 var(--bs-ok);
  /* The inset bar is drawn INSIDE the cell, so without this the first letter
     of a qualifying team's name sits under it — seen at 744px on rows 1 and 2. */
  padding-left: 10px;
}
/* The column the table sorts on, weighted so it reads as the answer and
   W-L reads as the detail. */
.bs .vh-standings td.vhf-cell-pts { font-weight: 700; color: var(--bs-ink); }

/* The manager beside the team. Deliberately QUIETER than the team name: the
   team is what the row is, the person is who owns it. `--bs-ink-3` is the
   token the theme already maps `--muted` to, so this reads as secondary in
   both palettes rather than reaching for a colour of its own. Declared BELOW
   the .vhf-cell-pts rule it sits next to, because equal specificity is
   decided by source order. */
.bs .vh-standings td.vhf-cell-owner {
  color: var(--bs-ink-3);
  font-size: 0.92em;
  white-space: nowrap;
}

/* ── the full scoreboard: one bordered row per game, each wrapping the same
   .vhf-matchup/-side/-name/-pts the anchor panel already uses — a repeated
   list of the exact box the homepage shows one of. ── */
.bs .vhf-scoreboard-row {
  border: 1px solid var(--bs-rule);
  border-radius: var(--radius);
  padding: 10px 12px;
  margin-bottom: 8px;
  background: var(--bs-sheet);
}
.bs .vhf-scoreboard-row.is-mine { border-color: var(--bs-camel-deep); background: var(--gold-bg); }
.bs .vhf-scoreboard-row .vhf-matchup { margin-top: 8px; }

/* ── league rosters, with a per-player chip and (for someone else's claimed
   team) a Propose trade button ── */
.bs .vhf-roster-card {
  border: 1px solid var(--bs-rule);
  border-radius: var(--radius);
  padding: 9px 11px;
  margin-bottom: 8px;
  background: var(--bs-sheet);
}
.bs .vhf-roster-card.is-mine { border-color: var(--bs-camel-deep); background: var(--gold-bg); }
.bs .vhf-roster-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.bs .vhf-roster-name { font-family: var(--bs-ed); font-size: 14px; font-weight: 700; color: var(--bs-ink); }
.bs .vhf-roster-chips { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
.bs .vhf-roster-chip {
  font-family: var(--bs-mono);
  font-size: 10.5px;
  color: var(--bs-ink-2);
  border: 1px solid var(--bs-rule);
  border-radius: var(--radius);
  padding: 2px 6px;
  background: var(--bs-sheet);
  cursor: pointer;
}
.bs .vhf-roster-chip:hover { background: var(--gold-bg); border-color: var(--bs-camel-deep); }

/* ── "How this league works" — static rules, ported unchanged ── */
.bs .vhf-rule-item { padding: 8px 0; border-bottom: 1px solid var(--bs-rule); }
.bs .vhf-rule-item:last-child { border-bottom: none; }
.bs .vhf-rule-head { font-family: var(--bs-ed); font-size: 14.5px; font-weight: 700; color: var(--bs-ink); }
.bs .vhf-rule-body { font-size: 13px; color: var(--bs-ink-2); line-height: 1.5; margin-top: 2px; }

/* ── the player detail drill-through ── */
.bs .vhf-hero {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr);
  gap: 14px;
  padding-bottom: 13px;
  border-bottom: 2px solid var(--bs-ink);
  margin-top: 12px;
}
.bs .vhf-hero-img {
  width: 100%;
  border: 1px solid var(--border-hi);
  border-radius: var(--radius);
  background: var(--bs-stock-2);
  object-fit: cover;
}
.bs .vhf-hero-body { min-width: 0; }
.bs .vhf-hero-name {
  font-family: var(--bs-ed);
  font-variation-settings: 'WONK' 1;
  font-weight: 900;
  font-size: 25px;
  letter-spacing: -.02em;
  line-height: 1.05;
  color: var(--bs-ink);
}

.bs .vhf-stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); border-bottom: 1px solid var(--bs-rule); margin-top: 14px; }
.bs .vhf-stat { padding: 11px 8px; border-right: 1px solid var(--bs-rule); }
.bs .vhf-stat:last-child { border-right: none; }
.bs .vhf-stat-val {
  font-family: var(--bs-mono);
  font-size: 20px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
  color: var(--bs-ink);
}
.bs .vhf-stat-lbl {
  font-family: var(--bs-mono);
  font-size: 9.5px;
  color: var(--bs-ink-3);
  text-transform: uppercase;
  letter-spacing: .06em;
  margin-top: 2px;
}

/* Weekly points — one hue, a hatched fill for the forecast, never a second
   colour (actual vs projected is CERTAINTY, not identity). See
   js/11e-fantasy.js's _vhFantasyWeeklyChart for why this is not
   js/10-charts-ui.js's drawRadar/drawRatingSparkline wearing a new name: a
   categorical week-by-week bar is a different chart FORM, not a reskin of a
   radar polygon or a time-series line. */
.bs .vhf-chart { margin-top: 10px; }
.bs .vhf-chart-plot { display: flex; align-items: flex-end; gap: 3px; height: 96px; }
.bs .vhf-chart-col { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; height: 100%; }
.bs .vhf-chart-bar { background: var(--bs-ink); border-radius: var(--radius-sm) var(--radius-sm) 0 0; }
.bs .vhf-chart-bar.is-proj {
  background: repeating-linear-gradient(45deg, var(--bs-ink) 0 2px, transparent 2px 4px);
  border: 1px solid var(--bs-ink);
  border-bottom: none;
}
.bs .vhf-chart-axis { display: flex; gap: 3px; margin-top: 5px; }
.bs .vhf-chart-axis-lbl { flex: 1; text-align: center; font-family: var(--bs-mono); font-size: 8.5px; color: var(--bs-ink-3); }
.bs .vhf-chart-axis-lbl.is-proj { color: var(--bs-blue); }

/* Eligible slots — bigger, filled pills, a step up from the small .vhf-tag
   badge: fantasy.html's own distinction between "a status worth a small
   chip" (rookie, injury) and "a slot this player can fill" (a bigger,
   solid-fill pill). */
.bs .vhf-slot-pill {
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .08em;
  color: var(--bs-sheet);
  background: var(--bs-ink);
  padding: 3px 7px;
  border-radius: var(--radius);
}
.bs .vhf-slot-pill.is-flex { background: var(--bs-blue); }

/* ══ PHASE 3: the draft room ═══════════════════════════════════════════════
   js/11e-fantasy.js's own header ("THE DRAFT ROOM") has the timer-lifecycle
   story; this is only the shapes that section's markup needs and the other
   fantasy surfaces don't — the clock card and the round-by-slot board. The
   pool reuses .vhf-search-input/.vhf-filter-row/.vhf-filter-btn/
   .vhf-prow-list above; the roster cards reuse League's own .vhf-roster-*
   verbatim. */

.bs .vhf-draft-clock {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 14px 16px;
  border: 2px solid var(--bs-ink);
  border-radius: var(--radius);
  background: var(--bs-sheet);
}
.bs .vhf-draft-clock.is-mine { border-color: var(--bs-ok); background: var(--green-bg); }
.bs .vhf-draft-clock-who { display: flex; flex-direction: column; gap: 3px; }
.bs .vhf-draft-clock-name {
  font-family: var(--bs-ed);
  font-variation-settings: 'WONK' 1;
  font-weight: 900;
  font-size: 20px;
  letter-spacing: -.01em;
  line-height: 1.15;
  color: var(--bs-ink);
}
.bs .vhf-draft-clock-num {
  font-family: var(--bs-mono);
  font-size: 30px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
  color: var(--bs-pen);
}
.bs .vhf-draft-clock.is-mine .vhf-draft-clock-num { color: var(--bs-ok); }

.bs .vhf-board-wrap { overflow-x: auto; margin-top: 10px; }
.bs .vhf-board { border-collapse: collapse; }
.bs .vhf-board th {
  font-family: var(--bs-mono);
  font-size: 8.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  font-weight: 600;
  padding: 0 5px 5px;
  text-align: left;
  border-bottom: 1px solid var(--bs-ink);
  max-width: 104px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bs .vhf-board td {
  padding: 3px;
  border: 1px solid var(--bs-rule);
  vertical-align: top;
  max-width: 104px;
  background: var(--bs-stock-2);
}
.bs .vhf-board td.is-filled { background: var(--bs-sheet); cursor: pointer; }
.bs .vhf-board td.is-current { outline: 2px solid var(--bs-camel-deep); outline-offset: -2px; }
.bs .vhf-board-rd { font-family: var(--bs-mono); font-size: 10px; color: var(--bs-ink-3); padding: 3px 6px 3px 0; }
.bs .vhf-board-player {
  font-family: var(--bs-ed);
  font-size: 11.5px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--bs-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bs .vhf-board-meta { font-family: var(--bs-mono); font-size: 8.5px; color: var(--bs-ink-3); }
.bs .vhf-board-onclock { font-family: var(--bs-mono); font-size: 8.5px; color: var(--bs-blue); padding: 3px; }

/* ── the auction's sealed-bid box ────────────────────────────────────────────
   Lives inside `.vhf-prow-act`, next to the projection and the action button,
   so the auction list is the SAME row component as every other player list —
   headshot, rookie/TE tag, injury tag and all. Those tags are not decoration
   here: there is no TE slot in Mark's roster, so the rookie-or-TE house rule is
   the only reason a tight end is worth money at all, and a bidder has to see
   which players satisfy it before deciding what to pay.

   Fixed width so a column of them lines up down the list. The spinners are
   stripped because at this size they swallow half the field on a phone. */
.vhf-prow-bid {
  width: 4.5rem;
  padding: 4px 6px;
  text-align: right;
  font-family: var(--bs-mono, ui-monospace, monospace);
  font-size: 15px;
  font-weight: 600;
  color: var(--bs-ink);
  /* ON REAL TOKENS AS OF 2026-09-09. `--bs-surface` and `--bs-accent` were
     never defined anywhere, so this rule drew a white field with a #1f6feb
     BLUE focus ring in an app whose ground is off-white and whose accent is
     bronze — and the 6px radius was three times the system's. Nothing errored
     and every fallback resolved, which is exactly why it survived: see
     test_no_stylesheet_reaches_for_a_token_that_does_not_exist. */
  background: var(--card-hi);
  border: 1px solid var(--bs-rule);
  border-radius: var(--radius);
  -moz-appearance: textfield;
  appearance: textfield;
}
.vhf-prow-bid::-webkit-outer-spin-button,
.vhf-prow-bid::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.vhf-prow-bid:focus-visible {
  outline: 2px solid var(--bs-blue);
  outline-offset: 1px;
}

/* ══ PICKEMS AND THE SEASON STANDING (js/11g-fantasy-pickems.js) ═════════════
   The card's whole job is to make a permutation legible on a touch screen while
   part of it is already read-only. Two decisions carry that:

   1. THE RANK POOL IS ALWAYS ON SCREEN, spent numbers struck rather than
      removed. A rank that simply vanished would read as a bug; a struck one
      reads as spent, which is the actual rule.
   2. NOTHING DRAGS. Rows lock at their own kickoff, so a drag list would carry
      pinned rows a student keeps trying to move. Tapping a rank cell opens a
      chooser under that row instead.

   Camel where it is SEEN, bronze where it must be READ — 01-tokens.css's rule.
   Camel appears here only as a fill behind a locked rank and as the picked
   team's ground; every number and label is ink or bronze. */

.bs .vhf-pickem-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--bs-rule);
}
.bs .vhf-pickem-week {
  font-family: var(--bs-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--bs-ink);
}
.bs .vhf-pickem-count {
  font-family: var(--bs-mono);
  font-size: 11px;
  letter-spacing: .06em;
  color: var(--bs-ink-3);
}

/* ── the pool ── */
.bs .vhf-pickem-pool {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  padding: 10px 0 14px;
}
.bs .vhf-pickem-pool-lab {
  font-family: var(--bs-mono);
  font-size: 10px;
  letter-spacing: .12em;
  color: var(--bs-ink-3);
  margin-right: 4px;
}
.bs .vhf-rank {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 30px;
  height: 30px;
  padding: 0 6px;
  font-family: var(--bs-mono);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--bs-ink);
  background: var(--bs-sheet);
  border: 1px solid var(--bs-rule);
  border-radius: var(--radius);
}
.bs .vhf-rank.is-spent {
  color: var(--bs-ink-3);
  text-decoration: line-through;
  background: var(--bs-stock-2);
}
/* Spent on a game that has already kicked off — gone for good, not merely
   reassignable. Camel as a FILL, never as the numeral's colour. */
/* INK on camel, not --bs-camel-on-ink. That token is camel FOR PLACING ON INK;
   used as the colour on a camel fill it is camel on camel, and the browser pass
   showed exactly that — two solid blocks with no readable digit. This is trap 12's
   sibling in 01-tokens.css: camel where it can be SEEN, never where it must be
   READ. Measured: what shipped was 1.00:1 — literally invisible. This is 4.77:1. */
.bs .vhf-rank.is-locked {
  background: var(--bs-camel);
  border-color: var(--bs-camel-deep);
  color: var(--bs-ink);
  text-decoration: none;
}

/* ── a game row ── */
.bs .vhf-pickem-list { display: flex; flex-direction: column; }
.bs .vhf-pickem-row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--bs-rule);
}
.bs .vhf-pickem-row.is-locked { opacity: .72; }

.bs .vhf-pickem-teams { display: flex; gap: 6px; min-width: 0; }
.bs .vhf-pickem-team {
  flex: 1 1 0;
  /* 44px: these are tapped with a finger on an iPad, not clicked. */
  min-height: 44px;
  padding: 0 10px;
  font-family: var(--bs-mono);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--bs-ink);
  background: var(--bs-sheet);
  border: 1px solid var(--bs-rule);
  border-radius: var(--radius);
  cursor: pointer;
  transition: background .12s ease, border-color .12s ease;
}
.bs .vhf-pickem-team:hover:not(:disabled) { border-color: var(--bs-ink-3); }
.bs .vhf-pickem-team:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 2px; }
.bs .vhf-pickem-team.is-picked {
  color: var(--bs-sheet);
  background: var(--bs-ink);
  border-color: var(--bs-ink);
}
.bs .vhf-pickem-team:disabled { cursor: default; }

.bs .vhf-pickem-rankcell {
  min-width: 52px;
  min-height: 44px;
  font-family: var(--bs-mono);
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  color: var(--bs-ink-3);
  background: var(--bs-stock-2);
  border: 1px dashed var(--bs-rule);
  border-radius: var(--radius);
  cursor: pointer;
}
.bs .vhf-pickem-rankcell.is-set {
  color: var(--bs-blue);
  font-size: 18px;
  font-weight: 600;
  background: var(--bs-sheet);
  border-style: solid;
}
.bs .vhf-pickem-rankcell:disabled { cursor: default; opacity: .6; }
.bs .vhf-pickem-rankcell:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 2px; }

.bs .vhf-pickem-when {
  min-width: 92px;
  text-align: right;
  font-family: var(--bs-mono);
  font-size: 10px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
}

.bs .vhf-pickem-chooser {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 10px 0 4px;
}
.bs .vhf-rank--tap { min-width: 44px; height: 44px; cursor: pointer; }
.bs .vhf-rank--tap:disabled { cursor: default; }
.bs .vhf-rank--tap.is-picked {
  color: var(--bs-sheet);
  background: var(--bs-ink);
  border-color: var(--bs-ink);
  text-decoration: none;
}
.bs .vhf-rank--tap:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 2px; }

/* Other people's picks, only once the game has kicked off. */
.bs .vhf-pickem-field {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  padding-top: 6px;
}
.bs .vhf-pickem-fieldpick {
  font-family: var(--bs-mono);
  font-size: 10px;
  letter-spacing: .04em;
  color: var(--bs-ink-3);
}

.bs .vhf-pickem-foot {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  padding-top: 16px;
}
.bs .vhf-pickem-hint { font-size: 12px; color: var(--bs-ink-3); max-width: 44ch; }
.bs .vhf-pickem-saved { color: var(--bs-ok); font-weight: 600; }

/* ── the season standing ── */
.bs .vhf-standing { width: 100%; }
.bs .vhf-standing-rank {
  font-family: var(--bs-mono);
  font-variant-numeric: tabular-nums;
  color: var(--bs-ink-3);
}
.bs .vhf-standing-num {
  text-align: right;
  font-family: var(--bs-mono);
  font-variant-numeric: tabular-nums;
}
.bs .vhf-standing-total { font-weight: 600; color: var(--bs-ink); }
.bs .vhf-standing tr.is-mine { background: var(--gold-bg); }
.bs .vhf-standing tr.is-mine td { border-color: var(--bs-camel-deep); }
.bs .vhf-standing-note { max-width: 62ch; margin-top: 8px; }

/* The contest columns are the first thing to go when the measure runs out —
   the total and the name are what a standing is for. */
@media (max-width: 700px) {
  .bs .vhf-standing th:nth-child(n+3):nth-last-child(n+2),
  .bs .vhf-standing td:nth-child(n+3):nth-last-child(n+2) { display: none; }
  .bs .vhf-pickem-row { grid-template-columns: 1fr auto; }
  .bs .vhf-pickem-when { grid-column: 1 / -1; text-align: left; min-width: 0; }
}

/* ── Trending adds (Sleeper-wide) — WAVE 5, 2026-09-07 ───────────────────────
   The board is now `.vhf-prow-list` of `.vhf-prow--agate` rows, so eight rows
   really do scan in one look and a player reads identically here, in the
   auction pool and in the box score.

   SEVEN RULES WERE RETIRED HERE, not left behind: `.vhf-trending`,
   `-row`, `-shot`, `-who`, `-name`, `-meta` and `-count` were a private
   reimplementation of `.vhf-prow`, one grid and one portrait size away from it.
   The primitives header below says old rules migrate onto `.vhf-num` surface by
   surface rather than being deleted, and that is so an UNSHIPPED wave cannot
   break on a rename — this is the last wave, and nothing else ever referenced
   these. `.vhf-trend-count` had already reached the primitive's own conclusion
   on its own ("the count is the point of the row, so it is the one tabular
   number here"); it is now `.vhf-num--row`, which says the same thing once.

   The two availability chips STAY. They are not a number treatment and have no
   primitive to migrate onto: the OK ink on `Free` against the plain rule on
   `Rostered` is the whole signal a scanner is looking for.

   AND `.vhf-trend-count` STAYS, reduced to the one thing that is not a number
   treatment: the width it reserves. Moving the count onto `.vhf-num--row` and
   dropping this was a REGRESSION, caught in the browser at 834 and not by any
   count of painted elements — `tabular-nums` makes digits equal width, but
   "227k" is four glyphs and "99k" is three, so the right edges still aligned
   while the LEFT edges stepped 8px and pushed the Free chips 9px in and out
   down the list. Exactly the defect wave 3 fixed on the lineup column, where
   `Change`/`Fill`/`Locked` were three lengths and moved the projection on every
   row. A reserved column belongs on the column, not on the primitive: putting
   a min-width on `.vhf-num--row` would set it for the box score too. */
.bs .vhf-trend-count { min-width: 42px; text-align: right; }
.bs .vhf-trend-free,
.bs .vhf-trend-owned {
  font-size: 11px; font-weight: 700; letter-spacing: .02em;
  padding: 2px 6px; border-radius: var(--radius);
  white-space: nowrap;
}
.bs .vhf-trend-free { color: var(--bs-ok); border: 1px solid var(--bs-ok); }
.bs .vhf-trend-owned { color: var(--bs-ink-2); border: 1px solid var(--bs-rule); }

/* ══ THE SHARED PRIMITIVES — 2026-09-06 ══════════════════════════════════════
   Five things every fantasy surface needs, defined once so the waves that
   follow inherit them rather than each inventing its own. The design sheet
   these come from is docs/superpowers/specs/2026-09-06-fantasy-agate-design.md.

   Why they were needed: one quantity — fantasy points — was being set three
   different ways in three places. `.vhf-matchup-pts` is Fraunces 900 at 30px,
   `.vhf-stat-val` is Plex Mono 600 at 20px, and `.vhf-prow-num` is Plex Mono
   400 at 12px WITH NO TABULAR FIGURES AT ALL. The same 12.4 therefore looked
   like three different kinds of object depending which surface you were on,
   and nothing lined up when it stacked into a column. On a live scoring
   surface — nine starters, two teams, a running total and a projection — that
   is the whole readability of the page.

   The existing three rules are deliberately NOT deleted here; they get migrated
   onto `.vhf-num` surface by surface as each wave touches them, so that a wave
   that has not shipped yet cannot be broken by a rename it did not ask for.
   `.vhf-trend-count` was the one that had already reached this conclusion on
   its own ("the count is the point of the row, so it is the one tabular number
   here"); as of wave 5 its type comes from `.vhf-num--row` and it keeps only
   the width it reserves for its column. */

/* ── 1. the number ───────────────────────────────────────────────────────── */
.bs .vhf-num {
  font-family: var(--bs-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
  color: var(--bs-ink);
}
.bs .vhf-num--hero  { font-size: 34px; font-weight: 600; line-height: 1; }
.bs .vhf-num--row   { font-size: 15px; font-weight: 500; line-height: 1.1; }
.bs .vhf-num--agate { font-size: 11.5px; font-weight: 400; line-height: 1.1; color: var(--bs-ink-3); }

/* ── 2. certainty, never identity ────────────────────────────────────────────
   The rule js/11e-fantasy.js's weekly chart already committed to — "actual vs
   projected is CERTAINTY, not identity" — lifted off the chart and made general.
   ONE hue for all three states. A projection is hatched and muted; a live number
   carries the only red mark in the fantasy system. Never a second colour: a
   green projection beside a black actual reads as good-vs-bad rather than
   forecast-vs-result, which is a different claim entirely.

   The hatch is a repeating-linear-gradient underline rather than a border so it
   sits under the digits without changing the row's box height — a projected and
   an actual number in the same column have to share a baseline. */
.bs .vhf-num.is-proj,
.bs .vhf-prow-num.is-proj {
  color: var(--bs-ink-3);
  background-image: repeating-linear-gradient(135deg,
    var(--bs-rule) 0 1px, transparent 1px 4px);
  background-position: 0 100%;
  background-size: 100% 3px;
  background-repeat: repeat-x;
  padding-bottom: 4px;
}
.bs .vhf-num.is-live { color: var(--bs-ink); }
.bs .vhf-live-dot {
  display: inline-block;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--bs-pen);
  vertical-align: .12em;
  margin-right: 5px;
}
/* A live game is the one place motion is honest here — it means "this number is
   still moving". Anything more than a slow pulse on a 5px dot would be noise on
   a page whose whole argument is density. */
@media (prefers-reduced-motion: no-preference) {
  .bs .vhf-live-dot { animation: vhf-live-pulse 2.4s ease-in-out infinite; }
}
@keyframes vhf-live-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* ── 3. the row at three densities ───────────────────────────────────────────
   `.vhf-prow` was one fixed shape, so a wire line and a live starter got the
   same 34px portrait and the same padding. These are modifiers on the existing
   row, not a second and third row builder — _vhFantasyPlayerRow already takes an
   options object, so the density rides in there. */
.bs .vhf-prow--agate { padding: 6px 14px; gap: 9px; }
.bs .vhf-prow--agate .vhf-prow-img { width: 22px; height: 22px; }
.bs .vhf-prow--agate .vhf-prow-nm { font-size: 12px; font-weight: 500; }
.bs .vhf-prow--agate .vhf-prow-sub { display: none; }
/* AND THE ONE SURFACE THAT NEEDS BOTH. `--agate` hides `.vhf-prow-sub` because
   there is no room for a second line beside a 22px portrait — which is right
   for a live starter, whose position is the SLOT they are sitting in. On the
   wire's trending board it would drop the two facts a manager is scanning for:
   what he plays, and who he plays for. So the sub comes back on the name's own
   line, which is what an agate sports page does anyway, and the row stays one
   line high and loses nothing.
   NOT applied to the auction pool, which stays at standard density on Mark's
   settled call — a bidder is reading tags and a projection as well, and that is
   more than one line can carry. */
.bs .vhf-prow--agate.vhf-prow--inline-sub .vhf-prow-name {
  display: flex;
  align-items: baseline;
  gap: 7px;
  min-width: 0;
}
.bs .vhf-prow--agate.vhf-prow--inline-sub .vhf-prow-nm {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bs .vhf-prow--agate.vhf-prow--inline-sub .vhf-prow-sub {
  display: block;
  white-space: nowrap;
  flex: none;
}

.bs .vhf-prow--expanded { padding: 13px 14px; flex-wrap: wrap; }
.bs .vhf-prow--expanded .vhf-prow-img { width: 42px; height: 42px; }

/* The stat line an expanded row carries. Keys come from VHF_KEY_LABEL, the same
   ones the game-log table already prints, so a player reads the same way in a
   row as in their own detail page. */
.bs .vhf-statline {
  flex-basis: 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 3px 16px;
  margin-top: 9px;
  padding-top: 9px;
  border-top: 1px dotted var(--bs-rule);
}
.bs .vhf-statline > div { display: flex; flex-direction: column; gap: 1px; }
.bs .vhf-statline dt {
  font-family: var(--bs-mono);
  font-size: 8.5px;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
}
.bs .vhf-statline dd {
  font-family: var(--bs-mono);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  font-weight: 500;
  color: var(--bs-ink);
  margin: 0;
}

/* ── 4. the freshness stamp ──────────────────────────────────────────────────
   The one element this product needs that Sleeper's does not. Sleeper's numbers
   are live to the second, so "as of" would be noise there; ours refresh on a
   sync, and a number whose age is not stated is a number a student will trust
   further than it deserves. Every surface showing a synced figure carries one. */
.bs .vhf-asof {
  font-family: var(--bs-mono);
  font-size: 11px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
}
.bs .vhf-asof b { color: var(--bs-ink-2); font-weight: 600; }

/* ── 5. the pre-league state ─────────────────────────────────────────────────
   Two definition lists: the rules of the league, and the two dates that matter
   before you have joined one. Both are hairline-ruled rows rather than cards —
   this is reference material a student reads once and then never again, and
   nine bordered cards would give nine equal-weight objects where what is wanted
   is one scannable list.

   The term column is a fixed measure so the definitions share a left edge; it
   collapses to a stacked label on a phone, where 9rem of the width is too much
   to spend on a label. */
.bs .vhf-rules,
.bs .vhf-cal {
  border-top: 1px solid var(--bs-rule);
  margin-top: 12px;
  /* CAPPED AT THE READING MEASURE, found in the browser at 1024 and not before:
     the definitions were running 806px against --read-measure's 672px, 20% over.
     These are prose a student reads once and has to actually absorb — the exact
     case that token exists for ("ONE column width for anything a student reads
     for more than a few seconds"), and nothing else on this screen was policing
     it because the grid's `1fr` will happily take whatever the iPad gives it.

     The cap goes on the LIST, not the definition cell, so the hairline rules end
     with the column instead of running on past the text. A rule that overshoots
     its own column is the thing that makes a page look like a table that lost
     its last column, which is precisely what this direction is trying not to be. */
  max-width: calc(9rem + 18px + var(--read-measure));
}
.bs .vhf-rule-row {
  display: grid;
  grid-template-columns: 9rem minmax(0, 1fr);
  gap: 4px 18px;
  padding: 11px 0;
  border-bottom: 1px solid var(--bs-rule);
}
.bs .vhf-rule-term {
  font-family: var(--bs-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  padding-top: .35em;
}
.bs .vhf-rule-def {
  font-family: var(--bs-ed);
  font-size: 14.5px;
  line-height: 1.5;
  color: var(--bs-ink-2);
}

/* The calendar carries a third column — the date itself, which is the thing
   being looked up, so it is the one element here set in the number face. */
.bs .vhf-cal-row {
  display: grid;
  grid-template-columns: 9rem auto minmax(0, 1fr);
  gap: 4px 18px;
  align-items: baseline;
  padding: 11px 0;
  border-bottom: 1px solid var(--bs-rule);
}
.bs .vhf-cal-what {
  font-family: var(--bs-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
}
.bs .vhf-cal-when {
  font-family: var(--bs-mono);
  font-variant-numeric: tabular-nums;
  font-size: 14px;
  font-weight: 600;
  color: var(--bs-ink);
  white-space: nowrap;
}
.bs .vhf-cal-note {
  font-family: var(--bs-ed);
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--bs-ink-3);
}

/* iPad portrait holds the three-column calendar; a phone does not, and Vanguard
   students are on iPads — 744 is the narrowest real device, not 320. The stack
   below is the safety net for a split-view window, not the target. */
@media (max-width: 700px) {
  .bs .vhf-rule-row { grid-template-columns: minmax(0, 1fr); gap: 3px; }
  .bs .vhf-rule-term { padding-top: 0; }
  .bs .vhf-cal-row { grid-template-columns: minmax(0, 1fr); gap: 3px; }
}

/* ── 6. the auction money band — WAVE 2, 2026-09-06 ──────────────────────────
   Restyle only; not one line of auction MECHANICS moved. Bids close Wed 17:00
   and this is the surface where a defect costs somebody a roster, so the rule
   for this wave was that nothing about validation, max_bid, settle order or
   what gets POSTed changes — only how it is painted.

   The budget was previously a sentence set in Fraunces ("You have $200") as
   though it were a team name, and `committed_if_every_bid_won` — which the API
   has always returned — appeared only as prose, and only once it had already
   gone over. Three figures a bidder is constantly comparing now sit together as
   figures. Over-commitment is deliberately NOT red: it is legal and the banner
   explains why, so it takes the accent, not the alarm. */
.bs .vhf-money,
.bs .vhf-figs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  border: 1px solid var(--bs-ink);
  border-radius: var(--radius);
  background: var(--bs-sheet);
  margin-bottom: 10px;
}
.bs .vhf-money-cell {
  padding: 13px 15px;
  border-right: 1px solid var(--bs-rule);
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.bs .vhf-money-cell:last-child { border-right: none; }
.bs .vhf-money-lbl {
  font-family: var(--bs-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
}
/* Over budget is legal here — it is the accent, not the pen. Red on this
   figure would read as "you have done something wrong", and the whole point of
   the banner beside it is that they have not. */
.bs .vhf-money .vhf-num.is-over { color: var(--bs-blue); }
.bs .vhf-money .vhf-num.is-over::after {
  content: ' over';
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
}

/* The countdown, out of the paragraph and onto its own line. It is the only
   thing in the band that moves while you are looking at it. */
.bs .vhf-draftband-clock {
  font-family: var(--bs-mono);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--bs-camel-on-ink);
  margin-top: 2px;
}

@media (max-width: 700px) {
  /* Two up rather than three: at 560 a third column puts $200 on two lines. */
  .bs .vhf-money,
  .bs .vhf-figs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .bs .vhf-money-cell:nth-child(2) { border-right: none; }
  .bs .vhf-money-cell:nth-child(3) { border-top: 1px solid var(--bs-rule); }
}

/* ── 7. the box score — WAVE 4, 2026-09-06 ───────────────────────────────────
   The agate page's centrepiece, and the one surface in this redesign that was
   BUILT rather than restyled: `_vhFantasyAnchor` previously painted two totals
   and a sentence.

   Two facing columns of starters over a centre slot rail — Sleeper's shape,
   because it is the right one: the eye compares the two numbers on a row, and
   the slot label between them says why those two players are being compared.
   The old `.vhf-matchup` grid is deliberately left in place; it still renders
   the fixture that has no saved lineups, where there are no starters to face
   off and two totals is the honest answer. */
.bs .vhf-box {
  border: 1px solid var(--bs-ink);
  border-radius: var(--radius);
  background: var(--bs-sheet);
  overflow: hidden;
}

.bs .vhf-box-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  gap: 10px;
  align-items: end;
  padding: 14px 14px 12px;
  border-bottom: 2px solid var(--bs-ink);
}
.bs .vhf-box-team { min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.bs .vhf-box-team.is-away { text-align: right; align-items: flex-end; }
.bs .vhf-box-team-nm {
  font-family: var(--bs-ed);
  font-variation-settings: 'WONK' 1;
  font-weight: 800;
  font-size: clamp(14px, 2vw, 17px);
  line-height: 1.12;
  letter-spacing: -.01em;
  color: var(--bs-ink);
}
.bs .vhf-box-team.is-away .vhf-box-team-nm { color: var(--bs-ink-2); }
/* The running total. Plex Mono, not Fraunces: it is a figure that changes while
   you watch it, and it has to align with the column of starter numbers below. */
.bs .vhf-box-run {
  font-family: var(--bs-mono);
  font-variant-numeric: tabular-nums;
  font-size: clamp(26px, 5vw, 34px);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -.025em;
  color: var(--bs-ink);
}
.bs .vhf-box-team.is-mine .vhf-box-run { color: var(--bs-blue); }
.bs .vhf-box-proj {
  font-family: var(--bs-mono);
  font-size: 11px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
}
.bs .vhf-box-vs {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  font-family: var(--bs-mono);
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  padding-bottom: 8px;
  white-space: nowrap;
}

/* the slot rail */
.bs .vhf-slot-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 4.6rem minmax(0, 1fr);
  align-items: center;
  border-bottom: 1px solid var(--bs-rule);
}
.bs .vhf-slot-row:last-child { border-bottom: none; }
/* Both starters still to play. Recessed rather than tinted — the ground says
   "nothing has happened here yet" without spending a colour on it. */
.bs .vhf-slot-row.is-tocome { background: var(--bs-stock-2); }
.bs .vhf-box .vhf-slot-badge {
  border-left: 1px solid var(--bs-rule);
  border-right: 1px solid var(--bs-rule);
  border-radius: 0;
  align-self: stretch;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 9px 2px;
  margin: 0;
}
.bs .vhf-starter {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px;
  align-items: center;
  padding: 8px 12px;
  min-width: 0;
}
/* The mirror. DOM is name-then-number on both sides; the away column swaps them
   with `order` so the two running numbers sit either side of the slot rail,
   which is what makes a row comparable at a glance. */
.bs .vhf-starter.is-away { grid-template-columns: auto minmax(0, 1fr); }
.bs .vhf-starter.is-away .vhf-st-who { text-align: right; order: 2; }
.bs .vhf-starter.is-away .vhf-num { order: 1; }
.bs .vhf-st-who { min-width: 0; }
.bs .vhf-st-nm {
  font-family: var(--bs-ui);
  font-size: 13px;
  font-weight: 600;
  color: var(--bs-ink);
  line-height: 1.2;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bs .vhf-st-meta {
  font-family: var(--bs-mono);
  font-size: 11px;
  letter-spacing: .04em;
  color: var(--bs-ink-3);
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bs .vhf-box-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 8px 16px;
  padding: 11px 14px;
  border-top: 2px solid var(--bs-ink);
  background: var(--bs-stock-2);
}

/* ── narrow: the rail becomes a heading ──────────────────────────────────────
   Below ~620 the three-column row cannot hold two names and two numbers, so the
   slot label goes above its pair and the two starters stack. Both keep their
   number on the right — the away side's mirrored layout is what makes the
   facing columns readable, and mirroring one row of a stack just looks broken.
   Vanguard students are on iPads, so this is the split-view net, not the target. */
@media (max-width: 620px) {
  .bs .vhf-slot-row { grid-template-columns: minmax(0, 1fr); }
  .bs .vhf-box .vhf-slot-badge {
    border: none;
    border-bottom: 1px solid var(--bs-rule);
    justify-content: flex-start;
    padding: 5px 12px;
    background: var(--bs-stock-2);
  }
  .bs .vhf-starter,
  .bs .vhf-starter.is-away { grid-template-columns: minmax(0, 1fr) auto; }
  /* Mirror OFF when stacked — both starters read name-first. */
  .bs .vhf-starter.is-away .vhf-st-who { text-align: left; order: 1; }
  .bs .vhf-starter.is-away .vhf-num { order: 2; }
  .bs .vhf-starter.is-away { border-top: 1px dotted var(--bs-rule); }
  .bs .vhf-box-head { grid-template-columns: minmax(0, 1fr); }
  .bs .vhf-box-vs { display: none; }
  .bs .vhf-box-team.is-away { text-align: left; align-items: flex-start; }
}

/* ── 8. the lineup, wave 3 ────────────────────────────────────────────────────
   A fourth column for the projection, so nine of them line up on the decimal
   point instead of trailing off the end of nine different sentences. The grid
   gains the column rather than squeezing the name: 62px badge, name, number,
   button. */
.bs .vhf-lineup-row { grid-template-columns: 62px minmax(0, 1fr) 3.4rem var(--vhf-act); }
.bs .vhf-lineup-proj { text-align: right; }
/* The action column is a FIXED measure, for two reasons. The buttons say
   Change / Fill / Locked, so an `auto` track was a different width on every
   row and left the projections on a ragged edge; and the total below can only
   sit under its own addends if it knows how wide the column to its right is. */
.bs .vhf-lineup-list { --vhf-act: 5.4rem; }
.bs .vhf-lineup-row > .vhf-btn-sm { width: 100%; }

/* The total the nine slots add up to, on the same right edge as the column
   above it so the sum sits under its addends. */
/* Measured 2026-09-07: this row was flex-end against the SECTION, which put the
   sum 78px right of the column it totals. It now shares the lineup row's grid,
   so the total lands directly under its addends — which is the only reason to
   print a sum in a column at all. */
.bs .vhf-lineup-total {
  --vhf-act: 5.4rem;
  display: grid;
  grid-template-columns: 62px minmax(0, 1fr) 3.4rem var(--vhf-act);
  gap: 10px;
  align-items: baseline;
  padding: 10px 0 2px;
  border-top: 1px solid var(--bs-ink);
  margin-top: 2px;
}
.bs .vhf-lineup-total .vhf-money-lbl { grid-column: 2; text-align: right; }
.bs .vhf-lineup-total .vhf-num { grid-column: 3; text-align: right; }
.bs .vhf-lineup-total .vhf-num { font-size: 19px; font-weight: 600; }

@media (max-width: 620px) {
  /* The button drops under the row rather than crushing the name to nothing. */
  .bs .vhf-lineup-row { grid-template-columns: 62px minmax(0, 1fr) auto; }
  .bs .vhf-lineup-row > .vhf-btn-sm { grid-column: 2 / -1; justify-self: start; }
}

/* ── 9. claim order, wave 5 ──────────────────────────────────────────────────
   The ordinal column and the two move buttons. `.vhf-list-row` gains a leading
   number and a control pair; the ordinal is agate because it is a position, not
   a quantity — it is read to locate a row, never compared or summed. */
.bs .vhf-list-row.is-ordered {
  display: grid;
  grid-template-columns: 1.6rem minmax(0, 1fr) auto auto auto;
  gap: 10px;
  align-items: center;
}
.bs .vhf-claim-order { text-align: right; }
.bs .vhf-claim-moves { display: flex; gap: 4px; }
/* Square, so a column of them reads as one control and not as two words. The
   arrow is the label; the accessible name is on aria-label. */
.bs .vhf-claim-move {
  padding: 4px 8px;
  line-height: 1;
  min-width: 1.9rem;
  text-align: center;
}

@media (max-width: 620px) {
  /* Order and name on top, the money and the controls beneath — five columns
     do not fit, and shrinking the name to fit them is the wrong trade. */
  .bs .vhf-list-row.is-ordered {
    grid-template-columns: 1.6rem minmax(0, 1fr) auto;
    row-gap: 6px;
  }
  .bs .vhf-list-row.is-ordered .vhf-claim-moves { grid-column: 2; }
  .bs .vhf-list-row.is-ordered > .vhf-btn-sm.is-danger { grid-column: 3; }
}

/* ── 10. the commissioner's league status ─────────────────────────────────────
   `.vhf-figs` shares `.vhf-money`'s grid — same object, and the auction band
   proved the shape — but the labels here are not money, so the class is not
   called money. Only the additions specific to the desk live below.

   A settled seat count takes the OK ink: "10 / 10" is the gate on opening an
   auction at all, and it is the one figure Tuesday's headcount lands in. */
.bs .vhf-commish-status-wrap {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 14px 0 16px;
  border-bottom: 1px solid var(--bs-rule);
  margin-bottom: 4px;
}
.bs .vhf-figs .vhf-num.is-ok { color: var(--bs-ok); }
/* AND `is-over`, for the wire's roster figure. The auction band's own
   `.vhf-money .vhf-num.is-over` rules are scoped to `.vhf-money`, so putting
   `is-over` on a `.vhf-figs` figure painted DEFAULT INK — the over-the-cap
   state was indistinguishable from a legal one, and nothing but the prose
   underneath said otherwise. Found by reading the computed colour off the
   painted box; asserting the class was present would have passed.
   The accent, never the alarm: a roster over the cap is legal, and the
   consequence lands on the lineup screen.
   The `' over'` suffix is deliberately NOT inherited here. On "$210" it reads
   as "$210 over"; on a roster printed "17 / 15" the limit is already in the
   figure and the sentence below already says "2 over", so a third statement of
   the same fact is noise. */
.bs .vhf-figs .vhf-num.is-over { color: var(--bs-blue); }
/* Slightly smaller than the auction's hero figures: this is orientation a
   commissioner glances at, not a number every decision is measured against. */
.bs .vhf-figs .vhf-num--hero { font-size: 26px; }

/* ── 11. the player page, wave 6 ─────────────────────────────────────────────
   `.vhf-stat-val` now rides .vhf-num, so it keeps only what is specific to a
   KPI tile: the larger size a four-up grid can carry. Everything else — the
   face, the tabular figures, the letter-spacing — comes from the primitive, so
   the same quantity can no longer look like a different object here than in
   the box score. */
.bs .vhf-stat .vhf-stat-val { font-size: 20px; font-weight: 600; }

/* The game log's points column, right-aligned and tabular via .vh-standings'
   own `.r`/`.v` classes — but that generic table is shared with every other
   vertical, so the tabular figures are scoped to the fantasy log rather than
   imposed on all of them. */
.bs .vhf-log-table td.r,
.bs .vhf-log-table th.r { font-variant-numeric: tabular-nums; text-align: right; }

/* ── 12. pickems, wave 6 ─────────────────────────────────────────────────────
   `.vhf-tabular` is the one-property helper for elements that already have
   their own face and size and only need their digits to line up — a team
   button carrying "-3.5", a rank cell carrying "12". Using .vhf-num there
   would also impose the mono face on a button whose label is a team name. */
.bs .vhf-tabular { font-variant-numeric: tabular-nums; }

/* The when-cell holds four states now (kickoff / LOCKED / live / FINAL), so it
   is a flex line rather than a bare string, and the live dot needs a baseline
   to sit on. */
.bs .vhf-pickem-when {
  display: inline-flex;
  align-items: baseline;
  gap: 3px;
  white-space: nowrap;
}
.bs .vhf-pickem-final { color: var(--bs-ink-2); }

/* ── DFS: the slate builder ────────────────────────────────────────────────
   The cap is the whole game, so it is the only thing here with a figure large
   enough to read from across a desk. Everything else is a list. */
.bs .vhf-dfs-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-top: 12px;
}
.bs .vhf-dfs-week { font-family: var(--bs-mono); font-size: 12px; letter-spacing: .06em; color: var(--bs-ink-2); }
.bs .vhf-dfs-count { font-family: var(--bs-mono); font-size: 12px; color: var(--bs-ink-3); }

.bs .vhf-dfs-meter {
  height: 6px;
  border-radius: 999px;
  background: var(--bs-stock-2);
  overflow: hidden;
  margin: 6px 0 4px;
}
.bs .vhf-dfs-bar { height: 100%; background: var(--bs-blue); transition: width var(--duration-normal, 300ms) ease; }
.bs .vhf-dfs-meter.is-over .vhf-dfs-bar { background: var(--bs-pen); }

.bs .vhf-dfs-money { display: flex; align-items: baseline; gap: 6px; margin-bottom: 10px; }
.bs .vhf-dfs-money .vhf-num { font-size: 20px; font-weight: 700; }
.bs .vhf-dfs-of { font-size: 12px; color: var(--bs-ink-3); }
.bs .vhf-dfs-left { margin-left: auto; font-family: var(--bs-mono); font-size: 12px; color: var(--bs-ink-2); }
.bs .vhf-dfs-left.is-over { color: var(--bs-pen); font-weight: 700; }

/* Every reason at once, which means this list can be nine items long. */
.bs .vhf-dfs-errors {
  margin: 0 0 10px;
  padding: 8px 10px 8px 26px;
  border: 1px solid var(--bs-pen);
  border-radius: var(--radius);
  background: rgba(194, 38, 31, .06);
  font-size: 12px;
  color: var(--bs-pen);
}
.bs .vhf-dfs-error { margin: 2px 0; }

.bs .vhf-dfs-slots { border-radius: var(--radius); overflow: hidden; }
/* The slot label is the anchor of the row — fixed width so nine rows read as a
   column rather than as nine differently-indented lines. */
.bs .vhf-dfs-slot {
  display: inline-block;
  min-width: 42px;
  font-family: var(--bs-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .06em;
  color: var(--bs-ink-3);
}
.bs .vhf-dfs-who { font-weight: 600; }
.bs .vhf-dfs-empty { color: var(--bs-ink-3); font-style: italic; }
.bs .vhf-dfs-budget { margin: 8px 0 2px; font-family: var(--bs-mono); font-size: 11px; }

/* Dimmed, never removed: a player who is too dear or already kicked off is
   information, and hiding him reads as the board being wrong. */
.bs .vhf-prow.is-dim { opacity: .55; }

.bs .vhf-dfs-foot { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 14px; }
.bs .vhf-dfs-hint { font-size: 12px; color: var(--bs-ink-3); }
.bs .vhf-dfs-saved {
  margin: 0 0 10px;
  padding: 8px 10px;
  border: 1px solid var(--bs-blue);
  border-radius: var(--radius);
  background: rgba(31, 90, 194, .06);
  font-size: 12px;
  color: var(--bs-blue);
}
/* The score, on a settled slate. Larger than the cap figure because by Monday
   it is the only number that matters. */
.bs .vhf-dfs-score { display: flex; align-items: baseline; gap: 4px; margin: 10px 0 2px; }
.bs .vhf-dfs-score .vhf-num { font-size: 30px; font-weight: 700; }

/* ── the draft monitor ───────────────────────────────────────────────────── */
.bs .vhf-mon-problems { margin: 6px 0 0; padding-left: 18px; }
.bs .vhf-mon-problem { font-size: 12px; margin: 2px 0; }
.bs .vhf-mon-head { display: flex; gap: 18px; flex-wrap: wrap; margin: 12px 0 4px; }
.bs .vhf-mon-detail {
  padding: 8px 10px;
  background: var(--bs-stock-2);
  border-bottom: 1px solid var(--bs-rule);
}
.bs .vhf-mon-sub {
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  margin: 6px 0 2px;
}
.bs .vhf-mon-line { font-family: var(--bs-mono); font-size: 11.5px; line-height: 1.55; }

/* ══ THE AUCTION BOARD, DESIGNED ══════════════════════════════════════════════
   Mark, 2026-09-09: the room worked and was never designed. Three specific
   complaints, and this section is the answer to each.

   1. "THREE CONTROLS, THREE STYLES." Search was `--bs-ui` at 13.5px on white
      with its own 10px bottom margin; the position pills and the sort select
      were mono 11px on transparent. Three rows, three faces, no relation. They
      are now ONE instrument: a single bordered surface holding all three on a
      shared baseline, so the board reads as having a control panel rather than
      an accumulation of widgets.

   2. "NO SENSE OF SCARCITY." See `positional_scarcity` — the count of players
      above replacement is circular (it equals what the league starts, by
      construction), so what ships is where the MONEY is. The band below the
      bar is one 10px rule split by share, darkest slice largest, and on the
      live board it shows RB and WR holding 81% of everything between them.
      That is the fact that changes a bid.

   3. "THE BID INPUT IS A BARE NUMBER FIELD." It was worse than bare: it was
      the one control in this file that had drifted off the design system
      entirely. `.vhf-prow-bid` asked for `--bs-surface`, `--bs-accent` and
      `--bs-ink-soft`, NONE of which exist in 01-tokens.css, so every one fell
      through to a hardcoded fallback — a #1f6feb BLUE focus ring in an app
      whose accent is bronze, a warm grey in a green-ink app, and a 6px radius
      against the system's 2px. It was also the only unscoped selector here.
      Fixed below, on real tokens.

   EVERY OVERRIDE IN THIS SECTION SITS BELOW THE RULE IT OVERRIDES, which is
   the whole reason it is appended at the end of the file rather than edited in
   beside its cousins. Equal specificity is decided by source order and an
   override declared above its base is inert — that has already shipped once
   here (see `.vhf-prow--nomedia`). */

.bs .vhf-board-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 7px 8px;
  margin-bottom: 8px;
  border: 1px solid var(--border-hi);
  border-radius: var(--radius);
  background: var(--bs-sheet);
}
/* The search field grows and the pills hold their size, so on a narrow iPad
   the row wraps with the pills intact rather than squeezing them to nothing. */
.bs .vhf-board-bar-search { flex: 1 1 11rem; min-width: 0; display: flex; }
.bs .vhf-board-bar-pos { display: flex; flex-wrap: wrap; gap: 5px; }

/* Search, stripped of its own frame because the BAR is the frame now. Below
   `.vhf-search-input`, and it has to be: both are (0,2,0). */
.bs .vhf-search-input--bare {
  width: 100%;
  margin-bottom: 0;
  padding: 4px 2px;
  border: none;
  border-bottom: 1px solid var(--bs-rule);
  border-radius: 0;
  background: transparent;
  font-family: var(--bs-mono);
  font-size: 12px;
}
.bs .vhf-search-input--bare::placeholder {
  color: var(--bs-ink-3);
  letter-spacing: .03em;
}
.bs .vhf-search-input--bare:focus-visible {
  outline: none;
  border-bottom-color: var(--bs-blue);
  border-bottom-width: 2px;
}
/* Sort keeps its type and loses `margin-left: auto` — inside the bar it is a
   member of the group, not a thing pushed to the far edge of a bare row. */
.bs .vhf-sort-select--bare { margin-left: 0; }

/* ── where the money is ──
   Not a chart. One rule, split by share, in the palette's own five-step ink
   ramp so the darkest slice IS the largest and the visual order matches the
   numeric one. No new colours, no hue encoding anything it should not. */
.bs .vhf-scarcity { margin: 0 0 10px; }
.bs .vhf-scarcity-kicker {
  font-family: var(--bs-mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  margin-bottom: 4px;
}
.bs .vhf-scarcity-band {
  display: flex;
  height: 10px;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--bs-stock-2);
}
.bs .vhf-scarcity-seg {
  min-width: 2px;
  transition: background var(--duration-fast, 150ms) ease;
}
/* A HAIRLINE BETWEEN SLICES. Found in a browser at 744px: RB and WR are 41%
   each and take adjacent steps of the ink ramp (#1b3a2a against #2f5240), so
   the largest boundary on the band — the one the whole readout is about — was
   the hardest of the four to see. The rule is the sheet colour rather than a
   border, so it cuts the band instead of shrinking each slice. */
.bs .vhf-scarcity-seg + .vhf-scarcity-seg {
  box-shadow: inset 1px 0 0 var(--bs-sheet);
}
.bs .vhf-scarcity-seg--1 { background: var(--bs-ink); }
.bs .vhf-scarcity-seg--2 { background: var(--bs-ink-2); }
.bs .vhf-scarcity-seg--3 { background: var(--bs-ink-3); }
.bs .vhf-scarcity-seg--4 { background: var(--bs-rule); }
.bs .vhf-scarcity-seg--5 { background: var(--bs-stock-2); }
/* The filtered position, lifted out of the ramp in the accent. BELOW the ramp
   so it wins on source order at equal specificity. */
.bs .vhf-scarcity-seg.is-on { background: var(--bs-blue); }

.bs .vhf-scarcity-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 3px 10px;
  margin-top: 5px;
  font-family: var(--bs-mono);
  font-size: 10.5px;
  color: var(--bs-ink-3);
  font-variant-numeric: tabular-nums;
}
.bs .vhf-scarcity-item { white-space: nowrap; }
.bs .vhf-scarcity-item.is-on { color: var(--bs-ink); font-weight: 700; }
/* The sentence under the band, for whichever position is filtered. Plain
   figures in plain words: a percentage on its own is a statistic, and what a
   bidder needs is what it MEANS for the position in front of them. */
.bs .vhf-scarcity-note {
  margin-top: 5px;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--bs-ink-2);
  max-width: 62ch;
}

/* ── the bid, as a decision ──
   A labelled group rather than a naked field, and the suggestion is now a
   BUTTON: it filled the box in already, so offering to put it back is the one
   thing a manager who has typed over it actually wants. Static text could not
   do that, and an unexplained number beside an input reads as a rule. */
.bs .vhf-bid { display: flex; align-items: center; gap: 6px; }
.bs .vhf-bid-lbl {
  font-family: var(--bs-mono);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  text-align: right;
  line-height: 1.1;
}
/* The dollar sign inside the field's own border, so the figure a manager types
   is a price rather than a quantity. */
.bs .vhf-bid-field {
  display: flex;
  align-items: baseline;
  gap: 1px;
  padding: 3px 6px;
  border: 1px solid var(--border-hi);
  border-radius: var(--radius);
  background: var(--card-hi);
}
.bs .vhf-bid-field:focus-within {
  border-color: var(--bs-blue);
  box-shadow: inset 0 -2px 0 var(--bs-blue);
}
.bs .vhf-bid-dollar {
  font-family: var(--bs-mono);
  font-size: 11px;
  font-weight: 600;
  color: var(--bs-ink-3);
}
.bs .vhf-bid-use {
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--bs-blue);
  background: transparent;
  border: 1px dashed var(--border-hi);
  border-radius: var(--radius);
  padding: 3px 5px;
  cursor: pointer;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.bs .vhf-bid-use:hover { background: var(--gold-bg); border-style: solid; }
.bs .vhf-bid-use:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 1px; }
.bs .vhf-bid-use[disabled] { opacity: .4; cursor: default; }

/* THE FIELD ITSELF, back on the design system. Every token the original asked
   for was fictional — see the section header. `.bs`-scoped and declared below
   the unscoped original, so this wins on both specificity and order. */
.bs .vhf-prow-bid--ticket {
  width: 3.4rem;
  padding: 0;
  border: none;
  border-radius: 0;
  background: transparent;
  font-family: var(--bs-mono);
  font-size: 15px;
  font-weight: 600;
  color: var(--bs-ink);
  font-variant-numeric: tabular-nums;
}
.bs .vhf-prow-bid--ticket:focus-visible { outline: none; }

/* ── paging ──
   The board pages itself now (IntersectionObserver on this sentinel), so the
   button below is a fallback for anything without one and for the node
   harness, which has no layout engine and therefore no intersections. */
.bs .vhf-board-end { height: 1px; }

@media (max-width: 700px) {
  /* iPad portrait and narrower: the bar wraps to two lines INSIDE its own
     border, which is the point of it being one surface. The bid label is the
     first thing to go — the dollar sign already says what the field is. */
  .bs .vhf-board-bar-search { flex: 1 1 100%; }
  .bs .vhf-bid-lbl { display: none; }
}

/* ══ THE DFS BUILDER, DESIGNED ════════════════════════════════════════════════
   Mark, 2026-09-09, on the most-used new screen: the cap meter "is the hero and
   does not look like it — a 6px bar and a 20px figure. On a settled slate the
   score is 30px, which is right — the cap should carry that weight while the
   slate is live." Plus no images, no projection, no per-slot money, and a
   picker that is a flat list of up to 592 rows.

   WHAT CARRIES THE WEIGHT IS WHAT IS LEFT, not what has been spent. A cap game
   is played down from the ceiling: "$12,400 left" is the number every next
   decision is measured against, and "$37,600 of $50,000" is the same fact
   arranged so a child has to do the subtraction. So the remainder is the hero
   figure and the spend is the caption under it.

   Appended at the end of the file, like the auction section above it, so every
   modifier lands below the rule it overrides. */

.bs .vhf-dfs-cap {
  padding: 12px 14px 13px;
  margin: 12px 0 10px;
  border: 1px solid var(--bs-ink);
  border-radius: var(--radius);
  background: var(--card-hi);
}
.bs .vhf-dfs-cap-kicker {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  font-family: var(--bs-mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
}
.bs .vhf-dfs-cap-fig {
  font-family: var(--bs-mono);
  font-size: 34px;
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: -.01em;
  color: var(--bs-ink);
  font-variant-numeric: tabular-nums;
  margin: 2px 0 1px;
}
.bs .vhf-dfs-cap-sub {
  font-family: var(--bs-mono);
  font-size: 11px;
  color: var(--bs-ink-3);
  font-variant-numeric: tabular-nums;
}
/* OVER THE CAP IS THE PEN, and it is the one state in this arm that earns it:
   an over-cap lineup cannot be saved, so it is an error rather than a strategy.
   Contrast the auction band, where over-committing is the point and the accent
   is deliberately NOT the pen. */
.bs .vhf-dfs-cap.is-over { border-color: var(--bs-pen); }
.bs .vhf-dfs-cap.is-over .vhf-dfs-cap-fig { color: var(--bs-pen); }

/* Twelve pixels rather than six, and ticked at each ninth so the bar reads as
   nine decisions rather than as a percentage. The ticks are a repeating
   gradient rather than nine elements: they are decoration on one box. */
.bs .vhf-dfs-cap-meter {
  position: relative;
  height: 12px;
  margin-top: 9px;
  border: 1px solid var(--bs-rule);
  border-radius: var(--radius);
  background: var(--bs-stock-2);
  overflow: hidden;
}
.bs .vhf-dfs-cap-fill {
  height: 100%;
  background: var(--bs-ink-2);
  transition: width var(--duration-normal, 300ms) ease;
}
.bs .vhf-dfs-cap-ticks {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to right,
    transparent 0,
    transparent calc(100% / 9 - 1px),
    var(--bs-sheet) calc(100% / 9 - 1px),
    var(--bs-sheet) calc(100% / 9));
}
.bs .vhf-dfs-cap.is-over .vhf-dfs-cap-fill { background: var(--bs-pen); }

/* The lineup's projected total. Subordinate to the cap while the slate is live
   — the cap is the constraint being solved and this is the objective — and it
   is the ONLY cross-position points figure on the screen, because a total is a
   sum rather than a comparison. See the note on value in the picker. */
.bs .vhf-dfs-proj {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin: 0 0 10px;
  font-family: var(--bs-mono);
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
}
.bs .vhf-dfs-proj-fig {
  font-size: 17px;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--bs-ink);
  font-variant-numeric: tabular-nums;
  text-transform: none;
}

/* ── the nine, with faces on them ──
   `.vhf-prow` is `32px | 1fr | auto` and the slot label needs a column of its
   own: crammed into the name line it pushed the name out of alignment down
   nine rows, which is the defect `.vhf-dfs-slot`'s min-width was working
   around. Four columns, so the labels, the faces, the names and the money each
   line up as a column. */
.bs .vhf-dfs-row { grid-template-columns: 34px 32px minmax(0, 1fr) auto; }
.bs .vhf-dfs-row .vhf-dfs-slot { min-width: 0; }
.bs .vhf-dfs-slot-cell {
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .06em;
  color: var(--bs-ink-2);
  text-align: left;
}
/* An empty slot is a hole in a lineup and should look like one: the face box
   is dashed and blank rather than a broken image. */
.bs .vhf-dfs-facehole {
  width: 32px; height: 32px;
  border: 1px dashed var(--border-hi);
  border-radius: var(--radius);
  background: var(--bs-stock);
}
/* What is still spendable on THIS slot, on an empty row. Not the whole
   remainder: the cheapest eligible man for each OTHER empty slot is held back,
   the same reserve rule the auction's `max_bid` applies, so a child cannot
   strand themselves with one slot they can no longer fill. */
.bs .vhf-dfs-room {
  font-family: var(--bs-mono);
  font-size: 10.5px;
  color: var(--bs-ink-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.bs .vhf-dfs-room.is-none { color: var(--bs-pen); }

/* ── the picker ──
   Up to 592 rows for a FLEX. Banded by salary, because that is the axis a cap
   game is shopped on — a child with $5,400 wants the $5,000s, not page nine of
   a list sorted by name. */
.bs .vhf-dfs-band {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 5px 9px;
  margin-top: 8px;
  border-top: 1px solid var(--bs-ink);
  border-bottom: 1px solid var(--bs-rule);
  background: var(--bs-stock);
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .08em;
  color: var(--bs-ink-2);
  /* NOT `text-transform: uppercase`, which every other kicker in this file
     carries. The label is "$2,000s" and uppercasing it renders "$2,000S" —
     found in a browser 2026-09-09. The rest of the band is already figures and
     has no case to normalise. */
  font-variant-numeric: tabular-nums;
}
.bs .vhf-dfs-band-n { font-weight: 400; color: var(--bs-ink-3); letter-spacing: .04em; }
/* A band nobody in it can afford. Shown rather than hidden, for the same
   reason a locked player is: a missing band reads as a missing board. */
.bs .vhf-dfs-band.is-dear { color: var(--bs-ink-3); border-top-color: var(--bs-rule); }

/* Value, and it is only ever shown INSIDE the picker. The picker is filtered
   to one slot's eligible positions, so every figure in the list is comparable
   to every other one in it. Across positions it is not: measured on the live
   board 2026-09-09, the median QB returns 3.06 points per $1,000 against the
   median RB's 1.87, and there is exactly ONE QB slot — an absolute value
   column would tell a child to spend the whole cap on cheap quarterbacks.
   That is the same trap `suggested_prices` records for defences, and it is why
   this number does not appear on the lineup rows. */
.bs .vhf-dfs-val {
  font-family: var(--bs-mono);
  font-size: 10.5px;
  color: var(--bs-ink-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* The best value in the eligible pool, marked rather than sorted-for. Reading
   the list by salary and still being shown where the value is is the whole
   lesson of a cap game. */
.bs .vhf-dfs-val.is-best { color: var(--bs-blue); font-weight: 700; }

@media (max-width: 700px) {
  /* iPad portrait: the slot column tightens and the per-slot room figure drops
     to its own line rather than fighting the button for the same track. */
  .bs .vhf-dfs-row { grid-template-columns: 28px 32px minmax(0, 1fr) auto; }
  .bs .vhf-dfs-cap-fig { font-size: 28px; }
}

/* ══ THE DRAFT MONITOR'S EXPANDED DETAIL ══════════════════════════════════════
   Mark, 2026-09-09: it was "built for correctness, not looks — the expanded
   detail is monospace lines with no structure and would read better as two
   columns (placed vs would-win)."

   IT IS A COMPARISON, so it is two columns. What a manager committed and what
   settling right now would actually hand them are the same list only when
   every bid wins, and the whole reason a commissioner opens this row is to see
   where the two come apart. Stacked, that comparison is a scroll and a memory
   test; side by side it is a glance.

   ONE COLUMN ON A NARROW SCREEN. Two columns of tabular figures below about
   620px start wrapping player names, and a wrapped name in a monospace column
   destroys the alignment that is the only reason to set it in monospace. */
.bs .vhf-mon-cols {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}
.bs .vhf-mon-col { min-width: 0; }
.bs .vhf-mon-row {
  display: grid;
  grid-template-columns: 3.6rem minmax(0, 1fr) 2.2rem;
  gap: 6px;
  font-family: var(--bs-mono);
  font-size: 11.5px;
  line-height: 1.6;
  font-variant-numeric: tabular-nums;
}
.bs .vhf-mon-amt { text-align: right; color: var(--bs-ink); font-weight: 600; }
.bs .vhf-mon-who {
  color: var(--bs-ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bs .vhf-mon-pos { color: var(--bs-ink-3); font-size: 10px; }
/* A BID THAT WOULD LOSE, struck through. The commissioner's question watching a
   settlement is not "what did they bid" but "which of these are about to
   evaporate" — and over-bidding is the strategy this league tells students to
   use, so most bids losing is success rather than a warning. Hence the ink's
   own muted step and not the pen. */
.bs .vhf-mon-row.is-lost .vhf-mon-amt,
.bs .vhf-mon-row.is-lost .vhf-mon-who {
  color: var(--bs-ink-3);
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}
.bs .vhf-mon-tally {
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: .04em;
  text-transform: none;
  color: var(--bs-ink-3);
}
/* The position count, under both columns rather than inside either: it is a
   fact about the roster the settlement produces, not about one of the lists. */
.bs .vhf-mon-foot {
  margin-top: 9px;
  padding-top: 7px;
  border-top: 1px solid var(--bs-rule);
  font-family: var(--bs-mono);
  font-size: 11px;
  color: var(--bs-ink-2);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 620px) {
  .bs .vhf-mon-cols { grid-template-columns: 1fr; }
}

/* The row whose detail is open. BELOW `.vhf-prow.is-clickable:hover`, which is
   declared far above in the phase-1 block and is (0,3,0) against this rule's
   (0,3,0) — so source order is what makes the open state survive the pointer
   moving away, and an iPad has no pointer to move. */
.bs .vhf-prow.is-open {
  background: var(--gold-bg);
  box-shadow: inset 2px 0 0 var(--bs-blue);
}

/* ══ INJURED RESERVE ══════════════════════════════════════════════════════════
   Mark, 2026-09-09: "It is a list under a list."

   IT WAS, LITERALLY: a `.vhf-prow-list` under a `.vhf-prow-list`, told apart
   only by the words above them. And a list is the wrong object anyway. IR is
   TWO SPOTS — Mark's promise in writing is "2 IR spots" — so what a manager
   needs to see is two slots, filled or empty, not a list that happens to be
   short today. Slots say "you have two of these" without a sentence; a list of
   one says nothing about the second at all.

   Same idiom as the DFS lineup's nine slots, which is the other place in this
   arm where a fixed number of holes is the whole shape of the thing.

   ONE COLUMN BELOW 620px, because two slots side by side each holding a name,
   a position and an Activate button is about 300px apiece. */
.bs .vhf-ir-slots {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-top: 8px;
}
.bs .vhf-ir-slot {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 9px 10px;
  border: 1px solid var(--bs-rule);
  border-radius: var(--radius);
  background: var(--bs-sheet);
  min-width: 0;
}
/* An empty spot is drawn as a spot rather than as nothing: dashed, quiet, and
   still obviously one of the two. The same reading the DFS builder's face-hole
   takes — an unfilled thing should look unfilled, not absent. */
.bs .vhf-ir-slot.is-empty {
  border-style: dashed;
  border-color: var(--border-hi);
  background: transparent;
}
.bs .vhf-ir-n {
  font-family: var(--bs-mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .08em;
  color: var(--bs-ink-3);
  white-space: nowrap;
}
.bs .vhf-ir-body { min-width: 0; flex: 1; }
.bs .vhf-ir-nm {
  font-family: var(--bs-ed);
  font-weight: 600;
  font-size: 14px;
  line-height: 1.25;
  color: var(--bs-ink);
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bs .vhf-ir-sub {
  font-family: var(--bs-mono);
  font-size: 9.5px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
}
.bs .vhf-ir-slot.is-empty .vhf-ir-nm {
  font-style: italic;
  font-weight: 400;
  color: var(--bs-ink-3);
}

@media (max-width: 620px) {
  .bs .vhf-ir-slots { grid-template-columns: 1fr; }
}

/* A LOGO IS FITTED DIFFERENTLY FROM A FACE. `.vhf-prow-img` is `cover` with
   `object-position: top center`, which is right for a photograph — it crops to
   the head. A team badge cropped to its top edge loses the bottom of the mark,
   so it is contained and shown whole, on the sheet rather than the recessed
   stock because most of these marks have transparent grounds. Below the base
   rule, which is what makes it win at equal specificity. */
.bs .vhf-prow-img--logo {
  object-fit: contain;
  object-position: center;
  background: var(--card-hi);
  padding: 2px;
}

/* ══ THE POSTSEASON BRACKET ═══════════════════════════════════════════════════
   Weeks 11-16, double elimination, everybody in. `src/fantasy/bracket.py` has
   the rules; this is what fourteen children read them off.

   NOT A TREE DRAWN WITH LINES. The obvious rendering of a bracket is a fanned
   diagram of connectors, and it is the wrong one here for two reasons: this is
   DOUBLE elimination, so teams cross between two trees and the lines would
   have to leave one and enter the other; and it has to be legible on a 744px
   iPad in portrait, where a six-round tree is either unreadable or sideways.

   SO IT IS TWO COLUMNS OF STANDING, plus this week's fixtures. That is what a
   student actually wants to know — am I still alive, how many lives have I
   got, and who do I play — and it is the same shape the monitor's expanded row
   settled on for the same reason. */
.bs .vhf-brk-lede {
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--bs-ink-2);
  max-width: 64ch;
  margin: 0 0 10px;
}
.bs .vhf-brk-cols {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
  margin-top: 8px;
}
.bs .vhf-brk-col { min-width: 0; }
/* The two sides are not equals and should not look it. Upstairs still has a
   life in hand; downstairs is playing to survive, which is the whole cost of
   finishing eighth. */
.bs .vhf-brk-col--up { border-top: 2px solid var(--bs-ink); padding-top: 7px; }
.bs .vhf-brk-col--down { border-top: 1px solid var(--bs-rule); padding-top: 8px; }
.bs .vhf-brk-kicker {
  font-family: var(--bs-mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  margin-bottom: 5px;
}
.bs .vhf-brk-kicker b { color: var(--bs-ink); font-weight: 700; }

.bs .vhf-brk-team {
  display: grid;
  grid-template-columns: 2.1rem minmax(0, 1fr);
  gap: 7px;
  align-items: baseline;
  padding: 4px 0;
  border-bottom: 1px solid var(--bs-stock-2);
}
.bs .vhf-brk-team:last-child { border-bottom: none; }
.bs .vhf-brk-seed {
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--bs-ink-3);
  font-variant-numeric: tabular-nums;
}
.bs .vhf-brk-name {
  font-family: var(--bs-ed);
  font-weight: 600;
  font-size: 13.5px;
  line-height: 1.3;
  color: var(--bs-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A team that is out. Struck rather than removed, and the seed stays: a
   bracket a child cannot find themselves in is a bracket that has lost them. */
.bs .vhf-brk-team.is-out .vhf-brk-name {
  color: var(--bs-ink-3);
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}
/* The reader's own team, marked the same way the standings table marks it. */
.bs .vhf-brk-team.is-mine { box-shadow: inset 2px 0 0 var(--bs-blue); padding-left: 6px; }

/* This week's fixtures. `_pair` is deterministic, so these are the actual
   games rather than a guess at them. */
.bs .vhf-brk-games { margin-top: 12px; }
.bs .vhf-brk-game {
  display: grid;
  grid-template-columns: 4.2rem minmax(0, 1fr);
  gap: 8px;
  align-items: baseline;
  padding: 5px 0;
  border-bottom: 1px solid var(--bs-rule);
  font-size: 13px;
}
.bs .vhf-brk-game:last-child { border-bottom: none; }
.bs .vhf-brk-where {
  font-family: var(--bs-mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
}
.bs .vhf-brk-vs { color: var(--bs-ink-3); font-size: 11px; }
/* A BYE IS NOT A GAME, and it reads as the reward it is rather than as a
   fixture with a missing opponent. Seeds 1 and 2 sit out the opening round. */
.bs .vhf-brk-game.is-bye .vhf-brk-where { color: var(--bs-blue); }

/* The champion. The one figure on this screen that earns real size — by week
   16 it is the only thing anybody is looking for. */
.bs .vhf-brk-champ {
  margin: 4px 0 10px;
  padding: 11px 13px;
  border: 1px solid var(--bs-blue);
  border-radius: var(--radius);
  background: var(--gold-bg);
}
.bs .vhf-brk-champ-k {
  font-family: var(--bs-mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--bs-blue);
}
.bs .vhf-brk-champ-n {
  font-family: var(--bs-ed);
  font-size: 24px;
  font-weight: 700;
  line-height: 1.15;
  color: var(--bs-ink);
  margin-top: 2px;
}

@media (max-width: 620px) {
  .bs .vhf-brk-cols { grid-template-columns: 1fr; }
}
