/* ══ THE CAPTION PASS ═══════════════════════════════════════════════════════
   A labelling face was doing the work of a reading face: on the dashboard the
   micro-label face outnumbered the body face three to one, all of it tracked
   mono caps at 9-9.5px. Measured side by side, CASE turned out to be the lever
   rather than the face — and mono set lowercase reads as terminal output, so
   the two moves belong together.

   Mono keeps what it is genuinely good at: the running head, the folio, the
   section kickers, and figures-with-units. What is DESCRIPTIVE becomes a
   caption — instrumentation face, sentence case, 11px floor.

   Scoped `.bs …` (0,2,0) and placed last, so it wins on source order without
   !important. Revert by deleting this block.
   ═══════════════════════════════════════════════════════════════════════════ */

.bs .dv2-stat-label,
.bs .dv2-ring-label,
.bs .dv2-tchip-label,
.bs .dv2-skill-meta,
.bs .dv2-hist-domain,
.bs .dv2-hist-meta,
.bs .dv2-tip,
.bs .dvp-sub,
.bs .ci-dname,
.bs .ci-gains-lbl,
.bs .ci-badge-lbl {
  font-family: var(--bs-ui);
  font-size: 11px;
  font-weight: 500;
  /* Tracking that helps a 9px cap hurts an 11px lowercase word. */
  letter-spacing: .01em;
  text-transform: none;
}

/* These two labels are stored lowercase in JS ('practice streak'), and the app
   has no case convention — some label strings are pre-cased in source, others
   rely entirely on CSS. `capitalize` would give Title Case, so lift only the
   first letter and leave the sentence alone. */
.bs .dv2-stat-label::first-letter,
.bs .dv2-tchip-label::first-letter {
  text-transform: uppercase;
}

/* Agate that stays mono because it is furniture or a unit, but still clears the
   11px floor — nothing below 11px should be carrying information. */
.bs .dv2-sec-title { font-size: 11px; letter-spacing: .18em; }
.bs .dv2-skill-pct { font-size: 11px; }

/* Only four measures on this screen actually realise a long line; the rest were
   latent container width, not set text. The history preview is deliberately NOT
   capped — it is one clipped line, so a narrower measure would clip more. */
.bs .dvp-overconf,
.bs .ci-hint { max-width: 68ch; }

/* ══ THE SUMMARY BAND ═══════════════════════════════════════════════════════
   The hero was `grid-template-columns: 1fr 248px`, and the two dials only need
   308px of that first column — so 440px of the most important block on the page
   was empty by construction. That hole, not line length, is what read as
   "clunky": the emptiness is horizontal, and page height is unchanged either way.

   A newspaper would set this as one ruled summary band, so: dials at the left,
   the three figures across the middle, the action at the right edge.

   An earlier attempt at this failed — the labels shattered onto three lines and
   the button swelled to 400px. What makes it work now is the caption pass above:
   11px Archivo sentence case is far narrower than tracked mono caps, so each
   figure keeps its label on one line.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (min-width: 901px) {
  .bs .dv2-hero-main {
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: 36px;
  }
  /* Promote the stats and the button to items of the band itself, so the middle
     column can stretch instead of sitting in a fixed 248px rail. */
  .bs .dv2-hero-side { display: contents; }
  .bs .dv2-hero-rings { justify-content: flex-start; gap: 46px; }
  .bs .dv2-hero-stats {
    flex-direction: row;
    align-items: flex-start;
    /* Wrap, don't overflow. The band needs ~1068px to sit on one line; between
       901 and that, the figures wrap to a second row instead of running off the
       card, which is how a band should degrade. */
    flex-wrap: wrap;
    row-gap: 18px;
    gap: 44px;
    /* The rule that separated the rail moves here — it still divides the
       figures from the dials, which is the distinction it was drawing. */
    border-left: 1px solid var(--bs-rule);
    padding-left: 36px;
  }
  .bs .dv2-hero-stats .dv2-stat { flex: 0 0 auto; }
  .bs .dv2-hero-stats .dv2-stat-label { white-space: nowrap; }
  .bs .dv2-hero-main > .btn-primary {
    justify-self: end;
    align-self: center;
    width: auto;
  }

  /* ── Cognition Lab: set the index in two columns ──
     Fifteen entries ran as one column down a 1024px page, so every row left a
     ~575px empty channel to the right of its description — the clearest instance
     of the same horizontal emptiness. A printed puzzle index would set this in
     columns, which uses the measure, shortens the page, and brings each row's
     sparkline in beside the entry it belongs to. */
  .bs .cog-grid {
    display: grid;
    /* minmax(0, 1fr), not 1fr: `1fr` is `minmax(auto, 1fr)`, and a row whose
       intrinsic minimum exceeds the track then pushes the grid wider than its
       container. Measured 2 overflowing ancestors before this. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: 52px;
  }
}

/* ══ THE SHEET ══════════════════════════════════════════════════════════════
   The question screen read worst of all at full size: a 183px card in a 900px
   window, top-aligned, left 444px of empty page below it — 49% of the screen.
   The card was floating in a void rather than sitting on a page.

   A sheet of paper does not stop where its text stops. Let the body fill the
   window and let the margin's rule run the full height, so the space below the
   question reads as the rest of the page — which is also what gives the margin
   somewhere to put a note.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (min-width: 921px) {
  #screen-practice.bs .practice-body {
    flex: 1 1 auto;
    align-items: stretch;
  }
  /* The document itself still starts at the top — only the sheet is full height. */
  #screen-practice.bs .bs-doc { align-self: start; }
  /* Sticky was compensating for a short rule; a full-height margin doesn't need it. */
  #screen-practice.bs .margin {
    position: static;
    min-height: 100%;
    display: flex;
    flex-direction: column;
  }
  /* Idle doodles collect under the kicker; anything earned sits at the foot of
     the margin, where a reader's own note would go. */
  #screen-practice.bs .margin .doodles { flex: 1 1 auto; }
}

/* Filling the sheet is not enough on its own — a 720px card holding three lines
   in a 1440x900 window still reads as a card in a void, and the margin rule just
   outlines the emptiness. The screen shows ONE question at a time, so set it
   like a lead item: the paper gets bigger type, not more furniture.
   Gated on height too, so a short laptop window doesn't push the answer field
   below the fold. */
@media (min-width: 1200px) and (min-height: 760px) {
  #screen-practice.bs .problem-card { max-width: 812px; }
  #screen-practice.bs .problem-card .problem-text { font-size: 19px; line-height: 1.6; }
  #screen-practice.bs .problem-card .problem-text.has-ask .bs-ask { font-size: 31px; }
  #screen-practice.bs .bs-folio { font-size: 46px; }
}

/* ── the lean sheet ──
   Bigger type alone only bought 25px back: a short numeric stem cannot fill a
   900px window and pretending otherwise means inflating type until it shouts.
   So the page adapts instead — `doc-lean` is set by measurement when the
   document leaves real slack, which happens for a bare stem and never for an
   R&W passage or a figure item.

   Given slack, do two things: sit the document on the optical centre of the
   sheet so the space is shared above and below rather than pooling at the foot,
   and give the sheet a visible bottom edge so the page ends instead of fading
   out. Centring is only ever applied HERE — applied unconditionally it left
   dead space above tall items, which is why it was removed once already. */
@media (min-width: 921px) {
  #screen-practice.doc-lean.bs .bs-doc {
    /* The sheet rule above sets align-self:start, which makes this box only as
       tall as its content — and justify-content cannot centre within a box that
       has no spare height. Stretch it first, then centre inside it. */
    align-self: stretch;
    justify-content: center;
    padding-bottom: 4vh;
  }
  /* The rule is a second grid row, so row one has to claim the slack or the
     margin's vertical rule stops short of the page edge and the two don't meet. */
  #screen-practice.doc-lean.bs .practice-body {
    grid-template-rows: minmax(0, 1fr) auto;
  }
  #screen-practice.doc-lean.bs .practice-body::after {
    content: "";
    grid-column: 1 / -1;
    align-self: end;
    border-top: 1px solid var(--bs-rule);
    margin-top: 12px;
  }
}

/* ══ THE MARGIN, EVERYWHERE ═════════════════════════════════════════════════
   Of 21 screens, exactly two carried any personality: the handwriting, the
   stamp, the pen and the doodles all lived on practice and feedback, and the
   margin was hard-scoped to `#screen-practice`. Every results page had the
   design's voice removed from it.

   The margin now belongs to the pages a student actually reads about
   themselves. Red pen stays what it always was — a margin annotation, the
   coach's own hand — and it earns its place rather than decorating: the note
   only appears when the week has actually produced something to remark on.
   ═══════════════════════════════════════════════════════════════════════════ */

.bs .margin-note {
  font-family: var(--bs-hand);
  font-size: 19px;
  line-height: 1.22;
  color: var(--bs-pen);
  /* Same hand as the coach's quip on the feedback sheet, a size down: this is
     an aside in a margin, not the verdict on a result. */
  transform: rotate(-1.4deg);
  transform-origin: left top;
  margin-top: 18px;
  max-width: 22ch;
}
.bs .margin-note::before {
  content: "";
  display: block;
  width: 26px;
  border-top: 1.5px solid var(--bs-pen);
  margin-bottom: 7px;
  opacity: .8;
}

/* 1280, not the practice margin's 921: the summary band needs ~952px of content
   column, so below this a rail squeezes the band until the figures overflow.
   Measured — at 1024 the band, the stat row and the history preview all broke. */
@media (min-width: 1280px) {
  /* The dashboard's body is the scroller (inline overflow-y), so the margin
     becomes a second track of it rather than a child of the content column. */
  #screen-dashboard.bs .practice-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(168px, 230px);
    column-gap: 34px;
    align-content: start;
    /* A page has gutters. Without this the margin rail runs flush to the
       viewport edge, which reads as a cut-off column rather than a margin. */
    padding-inline: 40px;
  }
  #screen-dashboard.bs .margin {
    position: sticky;
    top: 18px;
    display: flex;
    flex-direction: column;
    margin-top: 74px;
  }
}

/* Below rail width the margin becomes a foot-of-page note instead. It must not
   reserve height — the shared .margin sets min-height:300px for the question's
   sheet, which here would leave a 300px empty dashed box on a phone — and it
   disappears entirely when nothing was earned, since an empty margin is just a
   labelled void. Idle doodles belong in a rail, not stacked under a report. */
@media (max-width: 1279px) {
  #screen-dashboard.bs .margin {
    min-height: 0;
    border-left: none;
    padding-left: 0;
    margin: 26px 0 8px;
  }
  #screen-dashboard.bs .margin .doodles { display: none; }
  #screen-dashboard.bs .margin:not(:has(.margin-note)) { display: none; }
}

/* ── the front page and the lab get rails too ──
   The rail is a SIBLING of each content wrapper, never a track inside it. Making
   `.home-wrap` / `.drills-wrapper` a grid to hold the rail turned every child
   into a grid item, and grid items' vertical margins do not collapse the way
   block siblings' do — the lab gained ~26px per row across eight rows, a 208px
   hole above "Today's Warm-up". Both parents were already single-child flex
   boxes, so the rail goes in beside the wrapper and the wrapper stays block. */
@media (min-width: 1280px) {
  #screen-home.bs,
  #screen-drills.bs .mg-body-inner {
    flex-direction: row;
    align-items: flex-start;
    justify-content: center;
    gap: 40px;
  }
  #screen-home.bs > .margin,
  #screen-drills.bs .mg-body-inner > .margin {
    flex: 0 0 230px;
    display: flex;
    flex-direction: column;
    /* Overrides the shared .margin's 300px: the rail takes the height its
       content needs and never sets the page's rhythm. */
    min-height: 0;
    /* Clear the masthead so the rail starts at the body, not the nameplate. */
    margin-top: 96px;
  }
}
/* Below rail width both fall back to the dashboard's foot-note treatment: no
   reserved height, and absent entirely when nothing was earned. */
@media (max-width: 1279px) {
  #screen-home.bs > .margin,
  #screen-drills.bs .mg-body-inner > .margin {
    min-height: 0;
    border-left: none;
    padding-left: 0;
    margin: 22px 0 6px;
    align-self: stretch;
  }
  #screen-home.bs > .margin .doodles,
  #screen-drills.bs .mg-body-inner > .margin .doodles { display: none; }
  #screen-home.bs > .margin:not(:has(.margin-note)),
  #screen-drills.bs .mg-body-inner > .margin:not(:has(.margin-note)) { display: none; }
}

/* ══ STAMPS FOR WHAT WAS EARNED ═════════════════════════════════════════════
   The skill table sets every state in the same flat tag, so "mastered" and
   "locked" carry identical weight — the table records states but celebrates
   nothing. The feedback sheet already owns a rubber-stamp treatment, so reuse
   it here rather than invent a second one.

   Only the EARNED states are stamped. Twenty-three rows each with a tilted
   stamp would be noise, and a stamp on "locked" would be celebrating a locked
   door — so proficient and mastered get the stamp, and the neutral states stay
   quiet type. That is what makes the stamp mean something when it appears.

   No `opacity` to soften the ink: on paper alpha erases rather than mutes, so
   any recession is a chosen tone.
   ═══════════════════════════════════════════════════════════════════════════ */

.bs .dv2-chip.proficient,
.bs .dv2-chip.mastered {
  border-width: 2px;
  border-style: solid;
  border-radius: 0;
  padding: 3px 9px 2px;
  font-family: var(--bs-mono);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: .14em;
  transform: rotate(-1.8deg);
  /* The row is a table line; the stamp must not shift its neighbours. */
  transform-origin: center;
}
.bs .dv2-chip.mastered {
  /* Inked solid — the only filled stamp in the table, so the top state reads
     first when the eye runs down the column. */
  border-color: var(--bs-ok);
  background: var(--bs-ok);
  color: var(--bs-stock);
  transform: rotate(-2.6deg);
}
.bs .dv2-chip.proficient {
  border-color: var(--bs-ok);
  background: transparent;
  color: var(--bs-ok);
}
/* Locked is not a state to dress up: no stamp, no rotation, receded ink. */
.bs .dv2-chip.locked {
  border-color: var(--bs-rule);
  color: var(--bs-ink-3);
  background: transparent;
  transform: none;
}

/* ══ THE TRAINING PLAN — FIRST BROADSHEET PASS ══════════════════════════════
   This screen carried `.screen` and never `.bs`, which is why an audit found
   zero Fraunces on it: it was not a converted screen missing its type, it was
   never converted at all. Ten unrelated sizes live at once, four axis labels do
   real work at 10px, and `.pacing-headline` sets a 132-character line with no
   measure. `bs` is now on the screen div, so the shared layer applies and these
   rules finish the job.

   Same devices as the rest of the paper: card titles and bare figures in
   Fraunces (ink for a name, ultramarine for a rating — the skill card's
   existing split), section heads as mono kickers, descriptive labels as 11px
   Archivo sentence case. The pacing row's axis tags stay mono caps because they
   are figure captions, not prose — the precedent is `.stat::before`, not the
   dashboard's full-phrase labels.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Bare `1fr` is `minmax(auto, 1fr)`, so one long focus-skill name pushes the row
   past its container. Both states are mirrored under the ORIGINAL breakpoints:
   a single unguarded `.bs` rule here (0,2,0) would outrank the base rule's
   unscoped mobile collapse (0,1,0) and break single-column on a phone. */
.bs .training-header { grid-template-columns: minmax(260px, 360px) minmax(0, 1fr); }
@media (max-width: 720px) { .bs .training-header { grid-template-columns: minmax(0, 1fr); } }
.bs .week-preview { grid-template-columns: repeat(7, minmax(0, 1fr)); }
@media (max-width: 900px) { .bs .week-preview { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* Three tracks in 320px is a ~100px cell, and a day card holds a skill NAME —
   the base rule kept 3 all the way down and the cards overflowed. Two below
   480px, which is the narrowest a named day can honestly be set. */
@media (max-width: 480px) { .bs .week-preview { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.bs .training-section-title { font-family: var(--bs-mono); font-weight: 400; font-size: 11px;
  letter-spacing: .16em; color: var(--bs-blue); }

/* The focus skill is this page's headline; colour was doing a headline's job
   here, which is the accent's job elsewhere. Ink, at editorial weight. */
.bs .tt-focus { font-family: var(--bs-ed); font-variation-settings: 'WONK' 1;
  font-weight: 900; letter-spacing: -.02em; color: var(--bs-ink); }

.bs .td-label, .bs .tt-label, .bs .cadence-lbl { font-family: var(--bs-ui); font-weight: 500;
  font-size: 11px; letter-spacing: .01em; text-transform: none; color: var(--bs-ink-3); }

.bs .ps-num { font-family: var(--bs-ed); font-variation-settings: 'WONK' 1;
  font-weight: 900; letter-spacing: -.03em; color: var(--bs-ink); }
.bs .pacing-arrow { font-family: var(--bs-mono); color: var(--bs-ink-3); }
/* Off the 10px floor, and detracked as it grows. */
.bs .ps-lbl { font-family: var(--bs-mono); font-weight: 500; font-size: 11px;
  letter-spacing: .08em; color: var(--bs-ink-3); }
/* The base states fill a tint pill (`--gold-bg`, which retargeting turned into a
   pale ultramarine wash). In this system a status is a hairline-bordered tag,
   never a filled tint — so the fill goes and the state is carried by ink. */
.bs .pacing-intensity {
  font-family: var(--bs-mono); font-weight: 500; letter-spacing: .14em;
  background: transparent; border: 1px solid var(--bs-rule); color: var(--bs-ink-2);
}
.bs .pacing-intensity.on_target { border-color: var(--bs-ok); color: var(--bs-ok); }
.bs .pacing-intensity.steady,
.bs .pacing-intensity.focused { border-color: var(--bs-ink-2); color: var(--bs-ink); }
.bs .pacing-intensity.intensive,
.bs .pacing-intensity.urgent { border-color: var(--bs-miss); color: var(--bs-miss); }
/* The one measure on this screen that genuinely sets a long line. */
.bs .pacing-headline { max-width: 68ch; }

.bs .ws-label { font-family: var(--bs-ed); font-variation-settings: 'WONK' 1;
  font-weight: 900; font-size: 16px; letter-spacing: -.015em; color: var(--bs-ink); }
/* Was set in JetBrains Mono — a face outside the three type roles entirely.
   A skill's rating is a figure, so it reads like every other figure. */
.bs .ws-rating { font-family: var(--bs-ed); font-variation-settings: normal;
  font-weight: 900; font-size: 20px; letter-spacing: -.01em; color: var(--bs-blue); }
/* Holds a subject slug, already lowercase in source — so lift the first letter
   only. No `text-transform: lowercase`: it buys nothing here and would flatten
   an acronym the day a subject is ever displayed as one. */
.bs .ws-meta { font-family: var(--bs-ui); font-weight: 500; font-size: 11px;
  letter-spacing: .01em; color: var(--bs-ink-3); }
.bs .ws-meta::first-letter { text-transform: uppercase; }

.bs .dc-date { font-family: var(--bs-mono); font-weight: 500; font-size: 11px;
  letter-spacing: .08em; color: var(--bs-ink-3); }
.bs .dc-focus { font-family: var(--bs-ed); font-variation-settings: 'WONK' 1;
  font-weight: 900; font-size: 14px; line-height: 1.2; letter-spacing: -.01em;
  color: var(--bs-ink); min-height: 34px;
  /* A day cell is ~140px at phone width and holds a whole skill name; without
     this, "Trigonometry" alone overruns the card by 10px. */
  overflow-wrap: anywhere; }
.bs .dc-target { font-family: var(--bs-mono); font-weight: 500; font-size: 11px;
  letter-spacing: .04em; color: var(--bs-ink-3); }

/* Unearned badges muted with `opacity: .45` on the whole card, text included —
   the alpha-erasure trap again. Recede with a chosen ink so both states stay at
   full opacity and "earned" is the only thing that actually differs. */
.bs .badge-card { opacity: 1; }
.bs .badge-card:not(.earned) .badge-icon,
.bs .badge-card:not(.earned) .badge-name,
.bs .badge-card:not(.earned) .badge-desc { color: var(--bs-ink-3); }
.bs .badge-name { font-family: var(--bs-ed); font-variation-settings: 'WONK' 1;
  font-weight: 900; font-size: 13px; letter-spacing: -.01em; color: var(--bs-ink); }

/* The pacing chips are 62px wide at phone width and hold a 30px Fraunces figure:
   "54m 28s" wanted 66px in a 43px box. This was already clipping before the
   optical-size repair (46px into 43px) and the correct, wider glyphs made it
   about 4px worse — so it belongs to this pass now. */
@media (max-width: 460px) {
  .bs .dv2-tchip-val { font-size: 18px; letter-spacing: -.02em; }
}
