/* ══ THE VERTICAL HOMEPAGES ═════════════════════════════════════════════════
   One screen, four compositions (js/11b-verticals.js). Numbered 19 so it lands
   last; the NN- prefix is the cascade.

   THE HIERARCHY IS THE POINT. Three ranks of card, at three deliberately
   different weights, so the page answers "what do I do" before it answers "what
   else is there":

     route   the vertical's own destinations — a section, an exam, a bank.
             Largest, ruled, numbered by scale rather than by ornament.
     area    practice by content area. Half the weight, four or six up.
     subset  the plan, the record, the leaderboard. Agate, one line each —
             these are the pages Mark said are SUBSETS of a homepage, and their
             weight on the page has to say so.

   Uniform tiles would make all three read as one undifferentiated menu, which is
   what the old front page did and why it was retired.
   ══════════════════════════════════════════════════════════════════════════ */

/* NO inner scroller here. `overflow-y: auto` on this element made it its own
   scroll container inside a `height: 100%` screen, which capped the visible page
   at ~730px: the rounds, the content areas and the subsets were all rendered and
   all below the fold of a box nobody knew was scrolling. Every other page screen
   (.training-body, .coach-body, .lb-body) lets the document scroll instead. */
.bs .vh-row { display: flex; }
.bs .vh-wrap {
  width: 100%;
  max-width: var(--page-max);
  margin-inline: auto;
  /* padding-top was 30px. .page-head owns the whole distance from the running
     head now (02-chrome.css, THE PAGE HEAD part 2); leaving it here would make
     this screen's gap 30 + --mast-gap, which is exactly how nine screens ended
     up with nine different gaps. */
  padding: 0 var(--page-gutter) 72px;
  min-width: 0;
}

/* ── the head ──
   The type, the gap, the measure and the bottom margin are .page-head's now
   (02-chrome.css). This block held a fifth private ramp — clamp(30px, 4.4vw,
   46px) — and the landing is a FRONT PAGE, so it takes the 56px ceiling via
   .page-head--front rather than a ramp of its own. */

/* ══ BAND 1 · THE JUMP START ═════════════════════════════════════════════════
   docs/ARCHITECTURE-2026-08-07.md §3. The markup is variant b's, unchanged, and
   it carries `dvx dx-b` so all of 21-dashboard-variants.css applies to it as
   written — that file's own page-wrapper declarations were dropped when the
   dashboard page died, so nothing has to be overridden back off here.

   b was a whole page and its head opened with page-top air. Here it opens under
   the title and the lede, which have already set the top of the page. */
.bs .vh-jump .dx-b-head { padding-top: 0; }

/* The way into every modal — three KPI TILES now, not a ruled row of thin
   links. Walk dictation, 2026-08-09 PM: "if it's a modal, make it a KPI...
   doesn't have the same information bloated on the actual page", and, of
   the numbers opener specifically, "make it a fucking KPI that you click."
   `.vh-open-numberss` (a typo'd, unreferenced class from an earlier pass)
   is dropped along with the thin-row styling it belonged to. */
/* Direction C: "a three-box row at 2fr 1fr 1fr where the FIRST box is
   filled dark green... and the other two are plain boxes. This weighting
   is the point — the page must have a lead." A fixed 2fr/1fr/1fr, not the
   old auto-fit — auto-fit gave the three openers equal weight, which is the
   exact "same weight" defect every one of Mark's four directions exists to
   fix. Collapses to one column below 700, where three unequal columns would
   have squeezed the lead box down to the same width as the other two
   anyway. */
.bs .vh-openers {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr;
  gap: 14px;
  margin: 0;
}
/* A grid item's default `min-width: auto` refuses to shrink below its own
   min-content width, so an `fr` track is only a fraction until the content
   says otherwise — then the tracks quietly outgrow the container.
   MEASURED on home_ap_world at 768: the parent is 712px and the three tracks
   resolved to 303.2 + 186.6 + 231.3 = 721.1, which with two 14px gaps is 749
   inside 712. Starting at the 28px gutter that puts the right edge at 777 —
   the 9px overflow the walk has been reporting, and the 1px it reports at 320.
   Only ap_world trips it because only ap_world's KPI line is long enough; the
   other eleven exams are the same layout one word short of showing it.
   min-width: 0 lets `1fr` mean 1fr again. */
.bs .vh-openers > *,
.bs .vh-openers--four > * { min-width: 0; }

/* The three-track row had NO breakpoints of its own — only `--four` did — so at
   768 it stayed `2fr 1fr 1fr` and resolved to 342/171/171. min-width: 0 above
   stopped it overflowing, but 171px is still too narrow for a KPI carrying a
   sentence and a figure, which is what UI-STABILIZATION-2026-08-13 §3 is about.
   Two columns from 701-1000 with the lead spanning both keeps Direction C's
   weighting ("the page must have a lead") while giving each box a usable
   measure; one column at 700 and below, span reset so it cannot leave a hole. */
@media (max-width: 1000px) {
  .bs .vh-openers { grid-template-columns: 1fr 1fr; }
  .bs .vh-openers > .vh-open-lead { grid-column: 1 / -1; }
}
@media (max-width: 700px) {
  .bs .vh-openers { grid-template-columns: 1fr; }
  .bs .vh-openers > .vh-open-lead { grid-column: auto; }
}
/* The fourth box — Cognition's Daily puzzles, AP's Random / Start practice.
   `2fr 1fr 1fr 1fr`, not four equal tracks: the lead keeps its double width
   because "this weighting is the point — the page must have a lead" is
   Direction C's own sentence, and four equal boxes would be the same
   "everything at one weight" defect the weighting exists to fix. See
   _vhOpenersBand for why these three boxes moved up here out of bands of
   their own. */
.bs .vh-openers--four { grid-template-columns: 2fr 1fr 1fr 1fr; }
@media (max-width: 1000px) {
  /* Four tracks at 1000px would put the three 1fr boxes near 150px, which is
     narrower than "Wordle · Sudoku · The Chess Column" or a 26px figure and
     its unit can sit in. Two-up keeps the lead's weight on the first row. */
  .bs .vh-openers--four { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 700px) {
  .bs .vh-openers,
  .bs .vh-openers--four { grid-template-columns: 1fr; }
}
/* THE FIFTH BOX IS GONE. `.vh-openers--five` was fantasy's band until DFS made
   that arm six, and nothing in js/ has rendered it since — the only openers
   modifiers in use are `--two`, `--four` (11b-verticals.js) and `--six`
   (11e-fantasy.js). Removed 2026-09-09 rather than left to rot: it carried the
   same two-up-at-1100 breakpoint the `--six` rule has now been proved wrong
   about, so keeping it around was keeping a template for the defect.
   test_no_openers_band_declares_more_tracks_than_the_page_can_hold reads which
   modifiers are actually rendered, so it would come straight back into scope
   if anyone rendered it again. */

/* The sixth box — DFS, which like Pickems takes no league. Six tracks is where
   the lead-plus-rest row stops working, and `--four`'s comment already calls
   150px too narrow for a box carrying a word.

   THREE-UP UNCONDITIONALLY, and the six-track rule is GONE rather than
   breakpointed. It used to be `2fr 1fr 1fr 1fr 1fr 1fr` above 1300px, on the
   reasoning that "every iPad size lands there". Two things were wrong with
   that, both measured in a browser on 2026-09-09:

     * 1366 IS AN iPAD SIZE. iPad Pro in landscape is 1366 wide, above the
       breakpoint, so it got the six-track layout and its five small tiles
       resolved to 136px each — the exact defect the breakpoint existed to
       avoid, on a real device these students hold.

     * AND THE LAYOUT NEVER FITS ANYWHERE. `.vh-row` caps the page at
       max-width: 1080px, so this band is at most 1024px wide at ANY viewport.
       Measured at 1301, 1400, 1600, 1920 and 2560: the band is 1024px every
       time and the small tiles are 136px every time. Six tracks had no width
       at which it worked, so there was nothing to keep above a breakpoint.

   Three-up in 1024px gives 332px tiles, and 220px at 744 portrait. */
.bs .vh-openers--six {
  grid-template-columns: 1fr 1fr 1fr;
}
.bs .vh-openers--six > * { min-width: 0; }
.bs .vh-openers--six > .vh-open-lead { grid-column: 1 / -1; }
@media (max-width: 700px) {
  .bs .vh-openers--six { grid-template-columns: 1fr; }
}
/* Mark, 2026-08-10, on the rebuilt page: "it still feels that we are not
   making best use of space in the rectangle modals they are a bit awkward."
   The shape he was looking at was a kicker and a figure pinned to the two
   TOP corners and a lone `⌄` pinned to the bottom-left — every scrap of
   content on the perimeter and the whole middle empty, in a box the 2fr
   track makes ~500px wide. Three elements, three corners, no interior.
   So the box is now a KPI proper: kicker on top, then ONE baseline row
   carrying a real sentence on the left and the number on the right. The
   sentence is what fills the width the grid hands out — a wide box gets a
   long line ("Focus: Problem-Solving and Data Analysis"), a 1fr box gets a
   short one — so the box stops being sized independently of what is in it.
   The chevron is gone rather than repositioned: it was the only thing
   saying "clickable", and the hover/press letterpress below says it better
   without spending a row on it. */
.bs .vh-open-numbers {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
  width: 100%;
  background: var(--bs-sheet);
  border: 1px solid var(--bs-ink);
  border-radius: 0;
  padding: 14px 16px 15px;
  cursor: pointer;
  text-align: left;
  transition: box-shadow .12s ease, transform .12s ease, background .12s ease;
}
.bs .vh-open-numbers:hover { background: var(--bs-stock-2); box-shadow: 3px 3px 0 var(--bs-ink); }
.bs .vh-open-numbers:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--bs-ink); }
.bs .vh-open-numbers:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 2px; }
/* The lead box — Direction C's own colours, straight off 01-tokens.css:
   `--bs-ink` (#1b3a2a) fill, `--bs-camel-on-ink` (the one licensed way to
   set camel as TYPE — see that token's own note) for the label and chevron,
   `--bs-sheet` for the figure. The press shadow goes bronze rather than ink
   on this one box alone — an ink-on-ink shadow would barely register
   against its own dark fill, and gold reads as "the stamped one" exactly
   the way the lead is supposed to. */
.bs .vh-open-numbers.vh-open-lead {
  background: var(--bs-ink);
  border-color: var(--bs-ink);
}
.bs .vh-open-numbers.vh-open-lead:hover {
  background: var(--bs-ink-2);
  box-shadow: 3px 3px 0 var(--bs-camel-deep);
}
.bs .vh-open-numbers.vh-open-lead:active {
  box-shadow: 1px 1px 0 var(--bs-camel-deep);
}
.bs .vh-open-numbers.vh-open-lead .vh-open-lbl { color: var(--bs-camel-on-ink); }
.bs .vh-open-numbers.vh-open-lead .vh-open-fig,
.bs .vh-open-numbers.vh-open-lead .vh-open-line { color: var(--bs-sheet); }
/* --bs-stock-2 rather than --bs-ink-3 for the two quiet lines: on the dark
   fill the graphite greys invert into near-invisibility (ink-3 on ink is
   under 2:1), where the pale stock reads ~8.8:1 and still sits back from
   the sheet-white lead line beside it. */
.bs .vh-open-numbers.vh-open-lead .vh-open-fig-sub,
.bs .vh-open-numbers.vh-open-lead .vh-open-sub { color: var(--bs-stock-2); }
/* The one row that does the work: sentence left, number right, aligned on
   their BOTTOM edge rather than a shared baseline — the left side is two
   lines of different sizes and the right side is a 26px figure, so a
   baseline would hang the number off the lead line and leave the sub
   dangling below it. `min-width: 0` on the text column is what lets a long
   focus label wrap instead of shoving the figure off the right edge. */
.bs .vh-open-body {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  width: 100%;
  gap: 16px;
  /* Push the body to the FLOOR of the box. Grid rows stretch all three
     openers to the tallest of them, so the box with the least to say ends up
     with slack no matter what — the only question is where it goes. Pinned
     to the bottom it becomes Direction C's own diagonal (`.cur .box`,
     `justify-content: space-between`): kicker top-left, number bottom-right,
     the eye crossing the box instead of skirting an empty middle. Left
     floating under the kicker it was just the old dead space with better
     content in it. */
  margin-top: auto;
}
.bs .vh-open-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
/* `vh-open-line`, not `vh-open-lead` — `.vh-open-lead` is already taken, by
   the dark-fill modifier on the FIRST box (Direction C's "the page must have
   a lead"). Two different meanings of the same word, one class name. */
.bs .vh-open-line {
  font-family: var(--bs-ed);
  font-variation-settings: 'WONK' 1;
  font-weight: 900;
  /* 17, not 19. The 1fr boxes are ~242px wide and the figure block claims
     ~72px of that, so the line gets ~120px to work in; two more points of
     size is the difference between a two-line phrase and a four-line stack. */
  font-size: 17px;
  line-height: 1.2;
  letter-spacing: -.015em;
  color: var(--bs-ink);
}
.bs .vh-open-sub {
  font-family: var(--bs-ui);
  font-size: 12.5px;
  line-height: 1.35;
  color: var(--bs-ink-2);
}
.bs .vh-open-lbl {
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--bs-ink);
}
/* The KPI figure itself — the live number Mark named. Optional: an opener
   with nothing natural to show (Today's training, Training plan) renders
   without one rather than fabricating a count. */
.bs .vh-open-fig {
  font-family: var(--bs-ed);
  font-variation-settings: 'WONK' 1;
  font-weight: 900;
  font-size: 26px;
  line-height: 1;
  letter-spacing: -.02em;
  color: var(--bs-blue);
  font-variant-numeric: tabular-nums;
}
/* The second number AP's review opener carries alongside the first — Mark,
   this session: "questions answered and the AP score... composite and
   within unit." (js/11b-verticals.js's _vhHeadlineFigure 'ap' branch).
   Wraps the figure so a two-line KPI still right-aligns against the label,
   the same edge every other arm's bare-number figure already sits on. */
.bs .vh-open-fig-wrap { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }
.bs .vh-open-fig-sub {
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .06em;
  color: var(--bs-ink-3);
  text-align: right;
  font-variant-numeric: tabular-nums;
  /* The unit never wraps. "drill sessions" breaking across two lines made
     the Review opener the tallest box in the row, and the grid then
     stretched the other two to match — re-opening the exact dead space this
     pass exists to close, from the one element small enough to be overlooked. */
  white-space: nowrap;
}

/* ── nested ways-to-practise: one level at a time, with a way back up ─────
   Mark: "not a wall of buttons at the top level." `.vh-ways` is a bare
   wrapper `_vhPaintSatWays` (js/11b-verticals.js) clears and rebuilds on
   every click rather than appending a new section beside the old one. */
.bs .vh-back {
  display: inline-block;
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0 0 12px;
  cursor: pointer;
}
.bs .vh-back:hover { color: var(--bs-blue); }
.bs .vh-back:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 3px; }

/* ── the student-facing trap tab (numbers/week/traps dialog) ──────────────
   Mark: "know what traps you fall for." Own vocabulary rather than reusing
   the coach screen's `.dvp-*` (js/06-dashboard.js), which is coach-only. */
.vh-trap-list { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.vh-trap-row { display: grid; grid-template-columns: 140px 1fr 44px; align-items: center; gap: 10px; }
.vh-trap-name {
  font-family: var(--bs-ui);
  font-size: 12.5px;
  font-weight: 600;
  color: var(--bs-ink-2);
  text-transform: capitalize;
}
.vh-trap-bar { height: 8px; background: var(--bs-stock-2); border: 1px solid var(--bs-rule); }
.vh-trap-bar-fill { height: 100%; background: var(--bs-camel-deep); }
.vh-trap-count {
  font-family: var(--bs-mono);
  font-size: 11.5px;
  color: var(--bs-ink-3);
  text-align: right;
}
@media (max-width: 480px) {
  .vh-trap-row { grid-template-columns: 1fr 44px; }
  .vh-trap-bar { grid-column: 1 / -1; }
}

/* ── sections ──
   VERTICAL RHYTHM. MEASURED at 1024x768, the gaps stacked above the openers
   were 30 (head to band), 38 (band to section) and 39 (the kicker block) —
   three unrelated numbers rather than a scale, and 107px of pure air between
   the head and the one thing the page wants a student to do. They are a
   system now: 12 inside a labelled block, 24 between blocks, 36 from the
   running head. Nothing here gets smaller than the type needs; what goes is
   the arbitrariness. Arm homepages only — `.vh-*` is this file's own
   namespace and js/11b-verticals.js is its only builder. */
.bs .vh-section { margin-top: 24px; }
.bs .vh-kicker {
  font-family: var(--bs-mono);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  border-bottom: 1px solid var(--bs-rule);
  padding-bottom: 7px;
  margin-bottom: 12px;
}
.bs .vh-empty {
  font-family: var(--bs-ui);
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--bs-ink-2);
  max-width: 58ch;
  border-left: 2px solid var(--bs-rule);
  padding-left: 14px;
}
/* The inline retry a failed homepage render leaves behind (_vhRenderFailed,
   js/11b-verticals.js). Sits under the .vh-empty sentence, so it takes the
   same left inset rather than starting at the section edge and reading as a
   second, unrelated control. */
.bs .vh-retry {
  display: inline-block;
  margin: 12px 0 0 14px;
  padding: 7px 15px 6px;
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--bs-ink);
  background: var(--bs-stock-2);
  border: 1px solid var(--bs-ink);
  border-radius: 0;
  cursor: pointer;
  transition: background .12s ease, color .12s ease;
}
.bs .vh-retry:hover { background: var(--bs-ink); color: var(--bs-sheet); }
.bs .vh-retry:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 2px; }

/* Still used by _vhApUntaggedWays' own lede (js/11b-verticals.js) — kept
   when the profile band's own use of it (the deleted strip+ladder's intro
   line) went away with that band. */
.bs .vh-detail-lede {
  font-family: var(--bs-ui);
  font-size: 13px;
  line-height: 1.5;
  color: var(--bs-ink-2);
  max-width: 58ch;
  margin: -6px 0 14px;
}

/* ── cards, at three weights ── */
.bs .vh-grid { display: grid; gap: 14px; }
.bs .vh-card {
  display: flex;
  flex-direction: column;
  gap: 5px;
  text-align: left;
  background: var(--bs-sheet);
  border: 1px solid var(--bs-ink);
  border-radius: 0;
  cursor: pointer;
  transition: box-shadow .12s ease, transform .12s ease, background .12s ease;
}
/* The letterpress offset appears ON PRESS, not at rest — at rest on every card
   it is decoration, and the whole grid lifts off the page. */
.bs .vh-card:hover { background: var(--bs-stock-2); box-shadow: 3px 3px 0 var(--bs-ink); }
.bs .vh-card:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--bs-ink); }
.bs .vh-card:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 2px; }

.bs .vh-card-name {
  font-family: var(--bs-ed);
  font-variation-settings: 'WONK' 1;
  font-weight: 900;
  letter-spacing: -.015em;
  color: var(--bs-ink);
}
.bs .vh-card-sub {
  font-family: var(--bs-ui);
  color: var(--bs-ink-2);
  line-height: 1.4;
}
.bs .vh-card-fig {
  font-family: var(--bs-mono);
  font-size: 9.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  margin-top: auto;
  padding-top: 10px;
  border-top: 1px solid var(--bs-rule);
}

/* route — the vertical's own destinations.
   auto-FILL, not auto-fit. The difference is the whole of Mark's "not making
   best use of space in the rectangle" complaint on the ways-band: auto-fit
   COLLAPSES the empty tracks and stretches whatever is left across all of
   them, so the same card that is a well-proportioned ~240px on the AP gate
   (twelve subjects, four tracks full) became a ~500px slab on SAT and
   Competition, which have two. Identical markup, identical content, twice
   the width, and the extra 260px was pure air beside a one-line subtitle.
   auto-fill keeps the empty tracks, so a card is the same size whether the
   arm offers two ways in or twelve, and a two-card band reads as two things
   rather than as a grid that failed to fill. The leftover track width goes
   back to the page, which is dotted stock — not a hole. */
.bs .vh-grid-route { grid-template-columns: repeat(auto-fill, minmax(216px, 1fr)); }
.bs .vh-grid-route .vh-card { padding: 18px 20px 16px; min-height: 132px; }
.bs .vh-grid-route .vh-card-name { font-size: 23px; }
.bs .vh-grid-route .vh-card-sub { font-size: 13px; }
/* The bank whose rounds are showing below. A left rule rather than a fill: the
   card is still a button, it is just the one in play. */
.bs .vh-grid-route .vh-card.is-current {
  border-left: 5px solid var(--bs-bronze);
  background: var(--bs-stock-2);
}

/* area — practice by content area, half the weight.
   210px, not 178: the page column is ~1010px and 178 fitted four across, which
   split the six competition areas 4 + 2 and left an orphan pair. 210 gives three,
   so six areas read as 3 + 3 and four SAT domains as 3 + 1. */
.bs .vh-grid-area { grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 10px; }
.bs .vh-grid-area .vh-card { padding: 13px 15px; border-color: var(--bs-ink-3); }
.bs .vh-grid-area .vh-card-name { font-size: 15.5px; font-weight: 900; }
.bs .vh-grid-area .vh-card-sub { font-size: 11.5px; }

/* subset — agate. These are pages INSIDE the vertical, and the weight says it. */
/* auto-FIT here, unlike route and area above, and deliberately so: a subset
   card is a full-bleed agate ROW whose top border is the rule that separates
   it from the page. Under auto-fill a lone "Leaderboard" sat in the first of
   six tracks and its rule stopped a sixth of the way across — a hairline to
   nowhere. These want to stretch; the cards above want not to. */
.bs .vh-grid-subset { grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); gap: 0; }

/* ── one item is a ROW, not a one-track grid ──
   Mark, 2026-08-11: "it looks awkward when there is a single modal button or
   some other element hanging in a row by itself, it looks unbalanced." Three
   bands render exactly one card, and in an auto-fill four-track grid that
   card sat in the first track with three empty ones beside it.

   This page has already swung between the two opposite failures, so the
   shape matters more than the width. `auto-fit` collapsed the empty tracks
   and stretched the card to ~500px while it was still laid out as a tall
   vertical stack — name, sub and figure each on their own line, all the
   content around the edge and the middle empty. That was the "not making
   best use of space in the rectangle... a bit awkward" complaint. So a solo
   card is not merely widened, it is RE-LAID-OUT: name over sub on the left,
   figure hard right, one shallow band whose width is filled by content.
   Applied by _vhGrid whenever a non-subset band has exactly one item, so a
   band that drops to one in future cannot regress to the lone-card shape. */
.bs .vh-grid--solo { grid-template-columns: 1fr; }
/* Two items are half a row each, not two of four tracks. Measured before
   changing it: SAT and Competition both laid out four 245.5px tracks and put
   cards in two of them, leaving 503px of dead track at 1440 — the same
   unbalanced shape as the lone card, two-thirds as bad. The horizontal
   re-layout below is what keeps this from being the ~500px slab: the width is
   filled by a name, a subtitle and a right-hand terminal rather than by
   padding. */
.bs .vh-grid--pair { grid-template-columns: 1fr 1fr; }
@media (max-width: 700px) {
  .bs .vh-grid--pair { grid-template-columns: 1fr; }
}
.bs .vh-grid--solo .vh-card,
.bs .vh-grid--pair .vh-card {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: 'name fig' 'sub fig';
  align-items: center;
  column-gap: 24px;
  row-gap: 2px;
}
.bs .vh-grid--solo .vh-card-name,
.bs .vh-grid--pair .vh-card-name { grid-area: name; align-self: end; }
.bs .vh-grid--solo .vh-card-sub,
.bs .vh-grid--pair .vh-card-sub { grid-area: sub; align-self: start; margin: 0; }
/* The figure stops being a block under the text and becomes the bar's
   right-hand terminal, on the same optical line as the name. The rule above
   it goes: it was a divider between stacked blocks in a tall card, and in a
   bar it reads as a stray 100px dash floating over the number. */
.bs .vh-grid--solo .vh-card-fig,
.bs .vh-grid--pair .vh-card-fig {
  grid-area: fig;
  align-self: center;
  margin: 0;
  padding-top: 0;
  border-top: 0;
  text-align: right;
}
/* `min-height: 132px` is a CARD's proportion — it exists so a grid of route
   cards has an even floor. A bar has no neighbours to line up with, and
   holding it at 132px is what turns "fills its row" back into the ~500px
   slab Mark objected to, just wider. It sizes to its content instead. */
.bs .vh-grid--solo .vh-card,
.bs .vh-grid--pair .vh-card,
.bs .vh-grid-route.vh-grid--solo .vh-card,
.bs .vh-grid-route.vh-grid--pair .vh-card { min-height: 0; padding: 14px 20px; }

/* The bar's right-hand terminal where there is no figure to be one. Sized and
   coloured like the card's own subtitle rather than like a button: it says
   "this opens the next level", which is what these forks do, and it must not
   compete with the name for the eye. */
.bs .vh-grid--solo .vh-card-go,
.bs .vh-grid--pair .vh-card-go {
  grid-area: fig;
  align-self: center;
  justify-self: end;
  font-family: var(--bs-mono);
  font-size: 15px;
  color: var(--bs-ink-3);
  transition: transform .12s ease, color .12s ease;
}
.bs .vh-grid--solo .vh-card:hover .vh-card-go,
.bs .vh-grid--pair .vh-card:hover .vh-card-go {
  color: var(--bs-blue);
  transform: translateX(3px);
}
.bs .vh-grid-subset .vh-card {
  padding: 11px 2px;
  background: transparent;
  border: 0;
  border-top: 1px solid var(--bs-rule);
  gap: 2px;
}
.bs .vh-grid-subset .vh-card:hover { background: transparent; box-shadow: none; }
.bs .vh-grid-subset .vh-card:hover .vh-card-name { color: var(--bs-bronze); }
.bs .vh-grid-subset .vh-card:active { transform: none; box-shadow: none; }
.bs .vh-grid-subset .vh-card-name {
  font-family: var(--bs-ui);
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 0;
}
.bs .vh-grid-subset .vh-card-name::after { content: ' →'; color: var(--bs-ink-3); }
.bs .vh-grid-subset .vh-card-sub { font-size: 11.5px; }

/* ── the four rounds ──
   Sprint / Target / Team / Countdown. Four across on a wide window because they
   are a set the student compares before choosing, not a list they read down. */
/* Four across, and the minmax is set so that it IS four: at 196px the page
   column fitted three and dropped Countdown onto a row by itself, which is the
   same orphaned-tile problem the ballot grid had with a fifth choice. */
.bs .vh-rounds {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(164px, 1fr));
  gap: 14px;
}
.bs .vh-round {
  display: flex;
  flex-direction: column;
  gap: 6px;
  text-align: left;
  background: var(--bs-sheet);
  border: 2px solid var(--bs-ink);
  border-radius: 0;
  padding: 16px 18px 14px;
  cursor: pointer;
  transition: box-shadow .12s ease, transform .12s ease;
}
.bs .vh-round:hover { box-shadow: 4px 4px 0 var(--bs-ink); }
.bs .vh-round:active { transform: translate(2px, 2px); box-shadow: 2px 2px 0 var(--bs-ink); }
.bs .vh-round:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 2px; }
.bs .vh-round-name {
  font-family: var(--bs-ed);
  font-variation-settings: 'WONK' 1;
  font-weight: 900;
  font-size: 25px;
  line-height: 1;
  letter-spacing: -.02em;
  color: var(--bs-ink);
}
.bs .vh-round-meta {
  font-family: var(--bs-ui);
  font-size: 12.5px;
  color: var(--bs-ink-2);
}
/* The calculator rule, carried on the card. Bronze for permitted and plain ink
   for not: the distinction is carried by the WORD, with colour supporting it —
   never by colour alone (see the --amber note in 01-tokens.css). */
.bs .vh-round-calc {
  font-family: var(--bs-mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  border-top: 1px solid var(--bs-rule);
  padding-top: 9px;
  margin-top: auto;
}
.bs .vh-round-calc.is-yes { color: var(--bs-bronze); }

/* ══ THE ANCHOR PANEL — radar + standings table, Direction C ════════════════
   Replaces BAND 2's old profile strip + ladder (git history: the strip
   itself replaced a cognition-only radar on 2026-08-10 — e315766b — over a
   constraint, "one graphic that works at five dimensions and at fifteen,"
   that the "one radar per SECTION" rule below has since retired). Direction
   C, Mark's approved pick: "move it up and let it anchor the page, with the
   domains as a standings table beside it." Now the FIRST thing under the
   masthead on every arm (js/11b-verticals.js's `_vhProfileBand`, called
   ahead of the openers on all four render functions), not a band buried
   under the ways-to-practise grid the way the strip was. */
/* gap: 0, not 24px. The gap existed to separate two STACKED anchor panels,
   and after the SAT collapse (_vhAnchorSwitch) no band ever paints two at
   once — one section renders one panel, and more than one renders a tablist
   with exactly one panel visible. Left at 24px it sat between the tab row
   and its own panel and undid the -1px that attaches them, so the tabs read
   as a detached control floating above an unrelated box. */
.bs .vh-anchor-band { display: flex; flex-direction: column; gap: 0; margin-top: 20px; }

/* ── the section switcher (SAT only, today) ──
   Only rendered when the endpoint returns more than one section, which is SAT
   alone — see _vhAnchorSwitch in js/11b-verticals.js for why SAT's two
   stacked panels had to become one (the openers were 305px below the fold).

   Attached to the panel rather than floating above it: the tab row sits on
   the panel's own top border with -1px of margin, so the selected tab and the
   panel read as one object and the unselected one reads as behind it. Same
   hard-bordered, mono-kickered vocabulary as `.vh-open-numbers`; no pill
   shapes and no radius, which would be the only rounded thing on the page. */
.bs .vh-anchor-switch {
  display: flex;
  gap: 0;
  margin: 0 0 -1px;
  position: relative;
  z-index: 1;
}
.bs .vh-anchor-tab {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 9px 16px 8px;
  background: var(--bs-stock-2);
  border: 1px solid var(--bs-ink);
  border-right-width: 0;
  cursor: pointer;
  text-align: left;
  transition: background .12s ease, color .12s ease;
}
.bs .vh-anchor-tab:last-child { border-right-width: 1px; }
.bs .vh-anchor-tab:focus-visible {
  outline: 2px solid var(--bs-blue);
  outline-offset: 2px;
  z-index: 2;
}
.bs .vh-anchor-tab-lbl {
  font-family: var(--bs-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
}
/* The inactive tab's score is the whole reason the score is in the tab at
   all — it is the only place that half of the profile is legible without
   clicking. Tabular figures so the two tabs' numbers line up. */
.bs .vh-anchor-tab-score {
  font-family: var(--bs-ed);
  font-variation-settings: 'WONK' 1;
  font-weight: 900;
  font-size: 17px;
  line-height: 1;
  letter-spacing: -.01em;
  color: var(--bs-ink-2);
  font-variant-numeric: tabular-nums;
}
.bs .vh-anchor-tab:hover { background: var(--bs-sheet); }
.bs .vh-anchor-tab.is-on {
  background: var(--bs-sheet);
  border-bottom-color: var(--bs-sheet);
}
.bs .vh-anchor-tab.is-on .vh-anchor-tab-lbl { color: var(--bs-ink); }
.bs .vh-anchor-tab.is-on .vh-anchor-tab-score { color: var(--bs-blue); }
@media (max-width: 700px) {
  .bs .vh-anchor-switch { flex-direction: column; }
  .bs .vh-anchor-tab {
    border-right-width: 1px;
    border-bottom-width: 0;
    justify-content: space-between;
  }
  .bs .vh-anchor-tab:last-child { border-bottom-width: 1px; }
  .bs .vh-anchor-tab.is-on { border-bottom-color: var(--bs-ink); }
}

/* The radar track is sized from the DRAWING, and the drawing stopped being a
   220x220 square on 2026-08-11. Fixing the axis labels (11b-verticals.js,
   _vhRadarSvg) made the viewBox cover the labels as well as the rings, which
   measures ~279-307 units wide against 168 units of polygon — so at the old
   250px/220px track the polygon itself rendered 115-127px where it used to
   render 168px, a 25-32% shrink of the one graphic Mark said he preferred
   ("I do prefer the look of a radar chart"). Clean labels are not worth
   paying for in polygon size when the panel has the width to spare.

   250 -> 300 track / 220 -> 270 radar restores the polygon to ~151px, and
   at --wide's 320 -> 390 / 290 -> 360 to ~197px against the 221px it was.
   Both are ~90% of the old size rather than 100%: the last 10% costs another
   40-50px of track, and at --page-max 1080 the ten-row R&W standings table
   beside it still needs its own measure. Recompute with
   scripts/measure_radar_labels.py if a taxonomy ever renames its domains
   longer — polygonPx is reported per panel. */
/* `display: grid` below beats the UA stylesheet's `[hidden] { display: none }`
   on specificity, so _vhAnchorSwitch's own hidden panel stayed on the page —
   both SAT sections painted, the band was 985px, and the collapse did
   nothing at all. Declared here rather than as an inline style on the panel
   so the switcher stays a semantics-only change. */
.bs .vh-anchor[hidden] { display: none; }
.bs .vh-anchor {
  display: grid;
  grid-template-columns: 300px 1fr;
  gap: 34px;
  align-items: center;
  background: var(--bs-sheet);
  border: 1px solid var(--bs-ink);
  /* 22, not 28 — part of the same rhythm pass as `.vh-section` above. The
     panel is the tallest block on every arm homepage, so its interior is the
     one padding value that costs vertical budget twice. */
  padding: 22px 30px;
}
/* A section with a lot of axes gets a bigger graphic and a wider left
   column. SAT Reading & Writing has TEN domains: its standings table runs
   ~500px tall while a 220px radar plus score and kicker runs ~300px, so
   `align-items: center` split the 200px difference into two pools of dead
   space, one above the radar and one below the kicker — the panel Mark was
   pointing at when he said the rectangles were awkward. Ten labels around a
   220px decagon were also colliding with the polygon itself ("FORM &
   STRUCTURE" sat on the edge). Growing the graphic fixes both at once: the
   column gets taller so there is less to centre, and the labels get the
   room they needed anyway. Five- and six-axis sections are already balanced
   and keep the 250px column. */
.bs .vh-anchor--wide { grid-template-columns: 390px 1fr; }
.bs .vh-anchor--wide .vh-anchor-radar { width: 360px; }

@media (max-width: 760px) {
  .bs .vh-anchor,
  .bs .vh-anchor--wide { grid-template-columns: 1fr; gap: 20px; }
  /* One column now, so the graphic is bounded by the panel rather than by
     its own track — 290px would overflow a 320px viewport's ~228px of inner
     width. `.vh-radar-svg` is already width:100%/height:auto, so capping the
     wrapper scales the whole drawing, labels included. */
  .bs .vh-anchor-radar,
  .bs .vh-anchor--wide .vh-anchor-radar { width: 100%; max-width: 290px; }
}

/* "The panel must size to its content" — an unmeasured section is NOT this
   same 250px/1fr grid with an empty right column and a "Nothing measured"
   caption restating the graphic's own "Pending testing" overlay a second
   time (Mark: "not multiple times"). `.vh-anchor--pending` drops the grid
   and the standings column entirely, so the panel is just the placeholder
   square plus its border — hugging that content instead of stretching to
   `.vh-anchor-band`'s full width the way every measured `.vh-anchor` does. */
.bs .vh-anchor--pending {
  display: inline-flex;
  align-self: flex-start;
  padding: 20px;
}

.bs .vh-anchor-graphic { display: flex; flex-direction: column; align-items: center; text-align: center; }
.bs .vh-anchor-radar { width: 270px; }
.bs .vh-radar-svg { display: block; width: 100%; height: auto; overflow: visible; }
.bs .vh-anchor-score {
  font-family: var(--bs-ed);
  font-variation-settings: 'WONK' 1;
  font-weight: 900;
  font-size: 34px;
  line-height: 1;
  letter-spacing: -.02em;
  color: var(--bs-blue);
  margin-top: 8px;
  font-variant-numeric: tabular-nums;
}
.bs .vh-anchor-kicker {
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  margin-top: 5px;
}

/* The empty state. Mark, verbatim: "gray it out and put text over it saying
   'pending testing' — not multiple times, once there is even one data point
   it is fine." A flat grey square standing in for the radar, one overlay,
   never a caption repeated per row. */
.bs .vh-anchor-radar.is-pending {
  width: 220px;
  height: 220px;
  position: relative;
  background: var(--bs-stock-2);
  border: 1px solid var(--bs-rule);
  display: flex;
  align-items: center;
  justify-content: center;
}
.bs .vh-radar-placeholder { display: none; }
.bs .vh-anchor-pending-label {
  font-family: var(--bs-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
}

/* ── the standings table ──
   "a standings table beside it — each row carrying its own trend." A real
   `<table>`, not a card grid: Domain / Drills / Trend / Rating, rating
   right-aligned and numeric-faced, strongest first (the sports-page framing
   Direction C names, `_vhStandingsOrder`). */
.bs .vh-standings { border-collapse: collapse; width: 100%; }
.bs .vh-standings th {
  text-align: left;
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  padding: 0 10px 9px 0;
  border-bottom: 1.5px solid var(--bs-ink);
}
.bs .vh-standings th.r, .bs .vh-standings td.r { text-align: right; }
.bs .vh-standings td {
  padding: 11px 10px 11px 0;
  border-bottom: 1px solid var(--bs-rule);
  font-family: var(--bs-ui);
  font-size: 14px;
  color: var(--bs-ink);
}
.bs .vh-standings tbody tr:last-child td { border-bottom: 0; }
/* Density follows row count. A section with eight or more domains carries
   --wide, and those are the only panels whose HEIGHT is set by their table
   rather than by their graphic: AP Biology's eight units ran the band to
   472px, which left just 12px of the openers band above the fold at
   1024x768 (scripts/measure_openers_fold.py). 11px -> 8px of row padding
   takes ~6px off each of eight rows, which is the ~48px that costs nothing
   but air — the four- and six-row panels keep the roomier rhythm, because
   there the graphic is the taller column and tightening rows would buy
   nothing at all. Rows stay 36px, well clear of WCAG 2.2's 24px target.

   THE 36px ABOVE WAS NEVER TRUE, and the padding is not why. MEASURED on SAT
   Reading & Writing, the tallest --wide panel in the app: rows render 43px,
   not 36. The padding rule fires exactly as written (computed `8px 10px 8px
   0px`) — but line-height was left inherited at 1.6, and `td.v` sets the
   rating in 16px Fraunces, so the CONTENT box is 25.6px before any padding is
   added. 8 + 25.6 + 8 + 1 = 42.6. Padding was never the floor; the rating
   cell was. Constraining line-height here is what makes the rule reach the
   number it has claimed since it was written: 8 + 19.2 + 8 + 1 = 36.2.

   Ten rows x 7px is 70px off the panel, which is most of what put SAT R&W's
   openers below the 1024x768 fold. 1.2 rather than a tighter figure because
   Fraunces' descenders measured 5px outside a 1.0 box on the page title
   (02-chrome.css) and the same face sets this cell. */
.bs .vh-anchor--wide .vh-standings td {
  padding: 8px 10px 8px 0;
  line-height: 1.2;
}
.bs .vh-standings-row { cursor: pointer; transition: background .12s ease; }
.bs .vh-standings-row:hover, .bs .vh-standings-row:focus-visible {
  background: var(--bs-stock-2);
  outline: none;
}
.bs .vh-standings-row:focus-visible td:first-child { box-shadow: inset 2px 0 0 var(--bs-blue); }
.bs .vh-standings td.v {
  font-family: var(--bs-ed);
  font-variation-settings: 'WONK' 1;
  font-weight: 900;
  font-size: 16px;
  color: var(--bs-blue);
  font-variant-numeric: tabular-nums;
}
.bs .vh-trend-none {
  font-family: var(--bs-mono);
  font-size: 12px;
  color: var(--bs-ink-3);
}
/* Coloured by direction — green rising, grey flat, red falling — the day a
   real dim.trend arrives (see _vhTrendCell's own note on why every row is
   the honest dash today). */
.bs .vh-trend-spark { display: block; }
.bs .vh-trend-spark.is-up { color: var(--bs-ok); }
.bs .vh-trend-spark.is-flat { color: var(--bs-graphite); }
.bs .vh-trend-spark.is-down { color: var(--bs-miss); }
.bs .vh-trend-spark polyline { stroke: currentColor; }

@media (max-width: 480px) {
  .bs .vh-standings th:nth-child(2), .bs .vh-standings td:nth-child(2) { display: none; }
  .bs .vh-anchor { padding: 22px 18px; }
}

/* ── the ladder ── */
.bs .vh-ladder { margin-top: 4px; display: flex; flex-direction: column; gap: 26px; }
.bs .vh-ladder-group { display: flex; flex-direction: column; gap: 2px; }
.bs .vh-ladder-group-head {
  font-family: var(--bs-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  margin-bottom: 6px;
}
.bs .vh-ladder-row {
  display: grid;
  grid-template-columns: minmax(120px, 240px) 1fr auto;
  align-items: center;
  gap: 14px;
  width: 100%;
  background: transparent;
  border: 0;
  border-top: 1px solid var(--bs-rule);
  padding: 10px 0;
  cursor: pointer;
  text-align: left;
  font-family: var(--bs-ui);
}
.bs .vh-ladder-row:hover .vh-ladder-name { color: var(--bs-bronze); }
.bs .vh-ladder-row:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 2px; }
.bs .vh-ladder-name { font-size: 13.5px; font-weight: 700; color: var(--bs-ink); }
.bs .vh-ladder-bar-wrap { display: block; height: 8px; background: var(--bs-stock-2); border: 1px solid var(--bs-rule); }
.bs .vh-ladder-bar { display: block; height: 100%; background: var(--bs-camel-deep); }
.bs .vh-ladder-val {
  font-family: var(--bs-mono);
  font-size: 12px;
  font-weight: 600;
  color: var(--bs-ink-2);
  text-align: right;
  font-variant-numeric: tabular-nums;
}
/* "The truncation must say so out loud" — a real control, not a caption:
   clicking it opens the Review modal, where the full, untruncated ladder
   lives (openReviewModal, js/11b-verticals.js). */
.bs .vh-ladder-note {
  align-self: flex-start;
  margin-top: 8px;
  background: transparent;
  border: 0;
  padding: 0;
  font-family: var(--bs-mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--bs-ink-3);
  cursor: pointer;
  text-align: left;
}
.bs .vh-ladder-note:hover { color: var(--bs-bronze); }
.bs .vh-ladder-note:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 2px; }

@media (max-width: 480px) {
  .bs .vh-ladder-row { grid-template-columns: 1fr auto; }
  .bs .vh-ladder-bar-wrap { grid-column: 1 / -1; }
}

/* The full-list copy of the ladder inside the Review modal (openReviewModal)
   needs NO separate rule here — that modal is FRESH markup (js/11d-modals.js
   `openModal()`, `built` branch), and its panel already carries class `bs`
   (`el('div', 'vgm-panel bs')`, added for exactly this reason: "every
   freshly-built modal's content is reparented or freshly rendered from a
   broadsheet page"), so `.bs .vh-ladder` etc. above already match inside it. */

/* ══ BAND 3 · THE NUMBERS MODAL ══════════════════════════════════════════════
   "Numbers and the week live in a modal, opened from the page, not occupying
   it" (§3). A pasted-in supplement, not a floating card: hard rules, no radius,
   and the letterpress offset the vh-cards use on press — the same vocabulary as
   the page it covers, so it reads as another leaf of the same paper.

   THE ONLY SCROLLER IS .vh-modal-body, and it is height-bounded by the panel's
   min(84vh, ...) rather than being a full-height wrapper. That is the
   distinction tests/test_page_wrapper_scrolling.py draws, and this selector is
   in its ALLOWED_SCROLLERS with that argument. The modal is `hidden` at rest, so
   a full-page capture of the arm homepage never has content inside it to hide. */
/* `overflow: hidden` here is NOT a page clip — this element is a fixed
   viewport-sized overlay, not a wrapper around page content, and there is
   nothing inside it that a full-page capture could lose. It is what makes
   `overscroll-behavior: none` bind: a wheel over the scrim has no scroller of
   its own to move, so without a scroll container to stop at, it chains to the
   document and scrolls the page out from under the open dialog. */
.bs .vh-modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  overflow: hidden;
  overscroll-behavior: none;
}
/* A z-index ON THE MODAL IS NOT ENOUGH, and the number lies about it.
   `.screen` is `position: relative; z-index: 1` (01-tokens.css), which makes
   every screen its own STACKING CONTEXT — so `z-index: 60` above ranks the
   dialog against its siblings inside #screen-vertical and then the whole
   context composites at 1, under #global-bar's 30. Measured, not reasoned:
   `elementFromPoint(40, 40)` with the dialog open returned `gb-top`, the page
   header sat undimmed above the scrim in the plate, and the track switcher
   stayed clickable behind an `aria-modal="true"` dialog.

   So the SCREEN is what has to rise, and only while the dialog is open. Doing
   it with :has() rather than a class keeps the one source of truth for "is the
   modal open" as the `hidden` attribute the JS already toggles. */
#screen-vertical.bs:has(.vh-modal:not([hidden])) { z-index: 70; }
.bs .vh-modal[hidden] { display: none; }
.bs .vh-modal-scrim {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--bs-ink) 42%, transparent);
}
.bs .vh-modal-panel {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(760px, calc(100vw - 32px));
  max-height: min(84vh, 900px);
  display: flex;
  flex-direction: column;
  background: var(--bs-sheet);
  border: 2px solid var(--bs-ink);
  border-radius: 0;
  box-shadow: 8px 8px 0 var(--bs-ink);
}

.bs .vh-modal-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 18px;
  padding: 16px 20px 12px;
  border-bottom: 2px solid var(--bs-ink);
  background: var(--bs-stock-2);
}
.bs .vh-modal-title {
  font-family: var(--bs-ed);
  font-variation-settings: 'WONK' 1;
  font-weight: 900;
  font-size: 22px;
  line-height: 1;
  letter-spacing: -.02em;
  color: var(--bs-ink);
  margin: 0;
  margin-right: auto;
}
/* Two views, one dialog. §1's complaint about the three dashboards was three
   pages where there should be one page with a switch — so the switch is a
   tablist that reads as a folio rule, not as a pair of buttons. */
.bs .vh-modal-tabs { display: flex; gap: 0; }
.bs .vh-modal-tab {
  font-family: var(--bs-mono);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--bs-ink-3);
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  padding: 4px 12px 5px;
  cursor: pointer;
}
.bs .vh-modal-tab:hover { color: var(--bs-ink); }
.bs .vh-modal-tab.is-on { color: var(--bs-ink); border-bottom-color: var(--bs-bronze); }
.bs .vh-modal-tab:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 2px; }
.bs .vh-modal-x {
  font-family: var(--bs-mono);
  font-size: 20px;
  line-height: 1;
  color: var(--bs-ink-3);
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0 2px;
  cursor: pointer;
}
.bs .vh-modal-x:hover { color: var(--bs-ink); }
.bs .vh-modal-x:focus-visible { outline: 2px solid var(--bs-blue); outline-offset: 3px; }

.bs .vh-modal-body {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 20px var(--page-gutter) 26px;
  min-height: 0;
}
/* a's three tiles were sized for a 1040px page column. In a 760px dialog three
   of them are 240px each and the "Reading & Writing" label breaks to three
   lines, so below the panel's own breakpoint they stack — same builder, same
   three rows, one column. */
@media (max-width: 660px) {
  .bs .vh-modal-body .dx-tiles { grid-template-columns: minmax(0, 1fr); }
  .bs .vh-modal-body .dx-tile { border-left: 0; padding-inline: 0; }
  .bs .vh-modal-body .dx-tile + .dx-tile { border-top: 1px solid var(--bs-rule); }
}

/* ── the page measure on this screen ──
   .vh-row once held a 200px marginalia rail beside .vh-wrap, and these rules
   sized it. showVerticalHome() stopped attaching one — a homepage has no single
   item for a coach to remark on — and ARCHITECTURE-2026-08-07 §4 makes that the
   rule everywhere, so the two `> .margin` declarations are gone. What is left is
   the page measure itself, which .vh-wrap needs whether or not anything sits
   beside it. */
@media (min-width: 1280px) {
  #screen-vertical.bs .vh-row {
    gap: 40px;
    max-width: var(--page-max);
    margin-inline: auto;
    width: 100%;
    align-items: flex-start;
  }
  #screen-vertical.bs .vh-wrap { max-width: none; flex: 1 1 auto; }
}

/* ── unmeasured, not zero (Mark, 2026-08-12 spec §6) ──
   "Nothing is hidden when there is no data. Everything renders with dashes."
   The AP profile returns every unit of the exam now, so the standings table
   and the ladder both carry rows whose value is genuinely absent. They render
   the same em dash the Trend column has used all along — set quieter than a
   real number so a column of dashes reads as "not yet", not as a score. */
.bs .vh-standings td.v.is-unmeasured,
.bs .vh-ladder-val.is-unmeasured { color: var(--bs-ink-3); font-weight: 400; }
/* The bar track stays, so the row keeps its shape and the column still lines
   up; only the fill is absent. A zero-width bar at the left edge would read as
   a score of 1.0 on a 1-5 scale, which is the one thing this must not say. */
.bs .vh-ladder-bar-wrap.is-unmeasured .vh-ladder-bar { display: none; }

/* ── Row height on a touch screen ──────────────────────────────────────────
   The --wide density rule above is deliberate and measured: 8px of padding and
   a 1.2 line-height give 36px rows so AP Biology's eight units do not run the
   band past the 1024x768 fold (scripts/measure_openers_fold.py). That argument
   is about a DESKTOP fold.

   The 375 sweep flagged the same rows at 301x36 — under the 44px a thumb can
   reliably hit, on the control that is the door into the unit notes. Both are
   right, and they are not in conflict, because they are about different inputs
   at different widths: below 700px there is no 1024x768 fold to protect and no
   hover to fall back on, so the rows get their height back. */
@media (max-width: 699px) {
  .bs .vh-anchor--wide .vh-standings td,
  .bs .vh-standings td {
    padding-top: 13px;
    padding-bottom: 13px;
    line-height: 1.3;
  }
}

/* ── The Lab's two-up openers band ─────────────────────────────────────────
   Cognition drops Training plan and Review — by the numbers (see
   _vhOpenersBand: it measures a ceiling, has no exam date to plan toward and no
   traps to review), which leaves the two openers the Lab is actually for.

   An even split, not the 2fr lead the three- and four-box rows carry. The lead
   weighting exists to give a page ONE loudest thing among several competing
   ones; with two boxes there is nothing to out-weight, and keeping the 2fr would
   leave the dailies looking like the leftover beside it — which is the thing
   being fixed. Mark, 2026-08-21: "the dailies were somewhat harder to see." */
.bs .vh-openers--two { grid-template-columns: 1fr 1fr; }

@media (max-width: 700px) {
  /* Stacked rather than two 160px columns: each box carries a label, a figure
     and a caption, and none of that survives being halved on a phone. */
  .bs .vh-openers--two { grid-template-columns: 1fr; }
}
