/* Reserve the scrollbar gutter permanently. The page container is centred at max-width 1120px, so a
   screen WITH a scrollbar and one WITHOUT centre 7-8px apart — every navigation between a long screen
   and a short one nudged the whole layout sideways. Reserving the gutter keeps the container fixed.
   Unsupported browsers ignore it and simply keep the old behaviour. */
html { scrollbar-gutter: stable; }
/* LOCK THE PAGE BEHIND AN OPEN DIALOG (2026-09-15). The scrim dimmed the page and blocked clicks, but the
   wheel still scrolled it -- so the content a teacher was reading slid away underneath a modal she could
   not interact with, and on a trackpad it happened by accident.

   PURE CSS, via :has(), rather than a JS lock and an unlock. A lock that is applied on open and released on
   close needs a counter for stacked dialogs (Class Settings -> Delete confirm) and a dispose hook that fires
   at exactly the right moment; :has() asks the DOM instead of tracking it, so stacking, teardown and a
   dialog removed by any other means are all correct for free and there is nothing to leave stuck. :has() is
   already used elsewhere in this file, so it is not a new dependency.

   NO LAYOUT SHIFT, and that is what scrollbar-gutter: stable above buys: overflow: hidden is still a
   scrolling overflow value, so the gutter stays reserved and the centred container does not jump sideways
   the moment a dialog opens. Without that line this rule would be visibly worse than the bug.

   The dialog's own scrolling is untouched -- .rr-modal keeps max-height: 88vh with overflow-y: auto. */
html:has(.rr-modal-back), html:has(.rp-modal-backdrop) { overflow: hidden; }

:root {
    --rp-ink: #1f2933;
    --rp-head: #1f3864;
    --rp-accent: #10788d;
    --rp-accent-bright: #1a9fbf;
    --rp-accent-soft: #e2eef1;
    --rp-warm: #f5871f;
    --rp-line: #d9e2ec;
    --rp-bg-soft: #f7fafc;
    --rp-good: #1e7a34;
    --rp-good-bg: #e6f4ea;
    /* GOLD, IN TWO VALUES, because the standard asks two different things of it (2026-09-15).

       --rp-gold is for BORDERS AND ICONS: the current-level chip on the journey strip, the honours frame
       round a distinction certificate. Non-text, so the bar is 1.4.11 at 3:1, and 3.25:1 clears it.
       Its old comment claimed "contrast on white" without saying contrast for WHAT, which is how it came to
       be used for text as well.

       --rp-gold-text is for gold TEXT, and exists because .cert-distinction was wearing the value above. It
       renders "★ With Distinction ★" at 16px bold on the certificate -- bold does NOT reach the large-text
       bar until 18.7px, so it needed 4.5:1 and had 3.25:1. On the proudest page in the product.
       #8a6508 is 5.32:1 and still reads gold rather than brown. #997009 was the lightest gold that looked
       right and measures 4.49:1 -- under the bar by one hundredth, which is the whole argument for not
       cutting these fine. */
    --rp-gold: #b8860b;
    --rp-gold-text: #8a6508;
    --rp-bad: #b23b3b;
    --rp-bad-bg: #fbe9e9;
    --rp-hint-bg: #fff7e6;
    --rp-hint-line: #f0c36d;

    /* SECONDARY TEXT, one value (2026-09-15). Captions, sub-labels, table headers, attributions, "no work
       yet" placeholders -- everything quieter than body copy but still meant to be READ.

       It replaces FIVE hard-coded greys across 57 rules -- #7b8794 (38), #9aa5b1 (10), #8a94a0, #8a95a0,
       #94a3b8 -- every one of which failed WCAG 2.2 AA 1.4.3 at the sizes it was used: 3.66:1 down to
       2.50:1 against the 4.5:1 normal text needs, and almost all of it set between .72rem and .9rem.

       THE TWO-TIER GREY DID NOT SURVIVE, and that is a real loss honestly taken. There was a darker muted
       (#7b8794) and a lighter one (#9aa5b1), and NO lighter value can pass 4.5:1 -- the tier and the
       standard are incompatible at these sizes, so the tier went. Where a hierarchy is still wanted it has
       to come from weight, size or spacing, not from fading the text further.

       #636e7b, not the #68737f that fixed .rr-modal-hint first: 5.19:1 on white and 4.95:1 on --rp-bg-soft,
       which muted text sits on often enough to matter. #68737f is 4.61:1 on that panel -- passing, with
       almost no room for a later tweak to quietly break it. The same headroom argument that chose 28px over
       24px for a button, applied to the thing it was actually about. */
    --rp-muted: #636e7b;

    /* An INACTIVE control, which 1.4.3 exempts as incidental -- only the locked level chips wear it. Named
       rather than left as a literal so the exemption is a decision on the record and not an oversight that
       happens to look like one. */
    --rp-disabled: #9aa5b1;

    /* QUIET NON-TEXT AT THE 3:1 BAR (2026-09-15) — the row chevron, the empty-cell dash, the "off" milestone
       marker, and the boundary of a quiet button. They are graphics, so the bar is 1.4.11 Non-text Contrast
       at 3:1 rather than 1.4.3's 4.5:1, and this is the lightest blue-grey that clears it with room (3.20:1).
       It exists BECAUSE that distinction kept getting lost: each of the three was a separate literal between
       1.72:1 and 2.57:1, and the temptation each time was either to leave them alone or to darken them to
       --rp-muted and flatten a quiet mark into body text. One token holds the middle position. */
    --rp-mark: #86919f;
}

.rp-main { max-width: 1120px; margin: 0 auto; padding: 1.25rem 1.25rem 4rem; color: var(--rp-ink); flex: 1 0 auto; width: 100%; }

/* --- App chrome: navy header + footer bookend the light content (adapted from eLearnster
   team.master; footer lifted near-verbatim). Sticky footer via #app flex column. --- */
#app { display: flex; flex-direction: column; min-height: 100vh; }

.rp-appheader { background: #1b3a57; color: #fff; }
.rp-appheader-in { max-width: 1120px; margin: 0 auto; padding: 16px 1.25rem; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.rp-brand { display: flex; align-items: center; gap: 14px; }
/* Inline "dropped-A" logotype: the mark IS the letter A at the head of the wordmark. Cropped viewBox tucks
   it flush against "pply"; it towers above the caps (star up top) and its base drops to the p's descender. */
.rp-logo-mark { height: 40px; width: auto; display: inline-block; vertical-align: -5px; margin-right: 2px; }
.rp-brand-text { line-height: 1.12; }
.rp-wordmark { font-size: 25px; font-weight: 800; letter-spacing: .2px; }
.rp-wm2 { color: var(--rp-warm); }
.rp-tm { font-size: 11px; vertical-align: super; font-weight: 600; }
/* Subject-area descriptor — "Apply2Mastery Literacy" reads as one product name (brand bold, subject
   lighter, a thin divider between). Swap "Literacy" per program (Math, Science, …) in the family. */
.rp-subject { font-weight: 500; color: #aecbe8; margin-left: 10px; letter-spacing: .2px; }
.rp-tagline { font-size: 11px; color: #aecbe8; font-weight: 600; letter-spacing: .3px; margin-top: 9px; }
.rp-levelbadge { display: flex; align-items: center; gap: 8px; background: rgba(255, 255, 255, .10); border: 1px solid rgba(255, 255, 255, .22); padding: 7px 15px; border-radius: 999px; }
.lvl-emblem { display: block; flex: 0 0 auto; }
/* Tier-colored medallion (docs/LevelLabels_Plan.md palette). --tier fills the disc, --tier-mark the center;
   both unset (no level / Level 0) → transparent disc + accent center = the original neutral placeholder. */
.lvl-emblem-disc { fill: var(--tier, transparent); }
.lvl-emblem-mark { fill: var(--tier-mark, var(--rp-accent-bright)); }
.lvl-tier-1 { --tier: #B0774A; --tier-mark: #fdf3ea; }
.lvl-tier-2 { --tier: #9CA3AB; --tier-mark: #ffffff; }
.lvl-tier-3 { --tier: #D4AF37; --tier-mark: #fffbe9; }
.lvl-tier-4 { --tier: #D8DCE0; --tier-mark: #7c8894; }
.lvl-tier-5 { --tier: #7FD4E8; --tier-mark: #08313b; }
.rp-levelbadge .lvl-emblem { width: 28px; height: 28px; color: #fff; }
/* Level emblem beside the "Your {Level} Progress" heading (navy, on the light dashboard). */
.nd-arc-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 0 .5rem; }
.nd-arc-heading { display: flex; align-items: center; gap: 12px; }
.nd-arc .nd-arc-head h2 { margin: 0; }
.nd-arc-head .lvl-emblem { width: 30px; height: 30px; padding: 6px; box-sizing: border-box; background: var(--rp-accent-soft); color: var(--rp-accent); border-radius: 8px; margin-top: 0; }

/* Growth line (dashboard suggestion #2): a compact tier-color dot ladder + one climb sentence, under the
   Reading Progress heading. Dots run start-tier → Diamond; past filled, current lit + ringed, future hollow.
   All CSS (no inline SVG). Tier fills reuse docs/LevelLabels_Plan.md. */
.nd-growth { margin: 16px 0 1rem 20px; }
.nd-growth-ladder { display: flex; align-items: flex-start; margin: 0 0 9px; }
.nd-growth-step { display: flex; flex-direction: column; align-items: center; position: relative; }
.nd-growth-step.has-conn { margin-left: 28px; }
.nd-growth-step.has-conn::before { content: ""; position: absolute; top: 8px; left: -28px; width: 28px; height: 2px; background: #d7dce2; border-radius: 2px; }
.nd-growth-step.has-conn.conn-done::before { background: #b7bfc8; }
.nd-growth-dotcell { height: 17px; display: flex; align-items: center; }
.nd-growth-dot { width: 12px; height: 12px; border-radius: 50%; box-sizing: border-box; }
.nd-growth-dot.cur { width: 17px; height: 17px; box-shadow: 0 0 0 3px var(--nd-gt-ring, rgba(31,58,86,.18)); }
.nd-growth-dot.future { background: transparent; border: 1.5px solid #c7cdd4; }
.nd-growth-line { font-size: 13px; color: #42586a; line-height: 1.5; margin: 0; }
.nd-growth-line strong { font-weight: 600; color: #1f3a56; }
.nd-gt-1 { background: #B0774A; } .nd-gt-1.cur { --nd-gt-ring: rgba(176,119,74,.30); }
.nd-gt-2 { background: #9CA3AB; } .nd-gt-2.cur { --nd-gt-ring: rgba(156,163,171,.34); }
.nd-gt-3 { background: #D4AF37; } .nd-gt-3.cur { --nd-gt-ring: rgba(212,175,55,.32); }
.nd-gt-4 { background: #D8DCE0; } .nd-gt-4.cur { --nd-gt-ring: rgba(150,160,170,.34); }
.nd-gt-5 { background: #7FD4E8; } .nd-gt-5.cur { --nd-gt-ring: rgba(127,212,232,.40); }
.nd-arc-count { font-size: .85rem; color: #52606d; white-space: nowrap; flex: 0 0 auto; }
.rp-badge-text { display: flex; flex-direction: column; line-height: 1.05; gap: 8px; }
.rp-badge-kicker { font-size: 10px; color: #aecbe8; letter-spacing: .2px; }
.rp-badge-name { font-size: 15px; font-weight: 700; }
.rp-appheader-bar { height: 4px; background: var(--rp-accent-bright); }

/* Right side of the header: level badge + Sign Out. */
.rp-header-right { display: flex; align-items: center; gap: 14px; }

/* Account menu — the level badge (.rp-levelbadge) is the trigger; Home projects this in via SectionOutlet. */
.acct { position: relative; }
.acct-trigger { color: #fff; font: inherit; cursor: pointer; min-height: 44px; text-align: left; }
.acct-trigger:hover { background: rgba(255, 255, 255, .18); }
.acct-trigger:focus-visible { outline: 3px solid var(--rp-accent-bright); outline-offset: 2px; }
.acct-caret { flex: 0 0 auto; margin-left: 2px; transition: transform .15s ease; }
.acct-trigger[aria-expanded="true"] .acct-caret { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .acct-caret { transition: none; } }

.acct-menu { background: #fff; color: #22303c; border-radius: 12px; box-shadow: 0 16px 40px rgba(0, 0, 0, .25); padding: 6px; min-width: 380px; z-index: 1000; }
.acct-item { display: flex; align-items: center; min-height: 44px; padding: 0 14px; border-radius: 8px; font-size: .95rem; color: #22303c; cursor: pointer; }
.acct-item:hover, .acct-item.active { background: #eef2f5; }
/* Divider line between entries (not before the first). */
.acct-item + .acct-item { border-top: 1px solid #e6ebef; }
/* Sign Out is a normal-looking entry — no red text, no red hover (danger styling removed by request). */
.acct-item.danger:hover, .acct-item.danger.active { background: #eef2f5; }
/* One highlight at a time: while the mouse is over an item, drop the keyboard-active tint from every
   OTHER item, so hover and roving focus never both show. */
.acct-menu:has(.acct-item:hover) .acct-item.active:not(:hover) { background: transparent; }
/* Passage Reports — a modal picker of completed passages, opened from the account menu. Reuses the
   .rp-modal chrome (padding zeroed here); a header + scrolling list + footer, so any number of
   passages stays contained. */
/* Selector carries BOTH classes so it beats the later, equal-specificity `.rp-modal` base (which sets
   max-width:400px + padding:24px) — otherwise the dialog stays 400px no matter what max-width is set here. */
.rp-modal.acct-preport { max-width: 500px; padding: 0; overflow: hidden; display: flex; flex-direction: column; max-height: 80vh; }
.acct-preport-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 18px 20px 14px; border-bottom: 1px solid var(--rp-line); }
.acct-preport .acct-preport-head h2 { margin: 0; font-size: 1.2rem; color: var(--rp-head); }
.acct-preport-close { flex: 0 0 auto; width: 34px; height: 34px; display: flex; align-items: center; justify-content: center; border: 0; border-radius: 8px; background: transparent; color: #52606d; font-size: 1rem; cursor: pointer; }
.acct-preport-close:hover { background: #eef2f5; }
.acct-preport-close:focus-visible { outline: 3px solid var(--rp-accent-bright); outline-offset: 2px; }
.acct-preport-list { flex: 1 1 auto; overflow-y: auto; overscroll-behavior: contain; padding: 6px; }
.acct-preport-row { display: flex; align-items: center; gap: .7rem; width: 100%; text-align: left; padding: 10px 12px; border: 0; border-radius: 10px; background: transparent; color: inherit; font: inherit; cursor: pointer; }
.acct-preport-row:hover { background: #eef2f5; }
.acct-preport-row:focus-visible { outline: 3px solid var(--rp-accent-bright); outline-offset: -2px; }
.acct-preport .acct-preport-foot { margin: 0; padding: 12px 20px calc(16px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid var(--rp-line); color: var(--rp-muted); font-size: .82rem; text-align: center; }
/* Passage row internals: big number · two-line body (title over source type) · band chip. */
.acct-pnum { color: #52606d; font-size: 1.3rem; font-weight: 700; min-width: 1.9rem; text-align: left; flex-shrink: 0; }
.acct-pbody { flex: 1; min-width: 0; overflow: hidden; display: flex; flex-direction: column; line-height: 1.25; }
.acct-ptitle { font-weight: 600; color: var(--rp-ink); font-size: .95rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.acct-pgenre { color: var(--rp-muted); font-weight: 400; font-size: .78rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.acct-preport-row .band-pill { flex-shrink: 0; margin-left: .9rem; }

/* Passage List — full-page index of completed passages (account menu → Passage Reports). Rows reuse
   the .acct-preport-row look; the page adds a header, card borders, and a Back button. */
.plist-head { margin-bottom: 1rem; }
.plist-head h2 { margin: 0 0 .2rem; color: var(--rp-head); }
.plist-sub { margin: 0; color: var(--rp-muted); font-size: .9rem; }
.plist-rows { display: flex; flex-direction: column; gap: 6px; margin-bottom: 1.5rem; }
.plist-row { border: 1px solid var(--rp-line); background: #fff; }
.plist-row:hover { background: #f5f8fb; }

.acct-backdrop { display: none; }

/* Desktop: dropdown anchored under the badge, right-aligned to it. */
@media (min-width: 641px) { .acct-menu { position: absolute; top: calc(100% + 8px); right: 0; } }
/* Mobile: bottom sheet + scrim (reuses the modal backdrop tone). */
@media (max-width: 640px) {
    .acct-menu { position: fixed; left: 0; right: 0; bottom: 0; min-width: 0; border-radius: 16px 16px 0 0; padding: 8px 8px calc(8px + env(safe-area-inset-bottom, 0px)); box-shadow: 0 -8px 30px rgba(0, 0, 0, .3); }
    .acct-item { min-height: 52px; font-size: 1rem; }
    .acct-backdrop { display: block; position: fixed; inset: 0; background: rgba(15, 28, 43, .55); z-index: 999; }
}

/* "Are you sure?" confirmation dialog. */
.rp-modal-backdrop { position: fixed; inset: 0; background: rgba(15, 28, 43, .55); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 1rem; }
.rp-modal { background: #fff; color: #22303c; width: 100%; max-width: 400px; border-radius: 14px; padding: 24px 24px 20px; box-shadow: 0 20px 50px rgba(0, 0, 0, .3); }
.rp-modal h2 { margin: 0 0 8px; font-size: 1.25rem; color: var(--rp-head); }
.rp-modal p { margin: 0 0 20px; line-height: 1.5; }
.rp-modal-actions { display: flex; justify-content: flex-end; gap: 10px; }
.rp-modal-actions button { font-weight: 700; padding: 9px 16px; border-radius: 8px; cursor: pointer; }
/* THE EDGE, not the fill (2026-09-15). This button had no perceivable boundary: a #eef2f5 fill is 1.05:1
   against the dialog's white and the #d5dee6 border was 1.36:1, against the 3:1 that 1.4.11 asks of the
   visual information identifying a control. It was the safe half of a two-button confirm and the half you
   could not see.
   NO BLUE-GREY IN THE OLD FAMILY REACHES 3:1 -- #8f9dab is still only 2.77 -- which is why this was logged
   as needing a treatment rather than a darker value, and why it waited. --rp-mark is that treatment: the
   token already carried exactly this bar for the chevron and the dash, so a control boundary joins them
   rather than inventing a fifth grey.
   THE FILL IS LEFT ALONE. Once the edge carries the boundary the fill is decoration, and 1.4.11 asks
   nothing of it; the label stays #22303c at 11.99:1 on that fill.
   Cancel is still visibly the QUIETER of the two -- the confirm beside it is accent-filled -- which is the
   point: prominence follows reversibility, and this dialog's action is reversible. */
.rp-modal-cancel { background: #eef2f5; border: 1px solid var(--rp-mark); color: #22303c; }
.rp-modal-cancel:hover { background: #e2e8ee; }
.rp-modal-confirm { background: var(--rp-accent); border: 1px solid var(--rp-accent); color: #fff; }
.rp-modal-confirm:hover { filter: brightness(1.08); }
.rp-modal-actions button:focus-visible { outline: 3px solid var(--rp-accent-bright); outline-offset: 2px; }

.rp-appfooter { flex-shrink: 0; background: #1b3a57; color: #fff; text-align: center; padding: 22px 1.25rem 26px; border-top: 4px solid var(--rp-accent-bright); }
.rp-elogo-img { width: 160px; height: auto; display: inline-block; }
.rp-copy { font-size: 13px; color: #c7d6e6; margin-top: 12px; line-height: 1.6; }

.app-header h1 { font-size: 1.6rem; color: var(--rp-head); margin: .25rem 0 1rem; }
.app-header .subtle { font-size: .95rem; color: var(--rp-muted); font-weight: 400; }

.loading { color: var(--rp-muted); }
.lede { color: #52606d; margin-bottom: 1rem; }

/* ---- story list ---- */
.story-list { display: flex; flex-direction: column; gap: .75rem; max-width: 640px; }
.story-card {
    display: flex; align-items: center; justify-content: space-between;
    text-align: left; padding: 1rem 1.25rem; border: 1px solid var(--rp-line);
    border-radius: 12px; background: #fff; cursor: pointer; transition: all .12s ease;
}
.story-card:hover { border-color: var(--rp-accent); box-shadow: 0 2px 10px rgba(43,108,176,.12); transform: translateY(-1px); }
/* Completed mission row: play card + status (band + on-demand report link) */
.story-row { display: flex; align-items: stretch; gap: .6rem; }
.story-row > .story-card { flex: 1; }
.story-status { display: flex; flex-direction: column; align-items: flex-end; justify-content: center; gap: .25rem; flex: 0 0 auto; }
.btn-link { background: none; border: none; color: var(--rp-accent); cursor: pointer; font-size: .82rem; font-weight: 600; padding: 0; white-space: nowrap; }
.btn-link:hover { text-decoration: underline; }
.story-title { font-size: 1.1rem; font-weight: 600; color: var(--rp-head); }
.story-genre { font-size: .78rem; text-transform: uppercase; letter-spacing: .04em; padding: .2rem .55rem; border-radius: 999px; }
.genre-fiction { background: #e8f0fe; color: #1a56b8; }
.genre-nonfiction { background: #e6f4ea; color: #1e7a34; }

/* ---- two-column player ---- */
.passage-col { position: relative; }
@media (min-width: 992px) {
    .passage-col > .passage { position: sticky; top: 1rem; }
}
.passage {
    background: var(--rp-bg-soft); border: 1px solid var(--rp-line); border-radius: 12px;
    /* Left gutter reduced ~40% (1.25rem -> .75rem) for both passage displays (reading + question). */
    padding: 1.1rem 1.25rem 1.1rem .75rem; max-height: calc(100vh - 8rem); overflow-y: auto;
}
/* Reading phase: full-width story, natural page flow (no sticky, no inner scroll box) */
.passage-full { position: static; }
.passage-full > .passage { position: static; max-height: none; overflow: visible; }

/* ---- reference tabs (question phase) ----
   The passage and the second source share one column as tab panels. Stacking them left the source in
   whatever space remained below a viewport-height passage — measured at 1280x800 the poem was 1%
   visible at rest, and scrolling to it pushed the question stem off the top. The STACK is what sticks
   and scrolls now, so whichever panel is showing gets the column's whole height. */
.ref-stack { display: flex; flex-direction: column; gap: .5rem; }
@media (min-width: 992px) {
    .ref-stack { position: sticky; top: 1rem; }
    /* the stack owns the sticky behaviour; the inner passage must not also claim it */
    .passage-col .ref-stack > .ref-panel > .passage { position: static; }
}
.ref-tabs { display: flex; gap: .3rem; }
.ref-tab {
    flex: 1; padding: .45rem .8rem; font-size: .9rem; font-weight: 600; cursor: pointer;
    color: #52606d; background: #fff; border: 1px solid var(--rp-line);
    border-radius: 8px 8px 0 0; border-bottom-color: transparent;
}
.ref-tab:hover { color: var(--rp-accent); }
.ref-tab.on { color: var(--rp-head); background: var(--rp-bg-soft); border-color: var(--rp-line); border-bottom-color: var(--rp-bg-soft); }
.ref-tab:focus-visible { outline: 3px solid var(--rp-accent); outline-offset: -3px; }
/* One scroll container per panel. The inner .passage keeps its styling but gives up its own
   max-height/overflow, or the student gets two nested scrollbars in the same column. */
.ref-panel { max-height: calc(100vh - 10rem); overflow-y: auto; }
.ref-panel > .passage { max-height: none; overflow: visible; }
@media (max-width: 991.98px) {
    /* No sticky below the lg breakpoint, so cap the panel shorter — a full-height reference block
       would push the question more than a screen down, which is what made the phone unusable. */
    .ref-panel { max-height: 45vh; }
}
/* Action row: primary on the left, "Pause and Finish Later" pushed to the far right by its own
   margin-left:auto. They are deliberately NOT adjacent — pause ejects the student from the mission,
   so it should not sit a few pixels from the button they are repeatedly clicking to advance. */
.reading-actions { margin-top: 1.25rem; display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.reading-actions .btn-next { margin-top: 0; }
.passage-title { font-size: 1.15rem; color: var(--rp-head); margin: 0 0 .6rem; }

/* ---- Reading-phase cover band: navy header with a genre chip, the title, and a source-type motif.
   Full-bleed to the .passage card edges via negative margins; motif is a low-opacity watermark. ---- */
.passage-cover > .pcover {
    position: relative; overflow: hidden; background: var(--rp-head);
    margin: -1.1rem -1.25rem 1.1rem; padding: 1.05rem 1.25rem 1.1rem; border-radius: 11px 11px 0 0;
}
.pcover-body { position: relative; z-index: 1; }
.pcover-chip {
    display: inline-block; background: var(--rp-accent-bright); color: #04252e;
    font-size: .8rem; font-weight: 600; padding: .18rem .7rem; border-radius: 999px; margin: 0 0 .5rem;
}
.pcover-title { color: #fff; font-size: 1.5rem; line-height: 1.2; margin: 0; }
.pcover-title:focus, .pcover-title:focus-visible { outline: none; }
.pcover-motif { position: absolute; right: -6px; top: -4px; width: 172px; height: auto; opacity: .16; z-index: 0; }
/* Drop cap on the opening paragraph of the reading cover. */
.passage-cover .para-row:first-of-type .para-text::first-letter {
    float: left; font-size: 2.9rem; line-height: .74; font-weight: 700;
    color: var(--rp-accent); padding: .1rem .55rem 0 0;
}
/* Paragraph numbering — format B: left-margin, baseline, high-contrast (not superscript/color-only) */
.para-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: .6rem; margin: 0 0 .55rem; padding: .15rem .4rem; border-radius: 6px; transition: background .15s ease; }
.para-num { flex: 0 0 1.6rem; order: 0; text-align: right; font-weight: 700; color: #52606d; font-variant-numeric: tabular-nums; }
.para-text { flex: 1; order: 2; margin: 0 1.5rem 0 0; line-height: 1.75; }
.para-highlight { background: #fff3cd; }
/* order: 3 keeps the paragraph 🔊 visually on the RIGHT even though it comes
   before the text in the DOM (so tab order is: paragraph audio → its vocab word). */
.para-listen { flex: 0 0 auto; order: 3; border: none; background: transparent; cursor: pointer; font-size: 1rem; line-height: 1; padding: .15rem .3rem; border-radius: 6px; }
.para-listen:hover { background: #e6effb; }
.para-listen.on { background: var(--rp-accent); color: #fff; }
/* Per-paragraph Spanish (Support ≥ Vocab+Story): "ES" toggle sits by the 🔊, panel wraps below */
.para-es-toggle { flex: 0 0 auto; order: 4; border: 1px solid var(--rp-line); background: #fff; color: var(--rp-accent); cursor: pointer; font-size: .72rem; font-weight: 700; line-height: 1; padding: .2rem .4rem; border-radius: 6px; }
.para-es-toggle:hover { background: #eef4ff; border-color: var(--rp-accent); }
.para-es-toggle.on { background: var(--rp-accent); color: #fff; border-color: var(--rp-accent); }
/* UNIFORM BORDER (Ron, 2026-09-16), the standing rule: same border on all four sides, and louder is a
   border COLOUR rather than one side's width. Same shape as .rr-nextstep the same day -- the three quiet
   sides were --rp-line at 1.31:1 on white, so the 3px accent left edge WAS the border, and dropping it
   alone would have left an almost edgeless panel. The accent moved to all four sides: 5.13:1 against the
   page, 4.77:1 against the panel's own fill.
   The radius was already uniform here. */
.para-es-panel { flex: 0 0 100%; order: 5; margin: .1rem 0 .2rem 2.2rem; padding: .5rem .8rem; background: #f2f7ff; border: 1px solid var(--rp-accent); border-radius: 8px; }
.para-es-bar { display: flex; align-items: center; gap: .5rem; margin-bottom: .2rem; }
/* Head of a revealed Spanish block: the Spanish 🔊 leads the "Español" label (speaker at the beginning
   of the Spanish text). */
.para-es-head { display: flex; align-items: center; gap: .35rem; margin-bottom: .2rem; }
.para-es-listen { flex: 0 0 auto; display: inline-flex; align-items: center; gap: .3rem; border: none; background: transparent; color: var(--rp-accent); cursor: pointer; font-size: 1rem; line-height: 1; padding: .1rem .35rem; border-radius: 6px; }
.para-es-listen:hover { background: #e2ecfb; }
.para-es-listen.on { background: var(--rp-accent); color: #fff; }
/* the "Español" label rides INSIDE the button, so clicking it plays too; keep it readable while playing */
.para-es-listen.on .para-es-label { color: #fff; }
.para-es-label { font-size: .75rem; font-weight: 700; color: var(--rp-accent); text-transform: uppercase; letter-spacing: .03em; }
.para-es-text { flex: 1; order: 2; margin: 0; line-height: 1.6; color: #3e4c59; font-style: italic; }

/* Reusable actions menu (⋯) — shown when an item has 2+ actions (story paragraphs, vocab words) */
.am { position: relative; order: 3; display: inline-flex; }
.vocab-term .am { margin-left: auto; }
.am-btn { border: 1px solid var(--rp-line); background: #fff; color: var(--rp-accent); cursor: pointer; font-size: 1rem; line-height: 1; padding: .1rem .45rem; border-radius: 6px; }
.am-btn:hover { background: #eef4ff; border-color: var(--rp-accent); }
.am-btn.on { background: var(--rp-accent); color: #fff; border-color: var(--rp-accent); }
.am-menu { position: absolute; top: 100%; right: 0; z-index: 40; min-width: 210px; margin-top: 4px; background: #fff; border: 1px solid var(--rp-line); border-radius: 10px; box-shadow: 0 8px 24px rgba(31, 56, 100, .18); overflow: hidden; outline: none; }
.am-item { display: flex; align-items: center; gap: .55rem; padding: .5rem .7rem; cursor: pointer; font-size: .95rem; color: var(--rp-ink); }
.am-item + .am-item { border-top: 1px solid var(--rp-line); }
.am-ico { width: 1.2rem; text-align: center; flex: 0 0 auto; }
.am-item.active { background: #eef4ff; }
.am-item.on { font-weight: 700; color: var(--rp-accent); }
.listen-bar { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin: 0 0 1.4rem; }
.listen-bar .btn-secondary.on { background: var(--rp-accent); color: #fff; }
/* The parent's "Next" button sits at the RIGHT end of the top control row (no scrolling to reach it). */
.bar-actions { margin-left: auto; display: flex; align-items: center; gap: .5rem; }
.bar-actions .btn-next { margin-top: 0; }

/* In-passage vocabulary words (bolded; signal "defined term") */
.vocab-mark { font-weight: 700; color: var(--rp-head); }
.vocab-inline { position: relative; }
/* Interactive term (reading phase): Model-B — opens a definition bubble at the word */
.vocab-mark-btn { font: inherit; font-weight: 700; color: var(--rp-accent); background: none; border: 0; padding: 0 1px; margin: 0; cursor: pointer; border-bottom: 2px dotted var(--rp-accent); }
.vocab-mark-btn:hover { background: #eef4ff; }
.vocab-mark-btn:focus-visible { outline: 2px solid var(--rp-accent); outline-offset: 2px; border-radius: 2px; }
.vocab-pop { position: absolute; top: 1.7em; left: 0; z-index: 30; display: block; min-width: 220px; max-width: 320px;
    background: #fff; border: 1px solid var(--rp-accent); border-radius: 10px; box-shadow: 0 8px 24px rgba(31, 56, 100, .18);
    padding: .55rem .7rem; font-weight: 400; line-height: 1.45; color: var(--rp-ink); white-space: normal; }
.vocab-pop-head { display: flex; align-items: center; gap: .4rem; margin-bottom: .2rem; }
.vocab-pop-audio { display: inline-flex; }
.vocab-pop-word { font-weight: 700; color: var(--rp-head); }
.vocab-pop-def { display: block; color: #3e4c59; }

/* ===== Spanish home-language support (muted, secondary to the English) ===== */

/* Preview card */
.vocab-es { display: flex; align-items: baseline; gap: .4rem; flex-wrap: wrap; margin: .2rem 0 0; color: #52606d; font-style: italic; }
.vocab-es-term { font-weight: 700; font-style: normal; color: var(--rp-accent); }
.vocab-es .listen-btn { font-style: normal; }
.vocab-cognate, .vocab-false { font-size: .9rem; margin: .2rem 0 0; color: #52606d; }
.cognate-badge { display: inline-block; background: var(--rp-good-bg); color: var(--rp-good); font-weight: 700; border-radius: 6px; padding: 0 .35rem; margin-right: .25rem; }
.false-badge { display: inline-block; background: var(--rp-hint-bg); color: #8a6d3b; font-weight: 700; border: 1px solid var(--rp-hint-line); border-radius: 6px; padding: 0 .35rem; margin-right: .25rem; }

/* Inline bubble */
.vocab-pop-es { display: block; margin-top: .35rem; padding-top: .35rem; border-top: 1px dashed var(--rp-line); color: #52606d; font-style: italic; }
.vocab-pop-es-term { font-weight: 700; font-style: normal; color: var(--rp-accent); margin-right: .3rem; }
.vocab-pop-tag { display: inline-block; margin-top: .25rem; font-size: .8rem; font-weight: 700; font-style: normal; border-radius: 6px; padding: 0 .35rem; }
.vocab-pop-tag.is-cognate { background: var(--rp-good-bg); color: var(--rp-good); }
.vocab-pop-tag.is-false { background: var(--rp-hint-bg); color: #8a6d3b; border: 1px solid var(--rp-hint-line); }

.glossary { margin-top: .9rem; border: 1px solid var(--rp-line); border-radius: 10px; background: #fff; padding: .5rem .9rem; }
.glossary summary { font-weight: 600; color: var(--rp-head); cursor: pointer; }
.glossary dl { margin: .5rem 0 0; }
.glossary dt { font-weight: 700; margin-top: .4rem; }
.glossary dd { margin: 0 0 .2rem; color: #52606d; }

/* vocabulary entries (used in both the glossary and the preview) */
.vocab-term { display: flex; align-items: center; gap: .5rem; font-weight: 700; color: var(--rp-head); margin-top: .5rem; }
.vocab-term .listen-btn { margin-left: auto; }
.vocab-def { margin: 0; color: #334155; }
.vocab-sample { margin: 0 0 .4rem; color: #64748b; font-style: italic; font-size: .92rem; }
.vocab-eg { font-style: normal; font-weight: 600; color: var(--rp-muted); }

/* vocabulary preview (pre-teach screen shown before the story) */
.vocab-preview { max-width: 760px; }
.vocab-actions { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.vocab-actions .btn-next { margin-top: 0; }   /* else the primary button sits lower than Back (its own stacked-layout margin) */
.vocab-preview h2 { color: var(--rp-head); }
/* Intro is a programmatic focus target (announced on arrival), not a keyboard stop → no focus ring */
.vocab-intro:focus { outline: none; }
.vocab-list { margin: 1rem 0 1.25rem; list-style: none; padding: 0; }
.vocab-entry { padding: .6rem .9rem; border: 1px solid var(--rp-line); border-radius: 10px; background: var(--rp-bg-soft); margin-bottom: .6rem; }
.vocab-entry .vocab-term { margin-top: 0; font-size: 1.08rem; }

/* ---- questions ---- */
.progress-line { font-weight: 600; color: #52606d; margin-bottom: .75rem; display: flex; align-items: center; gap: .75rem; }
.progress-bar-track { flex: 1; height: 6px; background: var(--rp-line); border-radius: 999px; overflow: hidden; }
.progress-bar-fill { display: block; height: 100%; background: var(--rp-accent); transition: width .25s ease; }

.q-stem { display: flex; align-items: flex-start; gap: .6rem; font-size: 1.1rem; font-weight: 600; margin-bottom: .9rem; }
.q-stem > span { flex: 1; }
/* Stem is a programmatic focus target (tabindex=-1, not keyboard-tabbable), so no focus ring. */
.q-stem:focus { outline: none; }
/* Two-part item: "Part A" / "Part B" label above the question */
.q-part-label { font-size: .8rem; font-weight: 700; color: var(--rp-accent); text-transform: uppercase; letter-spacing: .03em; margin: 0 0 .5rem; }

/* reusable listen button (vocab words + questions) */
.listen-btn { flex: 0 0 auto; border: 1px solid var(--rp-line); background: #fff; cursor: pointer; font-size: .85rem; line-height: 1; padding: .2rem .45rem; border-radius: 8px; color: var(--rp-accent); }
.listen-btn:hover { background: #eef4ff; border-color: var(--rp-accent); }
.listen-btn.on { background: var(--rp-accent); color: #fff; border-color: var(--rp-accent); }
.listen-lang { margin-left: .3em; font-size: .68rem; font-weight: 600; letter-spacing: .02em; vertical-align: 1px; }
/* Spanish question support (Support = Spanish+Questions) — reveal on demand; content-pending. */
/* Question controls on their own left row above the stem — the Listen speaker is dressed to match the
   passage/poem .btn-secondary sitting next to it (fills navy while playing). */
.q-controls { margin: 0 0 .9rem; }
.q-controls .listen-btn { display: inline-flex; align-items: center; gap: .35rem; border: 1.5px solid var(--rp-accent); background: #fff; color: var(--rp-accent); font-size: .95rem; font-weight: 600; line-height: 1.5; padding: .5rem 1rem; border-radius: 10px; cursor: pointer; }
.q-controls .listen-btn:hover { background: #f0f6ff; }
.q-controls .listen-btn.on { background: var(--rp-accent); color: #fff; }
/* The base .listen-lang is a tiny .68rem "EN/ES" tag; here "Listen" is the button's full label, so
   render it at full button size to match the .btn-secondary next to it. */
.q-controls .listen-lang { font-size: .95rem; font-weight: 600; letter-spacing: normal; margin-left: 0; vertical-align: baseline; }
/* Spanish reveal — the WHOLE question repeated below (stem + lettered options) as ONE reference panel,
   not co-mingled into the option buttons. Styled like the poem's .para-es-panel; one "🔊 Español" speaker. */
/* UNIFORM BORDER (Ron, 2026-09-16), the standing rule: same border on all four sides, and louder is a
   border COLOUR rather than one side's width. Same shape as .rr-nextstep the same day -- the three quiet
   sides were --rp-line at 1.31:1 on white, so the 3px accent left edge WAS the border, and dropping it
   alone would have left an almost edgeless panel. The accent moved to all four sides: 5.13:1 against the
   page, 4.77:1 against the panel's own fill.
   THE RADIUS WENT UNIFORM TOO: it was `0 8px 8px 0`, square on the left so the corners sat flush
   against the accent bar. With no bar to sit against, square left corners are just an unexplained asymmetry. */
.q-es-panel { margin: 1rem 0 .4rem; padding: .7rem .85rem; background: #f2f7ff; border: 1px solid var(--rp-accent); border-radius: 8px; }
.q-es-panel .listen-btn { display: inline-flex; align-items: center; gap: .3rem; border: none; background: transparent; color: var(--rp-accent); font-weight: 600; cursor: pointer; padding: .1rem .2rem; margin-bottom: .45rem; }
.q-es-panel .listen-btn.on { background: var(--rp-accent); color: #fff; border-radius: 6px; }
.q-es-panel .listen-lang { font-size: .95rem; font-weight: 600; letter-spacing: normal; margin-left: 0; vertical-align: baseline; }
.q-es-stem { margin: 0 0 .5rem; font-weight: 600; font-style: italic; color: #2a3542; }
.q-es-options { display: flex; flex-direction: column; gap: .3rem; }
.q-es-opt { color: #3e4c59; font-style: italic; line-height: 1.5; }
.q-es-letter { font-weight: 700; font-style: normal; color: var(--rp-accent); margin-right: .25rem; }
.q-options { display: flex; flex-direction: column; gap: .55rem; }
.q-option {
    display: flex; align-items: flex-start; gap: .6rem; text-align: left;
    padding: .7rem .9rem; border: 1.5px solid var(--rp-line); border-radius: 10px;
    background: #fff; cursor: pointer; font-size: 1rem; transition: all .1s ease;
}
.q-option:hover:not(:disabled) { border-color: var(--rp-accent); background: #f5f9ff; }
.q-option:disabled { cursor: default; }
.q-letter { font-weight: 700; color: var(--rp-accent); }
.q-option.correct { border-color: var(--rp-good); background: var(--rp-good-bg); }
.q-option.correct .q-letter { color: var(--rp-good); }
.q-option.wrong { border-color: var(--rp-bad); background: var(--rp-bad-bg); opacity: .8; }
.q-option.wrong .q-letter { color: var(--rp-bad); }
/* ✓ / ✗ shape cue (bold + larger) so correct/wrong isn't color-only — WCAG 1.4.1 */
.q-letter.mark-ok, .q-letter.mark-no { font-size: 1.25rem; font-weight: 700; line-height: 1; }
.q-letter.mark-ok { color: var(--rp-good); }
.q-letter.mark-no { color: var(--rp-bad); }

.panel { margin-top: .8rem; padding: .7rem .9rem; border-radius: 10px; line-height: 1.55; }
.panel.hint { background: var(--rp-hint-bg); border: 1px solid var(--rp-hint-line); }
.panel.explain { background: #eef2ff; border: 1px solid #c7d2fe; }
.panel.correct { background: var(--rp-good-bg); border: 1px solid #a7d7b4; color: #14532d; }

/* LINE-HEIGHT IS PINNED (2026-09-15), for the reason the .btn-sm rule below already gives: a button
   otherwise inherits `normal`, which varies by font, so the height is an approximation rather than a number
   anyone can rely on. This was the one button left unpinned -- and it is the STUDENT's main button, the one
   a child presses on every screen, which made it the wrong one to leave as an estimate.
   1.25rem = 20px, so the button is 20 + 2 x .6rem = 39.2px. Chosen to sit at or just above what `normal`
   gives a 16px system font (~1.15-1.2), so nothing shrinks and no label can clip. In rem, never px: WCAG
   1.4.4 Resize Text means the box has to grow with the reader's default font size. */
.btn-next {
    margin-top: 1.1rem; padding: .6rem 1.1rem; border: none; border-radius: 10px;
    background: var(--rp-accent); color: #fff; font-size: 1rem; line-height: 1.25rem;
    font-weight: 600; cursor: pointer;
}
/* THE TEACHER BUTTON SIZE — 32px tall (Ron, 2026-08-22) = 1rem line-height + .5rem padding top and bottom.
   Declared ONCE and applied in the markup as `btn-next btn-sm`. It previously lived in four separate scoped
   rules that had each converged on the same values independently; changing the size meant four edits, and a
   missed one is how a button quietly drops below the accessibility floor.

   line-height is PINNED because a button otherwise inherits `normal`, which varies by font — the height
   would be approximate rather than a number you can rely on.

   Expressed in rem, never a fixed height. The target is WCAG 2.2 AA (docs/Accessibility_Rules.md), which
   includes 1.4.4 Resize Text: a px height clips its own label when a low-vision user raises their default
   font size, whereas padding grows with the text. 32px also sits well clear of the 24px floor in 2.5.8
   Target Size (Minimum).

   TEACHER SURFACES ONLY. The student app keeps full-size .btn-next — that is what a child taps.

   THE SECOND SELECTOR IS THE SHARED REPORTS (Ron, 2026-09-01). The Certification Report, the Progress Report,
   one passage and the certificate are rendered verbatim for BOTH audiences, so they cannot carry `btn-sm` in
   their own markup without shrinking the child's buttons. `.tc-drill` wraps them only on the teacher path, so
   the host decides the size and the components stay audience-agnostic — the same seam AdultContext uses.
   Declared as one selector list rather than a copied block: the sizes were four converged copies once already,
   and the whole point of this rule is that changing 32px is one edit. */
   THE THIRD SELECTOR IS EVERY DIALOG FOOTER (Ron, 2026-09-14). Dialog buttons had their own rule at 28px
   and now share this one. They were sized the same DAY as the 32px above and never compared to it: the
   argument recorded on that rule was 24-vs-28 -- whether the WCAG 2.2 AA 2.5.8 floor left enough headroom --
   so the dialog button was sized against a LEGAL MINIMUM rather than against this product's own button, and
   nobody ever asked 28-vs-32. A teacher clicking Save on the roster page got 32px and Save in a dialog got
   28px, for no stated reason. The two rules also differed in exactly ONE value -- vertical padding, .375rem
   against .5rem -- with identical font-size, line-height, horizontal padding and radius, which is what made
   folding them a rounding rather than a redesign.
   32px keeps 8px above the 2.5.8 floor instead of 4, and matches the macOS and Fluent default button. */
.btn-sm,
.tc-drill .btn-next { font-size: .8rem; line-height: 1rem; padding: .5rem .88rem; border-radius: 8px; }

/* Same 32px, reached differently: footer buttons reserve a 1.5px border box so .btn-danger can COLOUR a
   border instead of adding one. Without the transparent border the outlined Delete would come out 35px
   against a 32px Cancel, and the padding is reduced by exactly the border width so the height is unchanged
   for every other button. Declared here rather than on .btn-danger so the two can never disagree. */
.rr-modal-actions .btn-next { font-size: .8rem; line-height: 1rem; border-radius: 8px;
                              border: 1.5px solid transparent;
                              padding: calc(.5rem - 1.5px) calc(.88rem - 1.5px); }

/* THE DESTRUCTIVE BUTTON (Ron, 2026-09-15). Delete and Cancel were both the same blue: two identical
   buttons side by side, no weight on either, on the one dialog where getting it wrong cannot be undone.

   QUIET DANGER, NOT LOUD DANGER -- outlined red on white, not filled red. Filling it would make the
   destructive button the most prominent thing in the dialog, which fights the rule that already protects
   the safe choice by POSITION ("the right-most control is Cancel"). Colour marks the danger; it does not
   invite the click. Cancel keeps its filled blue and stays the visually obvious way out.

   #b3261e is the app's existing error red, not a new colour: 6.54:1 as a label on white (1.4.3 wants 4.5)
   and 6.54:1 as a border (1.4.11 wants 3). And colour is never the only signal -- the button says "Delete"
   (docs/Accessibility_Rules.md).

   PROMINENCE FOLLOWS REVERSIBILITY, which is why the student's sign-out confirm looks different and is
   still right: signing out is reversible ("your progress is saved"), so its action is the prominent one.
   These deletes are not. */
/* A BODY, NOT JUST AN OUTLINE (Ron, 2026-09-15). Both of these were white with a coloured border, and a
   white button on a white dialog leans entirely on a 1.5px line — it passes 1.4.11 at 3.20:1 and still does
   not read as a button. Ron spotted it on the STUDENT app's Cancel, which has had a grey fill all along and
   is the older, better design; the teacher app is the one that drifted.

   The tell on .btn-danger was sharper: it already HAD a tint, but only on :hover. A button whose resting
   state is white and whose hover invents a body has it backwards — hover should deepen a body, not create
   one.

   NO NEW COLOURS. The grey is lifted from .rp-modal-cancel; --rp-bad-bg is already in the palette
   (.q-option.wrong, .band-beginning).

   THE FILLS ARE DECORATION, both ~1.15:1 against white, and the BORDERS still carry the compliance —
   #86919f at 3.20:1 and #b3261e at 6.54:1 against the dialog behind them. Body AND boundary, not one
   standing in for the other. Labels measured on their own fills: #52606d on grey is 5.74:1, #b3261e on the
   red tint 5.58:1, both past 1.4.3's 4.5.

   The tier ordering survives: filled accent shouts, grey has weight, red has weight and reads as a warning.
   Danger is still not INVITED — its colour names it — it just no longer looks unfinished. */
.rr-modal-actions .btn-danger { background: var(--rp-bad-bg); border-color: #b3261e; color: #b3261e; }
.rr-modal-actions .btn-danger:hover { background: #f7dcda; border-color: #b3261e; color: #b3261e; }

/* THE QUIET BUTTON — the way OUT of a dialog (Ron, 2026-09-15). Cancel and "Back" were filled accent,
   identical to Save and Create beside them: two loud buttons giving no visual answer to "what did I come
   here to do". It was invisible until .btn-danger made one button mean something, and then the flatness of
   the rest was the obvious thing on the screen.

   THREE TIERS NOW, and one rule: exactly one FILLED button per footer, the action you opened the dialog for;
   .btn-quiet for the way out; .btn-danger for irreversible.

   THE EXCEPTION IS DELIBERATE AND IS THE INTERESTING PART: in the two DELETE CONFIRMS, Cancel stays FILLED.
   Delete is quiet red and Cancel is the loud one, because prominence follows reversibility -- a destructive
   confirm is the one place the SAFE choice should be the prominent one. So the rule reads: the primary is
   the action you came for, EXCEPT in an irreversible confirm, where the primary is the way out. That is what
   makes those two footers look deliberately different rather than accidentally odd.

   Border colour is --rp-mark, the token that already carries control boundaries at 1.4.11's 3:1; the label
   is #52606d at 6.46:1. It colours the transparent border the footer rule already reserves, so the height
   stays 32px. */
.rr-modal-actions .btn-quiet { background: #eef2f5; border-color: var(--rp-mark); color: #52606d; }
.rr-modal-actions .btn-quiet:hover { background: #e2e8ee; border-color: #52606d; color: var(--rp-head); }

/* THE TWO ADULT SIZES, and the rule that chooses between them (Ron, 2026-09-14). There were THREE in use --
   38px, 32px, 28px -- and no rule, which is how the size labelled "THE TEACHER BUTTON SIZE" came to cover 11
   of ~43 shipping buttons. Now:
     38px (bare .btn-next)  -- PAGE-LEVEL PRIMARY actions: Add Class, Print sign-in cards, View Certificate,
                               a Back that leaves the screen. One or two per page, and the biggest thing on it.
     32px (this rule)       -- EVERYTHING ELSE adult-facing, dialog footers included.
   Before adding a fourth size, say which of these two the control is and use it.

   margin-top: 0 clears the base button's 1.1rem, which is meant for stacked page layouts and would add
   itself INSIDE the footer's flex row on top of the 24px .rr-modal-actions already sets (flex items do not
   collapse margins). Zeroed at the base, so the 24px above the buttons is the whole gap -- it used to be
   cleared only on .rr-actions-split, which is why that one dialog was the only one with a deliberate gap. */
.rr-modal-actions .btn-next { margin-top: 0; }
.btn-next:hover { background: #245a94; }

/* ---- score summary ---- */
.score-summary h2 { color: var(--rp-head); }
.score-big { display: flex; align-items: baseline; gap: 1rem; margin: 1rem 0 1.25rem; padding: 1rem 1.25rem; border-radius: 12px; }
.score-pct { font-size: 2.4rem; font-weight: 800; }
.score-band { font-size: 1.2rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; }
.band-achieving { background: var(--rp-good-bg); color: #14532d; }
.band-approaching { background: #e8f0fe; color: #1a56b8; }
.band-emerging { background: var(--rp-hint-bg); color: #92600b; }
.band-beginning { background: var(--rp-bad-bg); color: #8a2b2b; }

.score-table { width: 100%; border-collapse: collapse; margin-bottom: 1.25rem; }
.score-table th, .score-table td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--rp-line); font-size: .95rem; }
.score-table th { color: #52606d; font-size: .8rem; text-transform: uppercase; letter-spacing: .04em; }
/* Report tables: strand name stays left; Questions/Score/Mastery centered under centered headers */
.story-report .score-table th:not(:first-child), .story-report .score-table td:not(:first-child),
.strand-table th:not(:first-child), .strand-table td:not(:first-child) { text-align: center; }
/* Report grouped by CCSS strand: strand header row + indented skill rows */
.strand-table .strand-head th, .strand-table .strand-head td { background: var(--rp-bg-soft); font-weight: 700; color: var(--rp-head); text-transform: none; font-size: .95rem; border-top: 2px solid var(--rp-line); }
.strand-table .skill-cell { padding-left: 1.4rem; color: #52606d; }
/* Certification Report — inline anchor drill-down: an anchor row expands to its contributing best-of-type
   passages (collapsed by default, so the printed certificate stays clean). */
.anchor-toggle { display: inline-flex; align-items: center; gap: .35rem; border: none; background: none; padding: 0; font: inherit; color: inherit; cursor: pointer; text-align: left; }
.anchor-toggle:hover { color: var(--rp-accent); }
.anchor-caret { display: inline-block; transition: transform .12s ease; font-size: .7rem; color: var(--rp-accent); }
/* Anchor rows carry BOTH the full skill name and a print short form; the print block swaps them. Screen is
   the wide medium and the accessible one, so it shows the full name — the short span is aria-hidden in the
   markup, so a screen reader hears the skill named once either way. */
.anchor-short { display: none; }
.anchor-caret.open { transform: rotate(90deg); }
.strand-table .anchor-contrib td { background: #f7fafd; color: #52606d; font-size: .88rem; }
.strand-table .anchor-contrib .contrib-cell { padding-left: 2.6rem; }
.contrib-type { color: var(--rp-muted); font-style: italic; margin-left: .5rem; font-size: .85rem; }

/* brand + level heading */
.app-header .tagline { color: var(--rp-accent); font-style: italic; font-weight: 600; margin: -.35rem 0 1rem; letter-spacing: .02em; }
.level-name { color: var(--rp-head); font-size: 1.3rem; margin: 0 0 .3rem; }

/* ---- simulation ---- */
.section-label { margin: 1.2rem 0 .5rem; font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: var(--rp-muted); font-weight: 700; }
.capstone-card { border: 1.5px solid var(--rp-accent); background: linear-gradient(180deg, #f5f9ff, #eef4ff); }
.genre-sim { background: #ece6ff; color: #5b3fb8; }

.sim-brief { background: #eef4ff; border: 1px solid #c7d9f5; border-radius: 12px; padding: 1rem 1.25rem; margin-bottom: 1rem; }
.sim-role { display: inline-block; font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; font-weight: 700; color: #5b3fb8; background: #ece6ff; padding: .2rem .6rem; border-radius: 999px; }
.sim-scenario { margin: .6rem 0 .4rem; line-height: 1.6; }
.sim-question { margin: 0; }
.sim-pt-note { margin: .55rem 0 0; padding-top: .55rem; border-top: 1px dashed #c7d9f5; font-size: .9rem; color: #40506b; }

/* Two-part Performance-Task framing */
.sim-part-banner { display: inline-block; font-size: .82rem; font-weight: 700; letter-spacing: .02em; color: #1f6a4d; background: #e3f5ec; border: 1px solid #b6e2cd; padding: .28rem .7rem; border-radius: 999px; margin-bottom: .7rem; }
.sim-part-break { background: #eef4ff; border: 1px solid #c7d9f5; border-radius: 12px; padding: 1.4rem 1.5rem; text-align: center; }
.sim-part-break p { margin: .7rem 0 1.1rem; line-height: 1.6; color: #2a3446; }
.sim-part-tag { display: inline-block; font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; font-weight: 700; color: #1f6a4d; background: #d8f0e3; padding: .22rem .7rem; border-radius: 999px; }

.source-packet { display: flex; flex-direction: column; gap: .8rem; max-height: calc(100vh - 6rem); overflow-y: auto; }
.packet-label { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: var(--rp-muted); font-weight: 700; }
.source { border: 1px solid var(--rp-line); border-radius: 10px; padding: .7rem .9rem; background: #fff; }
/* Cover mode (poem/source reading screen): match the passage card + navy cover band EXACTLY. Same padding,
   radius, and soft bg as .passage, and the band full-bleeds via negative margins equal to that padding, so
   the header is the same width. Reuses .pcover-chip/.pcover-title/.btn-secondary → identical fonts+buttons. */
.source.source-cover { padding: 1.1rem 1.25rem; border-radius: 12px; background: var(--rp-bg-soft); }
.source-cover > .pcover {
    position: relative; overflow: hidden; background: var(--rp-head);
    margin: -1.1rem -1.25rem 1.1rem; padding: 1.05rem 1.25rem 1.1rem; border-radius: 11px 11px 0 0;
}
.pcover-attrib { display: block; color: #cdd8ea; font-size: .82rem; font-style: italic; margin-top: .35rem; }
.source-head { display: flex; align-items: baseline; gap: .6rem; margin-bottom: .4rem; border-bottom: 1px solid var(--rp-line); padding-bottom: .35rem; }
.source-type { font-size: .68rem; text-transform: uppercase; letter-spacing: .04em; font-weight: 700; color: #fff; background: var(--rp-accent); padding: .12rem .5rem; border-radius: 999px; white-space: nowrap; }
.source-title { font-weight: 700; color: var(--rp-head); }
/* Byline / data credit. Quiet, but it must be on screen — RI.9 items depend on it. */
.source-attrib { font-size: .78rem; color: var(--rp-muted); font-style: italic; }
/* UNIFORM BORDER (Ron, 2026-09-16), the standing rule: same border on all four sides, and louder is a
   border COLOUR rather than one side’s width. The THIRD of the three Spanish reveal panels, swept with
   .para-es-panel and .q-es-panel rather than after them -- same job, same visual family, so a fix that
   reached two of three would have left the asymmetry looking deliberate.
   This was the sharpest case: it declared NO other border, so the 3px accent left edge was not the loud
   side, it was the only side. The accent moved to all four: 5.13:1 against the page (.source is #fff),
   4.89:1 against the panel’s own --rp-bg-soft fill.
   THE RADIUS WENT UNIFORM TOO: it was `0 6px 6px 0`, square on the left so the corners sat flush against
   the accent bar. With no bar to sit against, square left corners are just an unexplained asymmetry. */
.source-es-panel { margin-top: .7rem; padding: .6rem .7rem; background: var(--rp-bg-soft); border: 1px solid var(--rp-accent); border-radius: 6px; }
.chart-es-list { margin: .2rem 0 0; padding-left: 1.1rem; font-size: .9rem; }
.source .para { line-height: 1.6; margin: 0 0 .5rem; }
.tline { margin: .2rem 0; line-height: 1.5; }
/* Poem source: each authored line is its own line. Block display rather than <br> so a screen reader
   gets a real boundary between lines, and generous line-height because verse is read slowly. */
/* Second-source step: its own gated screen between the passage and the questions. */
/* LEFT-JUSTIFIED, not centred. The reading phase runs the story flush to the container's left edge, so
   a centred source column put the poem on a left margin the story never has — the two screens read as
   different layouts. The 760px cap stays (verse and short prose are unreadable at full container
   width); only the centring goes. */
.source-step { max-width: 760px; margin: 0; }
/* Same column as .source-step, so the back button starts exactly where the poem starts. Without this
   the actions spanned the full container while the source sat in its own narrower box, leaving the
   buttons visibly outdented from the text they belong to. */
.source-step-actions {
    max-width: 760px; margin: 1.25rem 0 0;
    display: flex; align-items: center; gap: .6rem;
}
/* Reading phase actions: a way out and a way on, matching the source step's shape. */
.story-actions { display: flex; align-items: center; gap: .6rem; }
.story-actions .btn-next { margin-top: 0; }
/* .btn-next carries its own top margin for stacked layouts; inside this flex row it would sit lower
   than the secondary button beside it. */
.source-step-actions .btn-next { margin-top: 0; }
.source-step-head { font-size: 1.35rem; color: var(--rp-head); margin: 0 0 .2rem; }
.source-step-head:focus-visible { outline: 3px solid var(--rp-accent); outline-offset: 4px; }
.source-step-lede { color: #52606d; font-size: .95rem; margin: 0 0 1rem; }
/* Same source, kept on screen beside the questions so an R.7 item isn't a memory test. */
.source-aside { margin-top: 1rem; padding-top: .9rem; border-top: 1px solid var(--rp-line); }

.poem { margin: .3rem 0; padding-left: .4rem; }
.poem-line { display: block; line-height: 1.9; }
.poem-line:empty { height: .9rem; }   /* an empty authored line is a stanza break */
.email-meta { font-size: .9rem; color: #52606d; margin-bottom: .4rem; }
.email-body { margin: 0; line-height: 1.6; font-style: italic; }
.post { padding: .35rem 0; border-bottom: 1px dashed var(--rp-line); line-height: 1.5; }
.post:last-child { border-bottom: none; }
.post-handle { font-weight: 700; color: var(--rp-accent); }

.chart { display: flex; flex-direction: column; gap: .35rem; }
.chart-row { display: flex; align-items: center; gap: .5rem; font-size: .9rem; }
.chart-label { width: 3.5rem; color: #52606d; }
.chart-track { flex: 1; background: #eef2f7; border-radius: 4px; height: 14px; overflow: hidden; }
.chart-bar { display: block; height: 100%; background: var(--rp-accent); }
.chart-val { width: 2.5rem; text-align: right; font-variant-numeric: tabular-nums; }
.chart-caption { font-size: .82rem; color: var(--rp-muted); margin: .4rem 0 0; font-style: italic; }

.panel.reflect { background: #f3f0ff; border: 1px solid #d6ccf5; }
.panel.reflect ul { margin: .4rem 0 0 1.1rem; }

/* ---- buttons + report ---- */
.btn-secondary { padding: .5rem 1rem; border: 1.5px solid var(--rp-accent); border-radius: 10px; background: #fff; color: var(--rp-accent); font-size: .95rem; font-weight: 600; cursor: pointer; }
.btn-secondary:hover { background: #f0f6ff; }
.summary-actions { display: flex; gap: .75rem; flex-wrap: wrap; margin-top: 1.1rem; }
.summary-actions .btn-next { margin-top: 0; }
.list-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }

.report h2 { color: var(--rp-head); }
.report h3 { color: var(--rp-head); font-size: 1.05rem; margin: 1.3rem 0 .5rem; }
.report-summary { display: flex; gap: .8rem; flex-wrap: wrap; margin: 1rem 0; }
.stat { flex: 1; min-width: 130px; background: var(--rp-bg-soft); border: 1px solid var(--rp-line); border-radius: 12px; padding: .9rem 1rem; }
.stat-num { display: block; font-size: 1.8rem; font-weight: 800; color: var(--rp-head); }
.stat-of { font-size: 1rem; font-weight: 600; color: var(--rp-muted); }
.stat-label { font-size: .82rem; color: #52606d; }
.band-pill { display: inline-block; padding: .12rem .55rem; border-radius: 999px; font-size: .78rem; font-weight: 700; }
/* Low-N safety net: an anchor band resting on fewer than the item floor (4) renders greyed + marked with
   an amber asterisk, so a band built on 1-3 questions never looks as confident as one from a real sample. */
.band-provisional { background: var(--rp-line); color: #52606d; }
.prov-mark { color: #9a5c00; font-weight: 700; margin-left: 1px; }
/* Skill-Booster placeholder marker (†) — distinct from the amber provisional * and the ** weighting note. */
.booster-mark { color: var(--rp-head); font-weight: 700; margin-left: 1px; }

/* Single-story "Reading Skills Report" */
/* All report views share one centred width — a step up from the 680px dashboard so tables (esp. the
   Progress matrix) don't wrap, but far narrower than the 1120px page the reports used to fill. */
.report { max-width: 800px; margin: 0 auto; }
.story-report { max-width: 800px; margin: 0 auto; }
/* "Your Journey" cross-level summary — a growth record on the Certification Report. */
/* 20px above (Ron, 2026-09-08, tried at 12px first and it still read tight): on screen Journey follows
   the report header .dl directly, and with no top margin its heading sat hard against the identity
   block. PRINT IS EXEMPTED below -- there Journey is a right-column grid item, not a sibling of the
   header, and the sheet fits in 988px of 988px with no headroom to give away. */
.journey { margin: 20px 0 7px; }
/* Title lives in the header band (.sbs-head) like the skills table, so no standalone heading. */
.journey-table td, .journey-table th { text-align: left; }
.journey-table th:nth-child(n+3), .journey-table td:nth-child(n+3) { text-align: center; }
/* margin-TOP is the heading-to-table gap: it collapses against the h3's smaller bottom margin and wins,
   so shrinking the heading's margin did nothing and this is the number that matters. */
.journey-table { margin-top: 14px; margin-bottom: .75rem; }   /* pull the footnote up — ~40% less than the .score-table default */
/* Indented to the table's TEXT edge, not the table's box edge: .score-table cells carry .6rem of side
   padding, so a note at 0 starts left of every value above it and reads as hanging off the section. */
.journey .report-note { padding-left: .6rem; }

.report-title { text-align: center; margin: .5rem 0 1.25rem; }
.report-title-1 { font-size: 1.5rem; font-weight: 700; color: var(--rp-head); }
.report-title-2 { font-size: 1.1rem; color: #52606d; margin-top: .15rem; }
/* TWO STACKS SIDE BY SIDE (2026-09-12), replacing a two-column grid that filled row by row. The columns are
   now declared rather than implied by DOM order, so omitting a field -- a home school has no district --
   costs its own line and nothing else. See the note on the block in ProgressReport. */
/* THE SUBSTITUTION NOTE. Bordered because it changes what the whole document means: a reader who misses it
   reads a Silver report as a Gold one. The heavier left edge is the attention, and it is a BORDER rather than
   a fill because browsers do not print background colours -- the thing that decided the band pills too.
   It is quiet in colour and loud in position: directly under the header, full width, ahead of everything it
   qualifies.
   A UNIFORM BORDER, never a heavier edge on one side (Ron, 2026-09-12 -- a standing rule). This carried a 4px
   accent down its left for one revision. The box is the attention; a thickened edge is decoration that reads
   as a status colour the app does not otherwise use, and it makes one callout shout against every other
   bordered block in the file. If this ever needs to be louder, the lever is the border COLOUR or the type,
   not the width of one side. */
.report-promoted { margin: 0 0 .9rem; padding: .5rem .8rem; font-size: .95rem; color: var(--rp-ink);
                   border: 1px solid var(--rp-line); border-radius: 6px; line-height: 1.45; }
.report-meta { display: flex; justify-content: space-between; align-items: flex-start; gap: 0 1.5rem;
               margin: 0 0 .45rem; padding: .7rem 1rem; background: var(--rp-bg-soft);
               border: 1px solid var(--rp-line); border-radius: 10px; }
.report-meta-who, .report-meta-where { display: flex; flex-direction: column; margin: 0; min-width: 0; }
/* The right stack hangs off the right edge, the way .rr-sheet-where does. align-items as well as text-align:
   the rows are flex boxes, so the BOX has to move right before the text inside it can. */
.report-meta-where { align-items: flex-end; text-align: right; }
.report-meta-where > div { justify-content: flex-end; }
/* AN UNSET VALUE DRAWS ITS DASH FROM CSS, not from a Dash() helper in three components. It is right where
   the labels are visible -- "District —" reads as a fact about the district -- and wrong where they are
   hidden, on the teacher's sheets, where it is a mark with nothing to attach to. Presentation, so it lives
   with the presentation and .rp-print-doc can turn it off. Set in the content, not a pseudo-element border,
   so it is copied and printed like any other text. */
.report-meta dd:empty::after { content: "—"; }
/* baseline, so the label and its value sit on ONE horizontal line — they are different sizes, and the
   default `stretch` aligns their BOXES rather than their text, which reads as a half-line step between
   them at this size gap. */
.report-meta > dl > div { display: flex; gap: .5rem; padding: .22rem 0; align-items: baseline; }
/* Fixed label column so every VALUE starts at the same x and they line up vertically, even though
   the labels differ in length (Level is much shorter than District). Not right-justified. */
/* TWO CHANNELS, NOT THREE (Ron, 2026-09-06). Label and value were separated by size AND weight AND colour
   at once, which reads fussy in a block of seven pairs. Size and colour carry it perfectly well; the bold
   was the one doing least, because a dark value beside a smaller grey label is already unambiguous.
   The size difference stays deliberately: it is what lets the eye land on the VALUE and treat the label as
   scaffolding. If it is ever equalised, widen the label column with it.

   THE COLUMN IS 6rem AND THE LABEL NEVER WRAPS (2026-09-06). It was 5.25rem = 84px, sized when the longest
   label was "District". "Report date" is the first label carrying a SPACE, so it is the first that CAN
   break, and at ~78px it sat close enough to the edge to wrap onto a second line — which dropped its value
   away from it and is what "label and value are not on the same line" was. nowrap is the guarantee; the
   extra width is what stops nowrap turning a wrap into an overflow. */
.report-meta dt { flex: 0 0 6rem; margin: 0; color: #52606d; font-size: .85rem; white-space: nowrap; }
/* min-width:0 lets a long value wrap inside its own cell instead of forcing the row wider. */
.report-meta dd { margin: 0; color: var(--rp-ink); min-width: 0; }
/* Where the student stands on the level, beside its name. Muted and a shade smaller: the LEVEL is the
   identity fact this row carries, and the standing qualifies it -- at the same weight the two competed. */
.report-meta .meta-standing { color: #52606d; font-size: .92em; }
/* OPEN LETTERHEAD, NOT A CARD, ON THE TEACHER'S SURFACES (Ron, 2026-09-12). The two class sheets carry their
   identity block as plain type with nothing drawn around it; this one carried a filled, bordered, rounded
   box, and that -- not the information in it -- was the whole of the difference Ron saw. A teacher reads all
   three together, so they should not use two visual systems.
   SCOPED TO .rp-print-doc, which is exactly the teacher's two surfaces: the drill-down view of a student's
   reports and the class-wide certification sheet. The STUDENT's own copy of the same report keeps the card --
   nothing in the student app is being compared to Class Strands, which no student ever sees, and a soft
   panel suits a screen a child reads. Context-sensitivity is not new here: this block already takes
   different padding, margins and grid placement inside .rp-print-doc.
   THE LABELS STAY, and that is the line between consistency and copying. The class sheets go unlabelled
   because every name on them is a place the teacher already knows, and a missing value is simply omitted.
   This block leads with the STUDENT and is read by a parent, where unlabelled names would leave them
   guessing which one is their child -- and it dashes unset values precisely because labels make "District —"
   still read as a fact. The two decisions are a matched pair.
   THE TYPE IS DELIBERATELY UNTOUCHED. Matching the class sheets' 1.05rem bold values would add height to a
   page fought for line by line, and bolding the values would reverse a considered decision above ("two
   channels, not three"). Removing the box was the whole of what was needed.
   It also GAINS height rather than costing it: .7rem of vertical padding and a border come off.

   THE FIRST PASS ONLY TOOK THE BOX OFF and that was not enough -- Ron: "the header was not matched up very
   well ... no labels like Student or Teacher and the right side is right justified". Measured against
   .rr-sheet-id afterwards, the two blocks differed in FIVE ways, not one: the box, the labels, the
   justification, and a type ladder in two parts (a bold dark first line, lighter grey lines beneath) that
   this block did not have at all -- every value here was one size and one weight.
   WHAT MADE IT CHEAP: this grid fills ROW BY ROW into two columns, so its odd children are column one and
   its even children column two. That already yields Student/Teacher/Class/Level at the left and
   District/School/Report date at the right -- which IS the class sheets' who-left, where-right split. The
   block was never laid out differently, only dressed differently, so nothing is reordered below and the
   markup is untouched. That matters: CSS can be scoped to .rp-print-doc and markup cannot, so the student's
   own copy keeps the card it is meant to keep.
   THE LABELS ARE HIDDEN, NOT DELETED -- .sr-only's own declarations, copied rather than classed because
   adding a class would mean editing markup shared with the student app. A sighted reader sees the clean
   two-column block the class sheets use; a screen reader still announces "Student: Amara Okafor", which is
   the thing a parent-facing record cannot afford to lose. dt leaves the flow entirely, so each value starts
   at its column edge with no 6rem gutter in front of it.
   THE ONE DIFFERENCE CSS CANNOT CLOSE is the em dash. The class sheets OMIT an unset district or school;
   this block renders "—" because a label made that read as a fact, and with the labels hidden a bare dash
   would float in the right stack. Changing it means changing Dash() in three components and therefore the
   student's copy too, so it is left alone and noted here. */
.rp-print-doc .report-meta { background: transparent; border: 0; border-radius: 0; padding: 0; }
.rp-print-doc .report-meta dt { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
                                overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
/* Line-height carries the rhythm, the way it does in .rr-sheet-who -- so the row padding that spaced a
   labelled block comes off, here and in the print override further down. */
.rp-print-doc .report-meta > dl > div { padding: 0; }
.rp-print-doc .report-meta dd { font-size: 1.05rem; line-height: 1.35; font-weight: 600; color: #52606d; }
/* THE FIRST LINE OF EACH STACK takes the weight and the dark ink, exactly as .rr-sheet-who's first span
   does; everything beneath steps down. :first-child of a declared stack, not an nth-child count across a
   grid -- which is the whole reason the structure changed: the old version would have promoted the wrong
   line the moment a field was omitted. */
.rp-print-doc .report-meta > dl > div:first-child dd { font-weight: 700; color: var(--rp-head); }
/* No dash on these sheets: the labels are hidden here, so there is nothing for it to qualify. */
.rp-print-doc .report-meta dd:empty::after { content: none; }
/* Student Mission History — every mission x the four strands. A "Skill Strands" legend defines the
   numbered strand codes so the data columns stay one character wide; cells are tinted by band using the
   same colours as the band pills. Wide table — this is the report that paginates. */
/* Legend: a quiet one-line code->name key, left-justified, no fill — it recedes behind the report title
   and the table's own "Skill Strands" group header. Wraps gracefully on narrow widths. */
.mhr-legend { display: flex; flex-wrap: wrap; gap: .3rem 1.4rem; width: fit-content; max-width: 100%; margin: 0 0 1rem; border: 1px solid var(--rp-line); border-radius: 10px; padding: .5rem .9rem; }
.mhr-key { font-size: .82rem; color: var(--rp-ink); white-space: nowrap; }
.mhr-n { font-weight: 700; margin-right: .25rem; color: var(--rp-head); }
.mhr-t { width: 100%; border-collapse: collapse; table-layout: fixed; margin: 0 0 1rem; }
.mhr-t th, .mhr-t td { border: 1px solid var(--rp-line); padding: .4rem .5rem; font-size: .85rem; text-align: center; }
.mhr-t th { color: #52606d; font-weight: 700; background: var(--rp-bg-soft); }
.mhr-t .nm { text-align: left; }
/* The "Skill Strands" group header + the vertical separators that frame the four strand columns. */
/* A RULE EVERY SECOND PASSAGE (Ron, 2026-09-12), the treatment the roster and Class Strands already take.
   Twenty rows of a fully gridded table is a lot of line; the verticals stay, so the columns still read, and
   the passages pair up.
   BOTH SIDES OF THE SEAM HAVE TO GO. border-collapse merges the bottom border of one row with the top border
   of the next, and with equal width, style and colour there is no defined winner -- clearing only one leaves
   the line drawn by the other.
   COUNTED FROM THE RIGHT PLACE: this tbody opens with .mhr-all and closes with .mhr-mastery, so the first
   PASSAGE is nth-child(2). Pairing on even/odd from there puts the rule under the second passage of each pair
   and leaves those two summary rows their own borders, which mark a change of kind. */
.mhr-t tbody tr.mhr-row:nth-child(even) td { border-bottom-color: transparent; }
.mhr-t tbody tr.mhr-row:nth-child(odd) td { border-top-color: transparent; }
.mhr-group { border-bottom: 1px solid var(--rp-line); }
.mhr-t .mhr-group, .mhr-t .mhr-gstart, .mhr-t .mhr-avg, .mhr-t .mhr-avg-h { border-left: 2px solid #b9c6d4; }
.mhr-t .mhr-avg { font-weight: 700; }
/* Average cell = a fixed-width, right-aligned number slot, so the % signs stack straight down the column.
   The reserved ✓ slot that used to sit beside it went with the mark on 2026-09-04 (see .mhr-name below): the
   average is how a passage was CHOSEN, not a number anything uses, so it holds a number and nothing else. */
.mhr-t .mhr-avg .mhr-avg-num { display: inline-block; width: 2.7em; text-align: right; }

/* THE CHAMPION MARK sits with the TYPE, because the surprising thing about this table is that a lower
   Average can be checked while a higher one is not -- the quota is the best three of EACH type, never the
   best twelve overall, and only the type cell can say so. A RESERVED SLOT on every row, occupied or not:
   the cell is centred, so a mark that took width on champions alone would shift their labels off the
   column's axis. inline-block for the width to apply; left-aligned inside so the tick sits against the
   label rather than drifting to the far edge of its own box. */
.mhr-t .mhr-type .mhr-champ { display: inline-block; width: 1em; margin-left: .35em; text-align: left; }
/* THE TICKS LINE UP DOWN THE COLUMN, which takes two rules, and the first attempt shipped only one.

   THE LABEL NEEDS THE SLOT, not just the mark. The cell is centred and the labels differ in width, so
   "Nonfiction 1 ✓" and "Fiction 2 ✓" centre as blocks of different widths and put their ticks at two
   different x positions. A reserved slot on the MARK cannot fix that: what varies is what comes before it.
   Slotting the label makes every cell's content one width, so it centres identically on every row.

   PADDING THE LABELS TO EQUAL LENGTH WITH HARD SPACES DOES NOT WORK, and was tried: the font is
   proportional, so "Nonfiction 1" (12 chars) and "Fiction 1" (9) differ by the widths of N, o and n, which
   are not an integer number of spaces. Equal character counts give unequal pixel widths -- the ticks land
   near-aligned, which reads as carelessness where a plain misalignment reads as a bug.

   nowrap IS THE OTHER HALF. The slot alone made the cell content wider than the column, so the mark wrapped
   to a second line -- fixing the horizontal problem by creating a vertical one. nowrap forbids that.
   IT DOES NOT WIDEN ANYTHING: this comment said nowrap would make the browser give the column the width it
   needs "since a colgroup percentage is a hint, not a cap", which is true of an AUTO table and false here --
   `table-layout: fixed` means the declared width is exactly what the column gets and the surplus overflows.
   The column fits because it is sized to fit (.mhr-c-genre), not because nowrap asked for room.

   min-width, not width: it equalises the four labels TypeName returns, and a longer one added later grows
   its cell instead of overflowing under the tick. Left-aligned inside the slot so the labels stack too. */
.mhr-t .mhr-type { white-space: nowrap; }
.mhr-t .mhr-type .mhr-typename { display: inline-block; min-width: 6.2em; text-align: left; }
.mhr-t .mhr-all td { font-weight: 700; }
/* Clickable passage rows in the Progress Report → each opens its Passage Report. The "All passages"
   aggregate row (.mhr-all) is NOT interactive. Hover uses an inset overlay so the band tints show through. */
.mhr-t tbody tr.mhr-row { cursor: pointer; }
.mhr-t tbody tr.mhr-row:hover td { box-shadow: inset 0 0 0 999px rgba(60, 90, 130, .06); }
.mhr-t tbody tr.mhr-row:focus-visible { outline: 2px solid var(--rp-accent); outline-offset: -2px; }
.mhr-chev { color: var(--rp-accent); font-weight: 700; margin-right: 3px; }
/* Summary rows below the passages: the booster placeholder (per-cell clickable) and the mastery counts. */
.mhr-t tr.mhr-booster td, .mhr-t tr.mhr-mastery td { border-top-width: 1.5px; }
.mhr-t tr.mhr-booster .nm { font-weight: 600; }
.mhr-t tr.mhr-mastery td { color: var(--rp-muted); }
.mhr-t tr.mhr-mastery .nm { font-style: italic; }
.mhr-t td.mhr-dash { color: var(--rp-muted); }
.mhr-t td.mhr-bcell { cursor: pointer; }
.mhr-t td.mhr-bcell:hover { box-shadow: inset 0 0 0 999px rgba(60, 90, 130, .06); }
.mhr-t td.mhr-bcell:focus-visible { outline: 2px solid var(--rp-accent); outline-offset: -2px; }
.mhr-champ { color: var(--rp-good); font-weight: 700; }   /* ✓ marking a beat champion the skill scores use */
.mhr-foot-note { font-size: .8rem; color: #52606d; margin: .7rem 0 0; line-height: 1.4; }
/* The footnotes as one block, so they cannot split across a page: see the note at the markup for why
   break-inside is the only avoid-rule Chrome's print engine can be relied on for.
   NOT called .mhr-foot, which was the name it wanted: that class survived in this file with a margin on it
   after the markup that used it was removed, so a wrapper taking the obvious name would have silently
   inherited a 1rem bottom margin from a rule for a block that no longer exists. */
.mhr-notes { break-inside: avoid; }
/* Skill boosters — their own keyed-by-skill list below the matrix (a booster tests one weak skill, so it
   doesn't belong in the four-column grid). Each row opens that booster's capped report. */
.mhr-boosters { margin-top: 1.25rem; }
.mhr-boosters-h { font-size: .95rem; font-weight: 700; color: var(--rp-head); margin: 0 0 2px; }
.mhr-boosters-sub { font-size: .8rem; color: var(--rp-muted); margin: 0 0 10px; }
.mhr-brow { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border: 1px solid var(--rp-line); border-radius: 8px; cursor: pointer; margin-bottom: 8px; }
.mhr-brow:hover { background: var(--rp-bg-soft); }
.mhr-brow:focus-visible { outline: 2px solid var(--rp-accent); outline-offset: -2px; }
.mhr-bskill { flex: 0 0 120px; font-size: .8rem; color: var(--rp-muted); }
.mhr-btitle { flex: 1; min-width: 0; font-size: .85rem; font-weight: 600; color: var(--rp-head); }
/* COLUMN WIDTHS IN REM, NOT PERCENT (Ron, 2026-09-04), and the reason is WCAG 1.4.4 rather than taste.
   This table is `table-layout: fixed`, so a column gets exactly the width declared and over-long content
   OVERFLOWS the cell instead of widening it -- and every value in the five score columns is a single
   unbreakable token ("100%", the header word "Average"), so it cannot wrap out of trouble either.

   Under percentages that made text-only enlargement a cliff. The columns were percentages of an 800px
   measure, so they did not move when a reader raised their default font: the arithmetic put the score
   columns over the edge at about 170% and the Type column, once the ✓ moved into it, at about 108% -- short
   of the 200% the guideline requires. (Browser ZOOM was never affected: it scales the measure and the type
   together, which is also why 1.4.10 Reflow was fine.)

   In rem the columns grow with the reader's font, so the overflow point goes away entirely; past roughly
   150% the TABLE outgrows its container instead, and .mhr-scroll turns that into sideways scrolling, which
   is what 1.4.10 permits for a data table.

   The sizes are content + the 1rem of horizontal cell padding, plus BREATHING ROOM: "100%" is ~1.96rem and
   the "Average" header ~3.5rem (it is wider than any value beneath it, so the HEADER sizes that column), and
   each score column then carries **2 spaces of air on each side** on top -- .875rem, ~14px (Ron, 2026-09-04).
   Sized to the content alone they cleared "100%" by 4px, which is technically enough and looks starved. The
   air is bought with WIDTH rather than padding because the cells are centred: extra width lands evenly on
   both sides by itself, and under fixed layout width is the only thing that actually moves.

   WHAT IT COSTS, stated because it is a real trade and not free: those 14px are spent five times over (four
   strands and the Average), so 71px comes out of the passage column, which is unsized and takes whatever is
   left. That drops it from ~397px to ~326px against a longest title needing ~329px -- so exactly one title
   in the corpus, Gold's 42-character "The Number That Cannot Tell You Everything", wraps to two lines. Three
   spaces a side would have cost four titles, and four spaces sixteen.

   The passage column is deliberately unsized -- in fixed layout it takes the remainder, ~41% of an 800px
   report, up from 28%. That was the request that started this: the score columns were carrying 30-50px of
   slack each while a THIRD of all titles wrapped. */
/* WIDER TITLES, PAID FOR BY THE COLUMNS THAT WERE IDLE (Ron, 2026-09-12). Two passage titles wrapped to a
   second line, and on a report that spilled its footnotes onto page two every line counts. .mhr-c-name takes
   the remainder under table-layout: fixed, so narrowing anything else widens it without a number of its own.
   WHERE THE ROOM ACTUALLY WAS, measured rather than eyeballed:
     - each skill column held "100%" at most under a heading that is a single DIGIT -- about 2.6rem of content
       in 4.1rem. Four columns, and much the largest source.
     - Average was 5.7rem for a 4.0rem heading; the word "Average" is what binds it, not the number.
     - TYPE had almost nothing, which is the surprise: its white space is the tick slot plus the min-width that
       equalises the four labels so the ticks line up (see .mhr-typename). It gives 0.25rem and no more, and
       taking more would overflow "Nonfiction 2" under the tick.
   Together ~5.5rem, about 89px, so the title column goes from roughly 273px to 362px on a Letter sheet. Each
   figure keeps air above its floor -- these are content minimums, not targets. */
.mhr-c-name { /* no width: takes the remainder, so every reduction below lands here */ }
.mhr-c-genre { width: 7.35rem; }
.mhr-c-s { width: 3.1rem; }
.mhr-c-avg { width: 4.4rem; }
/* The table may exceed its container once text is enlarged; the wrapper scrolls rather than the page. */
.mhr-scroll { overflow-x: auto; }
.mhr-ach { background: var(--rp-good-bg); color: #14532d; }
.mhr-app { background: #e8f0fe; color: #1a56b8; }
.mhr-eme { background: var(--rp-hint-bg); color: #92600b; }
.mhr-beg { background: var(--rp-bad-bg); color: #8a2b2b; }
/* .mhr-foot / .mhr-foot-h / .mhr-foot-k / .mhr-blist / .mhr-blist li / .mhr-dot were REMOVED here on
   2026-09-04: six rules for a booster footer block whose markup is long gone -- nothing in the app carries
   any of those classes. Found because a new footnote wrapper wanted the name .mhr-foot and would have
   inherited a margin from a rule for an element that does not exist. */
/* Report sub-section (e.g. "Support used") inside the single-mission Skill Report. */
.report-section { font-size: 1.05rem; color: var(--rp-head); margin: 0 0 .25rem; }
/* Passage Report "Support used" heading — matches the "Reading strand" column header (small, uppercase,
   grey, letter-spaced), with extra space above to separate it from the table above. */
.support-h { margin: 1.6rem 0 .4rem; font-size: .8rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: #52606d; }
.report-note { font-size: .85rem; color: var(--rp-muted); margin: 0 0 .6rem; }
/* THE FOOTNOTE BLOCK SETS ITSELF APART, THEN CLOSES UP (Ron, 2026-09-06). 12px above the first note so the
   small print is visibly a different register from the record it annotates — it was butting straight onto
   whatever ended above it. Between the notes, half the usual .6rem: they are a list of short asides read
   together, and the default spacing made four one-line notes look like four separate paragraphs.
   Scoped to .report-notes, NOT to .report-note globally — the same class carries the "Not offered" line
   under the support table, which has its own spacing and is not part of this list.
   :last-child kills the trailing margin, which is pure dead space at the very bottom of the page. */
/* A rule divides the record from its small print. Symmetric 12px either side, so the line sits IN the gap
   rather than pushed against one edge of it — the 12px asked for above the first note now reads as the
   space below the separator. */
/* 19px above, up from 5px (Ron, 2026-09-08). Pulling the support note up to its table on screen took 16px
   out of everything below it, the footnote block included -- the note moved where it was wanted and the
   footnotes came with it, which was not. This puts the block back where it sat relative to the support
   section. ONLY ProgressReport renders .report-notes, and print sets its own margin-top under
   .rp-print-doc, so neither another report nor the printed sheet is touched by this number. */
.report-notes { margin-top: 19px; padding-top: 5px; border-top: 1px solid var(--rp-line); }
.report-notes .report-note { margin-bottom: .3rem; }
.report-notes .report-note:last-child { margin-bottom: 0; }
.score-table tfoot .overall-row td { border-top: 2px solid var(--rp-line); border-bottom: none; font-weight: 700; color: var(--rp-head); padding-top: .55rem; }

/* Student dashboard (progress panel atop the missions) */
.dashboard { background: var(--rp-bg-soft); border: 1px solid var(--rp-line); border-radius: 12px; padding: 1rem 1.25rem; margin: 0 0 1.5rem; }
.dash-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: .8rem; }
.dash-level-label { display: block; font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; color: var(--rp-muted); }
.dash-level { font-size: 1.4rem; font-weight: 700; color: var(--rp-head); }
.dash-recent { font-size: .9rem; color: #52606d; }
.dash-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: .75rem; }
.dash-card { background: #fff; border: 1px solid var(--rp-line); border-radius: 10px; padding: .7rem .8rem; }
.dash-num { display: block; font-size: 1.6rem; font-weight: 700; color: var(--rp-head); }
.dash-of { font-size: 1rem; font-weight: 600; color: var(--rp-muted); }
.dash-label { display: block; font-size: .85rem; color: var(--rp-ink); margin-top: .1rem; }
.dash-sub { display: block; font-size: .72rem; color: var(--rp-muted); }
.dash-next { margin: .9rem 0 0; color: var(--rp-ink); }
.dash-next-label { font-weight: 700; color: var(--rp-accent); }
@media (max-width: 640px) { .dash-cards { grid-template-columns: repeat(2, 1fr); } }
.dash-card-btn { cursor: pointer; text-align: left; font: inherit; }
.dash-card-btn:hover { border-color: var(--rp-accent); background: #f5f9ff; }

/* ===== Student dashboard (the live home base — StudentDashboard.razor) ===== */
.nav-dash { max-width: 800px; }
.nd-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: 28px; flex-wrap: wrap; }
.nd-name { font-weight: 700; color: var(--rp-head); font-size: 1.3rem; line-height: 1.2; }
/* Support status under the student name — STATUS, not controls (a teacher sets these). Both are
   listed even when Off so a student discovers the option and can ask for it. */
/* Two left-aligned rows opposite the name: a mini status table, so On/Off compare at a glance. */
/* Header class block: teacher name over class name (right), sharing the .nd-name type with the student name.
   No labels — the student's own name, teacher, and class are self-evident. A small gap separates the two
   right-side names so they don't read as one wrapped line. */
.nd-classinfo { text-align: right; }
.nd-name-gap { margin-top: .2rem; }
.nd-support-hint { font-size: .82rem; color: var(--rp-muted); margin: .4rem 0 1rem; }
/* Living level strip (docs/LevelReview_Spec.md): LEFT-justified so a 3–5 pill row keeps a stable left
   anchor (no re-centering as levels are added on promotion) and lines up with the cards below. Pills size
   to content, not flex:1. Completed + current-in-review pills are <button>s — reset their chrome. */
.nd-journey { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: 1.25rem; }
.nd-lvl { display: inline-flex; align-items: center; gap: .32em; padding: .5rem .7rem; border: 1px solid var(--rp-line); border-radius: 8px; font-family: inherit; font-size: .78rem; line-height: 1.2; color: var(--rp-muted); background: #fff; }
button.nd-lvl { cursor: pointer; }
button.nd-lvl:hover { border-color: var(--rp-accent); background: #f5f9fb; }
.nd-lvl.cur { border: 2px solid var(--rp-gold); color: #2a2f36; font-weight: 700; background: #fffdf5; }
.nd-lvl.done { color: #4a5560; }
.nd-lvl.locked { color: var(--rp-disabled); }
.nd-lvl.viewing { box-shadow: 0 0 0 2px var(--rp-accent); border-color: var(--rp-accent); }
/* Icon carries state too (shape + colour), so it stays legible without relying on colour alone: gold star =
   current/active, green check = completed, grey padlock = locked. Deeper gold, not bright yellow (contrast). */
.nd-lvl-icon { display: inline-flex; align-items: center; font-size: 1rem; }
.nd-lvl-icon.cur { color: var(--rp-gold); }
.nd-lvl-icon.done { color: var(--rp-good); }
.nd-lvl-icon.locked { color: var(--rp-disabled); }
.nd-arc { background: var(--rp-bg-soft); border: 1px solid var(--rp-line); border-radius: 12px; padding: 1rem 1.25rem; }
/* Bootstrap's Reboot sets every heading to font-weight:500, so h2/h3 do NOT arrive bold the way the
   browser default would give them. These two are section titles and have to carry weight, so state it
   explicitly rather than relying on the element. */
.nd-arc h2 { font-size: 1.15rem; font-weight: 700; color: var(--rp-head); margin: 0 0 .8rem; }
/* Progress-to-promotion bar (docs/Promotion_ProgressModel.md) — the four skills' cumulative fill,
   averaged, over one headline bar; below it one row per skill (mini-meter · right-aligned fill % ·
   quality band pill). The bar shows from the first passage (only rises); pills appear at 4+ passages. */
/* Two boxed cards: the OVERALL "Progress to your Challenge" headline (big %), then the four-skill
   breakdown. Plain white + hairline border — distinct from the teal-accented ACTION cards (.nd-continue)
   that follow, so info reads as bordered-neutral and actions read as accented. */
.nd-prog-card, .nd-skills-card { background: #fff; border: 1px solid var(--rp-line); border-radius: 12px; padding: 1rem 1.1rem; margin: 0 0 .9rem; }
/* The skills card now sits BELOW the action block — give it the same top margin the up-next card (.nd-continue)
   uses, so the gap above it matches the rest of the stack. */
.nd-skills-card { margin-top: 1rem; }
/* Mastery mode (accelerated) — a celebratory green wash + green headline/fill, so the card reads as a reward. */
.nd-prog-mastery { background: #f0faf5; border-color: #b7e4cd; }
.nd-prog-mastery .nd-prog-big { color: #1a7f5a; background: #dcf3e7; }
.nd-prog-mastery .nd-prog-fill { background: #1a7f5a; }
/* Read-only level REVIEW progress card — calm neutral/accent (a finished level, "here's how it went"). */
.nd-prog-review { background: #f7f9fb; border-color: #cdd8e2; }
.nd-prog-review .nd-prog-big { color: var(--rp-accent); background: var(--rp-accent-soft); }
.nd-prog-review .nd-prog-fill { background: var(--rp-accent); }
/* Finished-level review card = the completion STORY (label + one dynamic line), no % or bar. */
.nd-prog-review-body { padding: .1rem 0; }
.nd-prog-review-title { font-size: 1.1rem; font-weight: 700; color: var(--rp-head); }
.nd-prog-review-desc { margin: .35rem 0 0; font-size: .88rem; color: #52606d; }
/* The achievement line (overall mastery + Challenge, or the encouraging band line) — a touch heavier than
   the promotion desc so the numbers read as earned, without competing with the title. */
.nd-prog-review-mastery { margin: .3rem 0 0; font-size: .9rem; font-weight: 600; color: var(--rp-head); }
.nd-prog-head { display: flex; align-items: center; gap: .85rem; margin: 0 0 .7rem; }

/* Sticky "Reviewing {level}" bar shown while revisiting a finished level (docs/LevelReview_Spec.md):
   names the mode and is the always-visible way back to the live dashboard. */
.review-bar { position: sticky; top: 0; z-index: 20; display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
    background: var(--rp-accent-soft); border: 1px solid #bcd6dd; border-radius: 10px; padding: .55rem .8rem; margin: 0 0 1rem; }
.review-back { font: inherit; cursor: pointer; border: 1px solid var(--rp-accent); background: #fff; color: var(--rp-accent);
    font-weight: 600; padding: .35rem .75rem; border-radius: 8px; }
.review-back:hover { background: var(--rp-accent); color: #fff; }
.review-label { color: #33525a; font-size: .9rem; }
.nd-prog-big { flex: 0 0 auto; font-size: 1.7rem; font-weight: 800; color: var(--rp-accent); background: var(--rp-accent-soft); border-radius: 10px; padding: .25rem .55rem; font-variant-numeric: tabular-nums; line-height: 1; }
.nd-prog-title { font-size: .95rem; font-weight: 700; color: var(--rp-head); }
.nd-prog-desc { font-size: .8rem; color: var(--rp-muted); }
.nd-prog-track { position: relative; height: 22px; background: var(--rp-bg-soft); border: 1px solid var(--rp-line); border-radius: 999px; overflow: hidden; }
.nd-prog-fill { height: 100%; background: var(--rp-accent); border-radius: 999px; transition: width .3s ease; }
/* Bar markers sit in a wrapper OVER the 22px track (the track clips its overflow). The marker visuals + states
   now live on the ProgressMilestoneButton classes (.nd-ms-*) below; this wrapper is shared. */
.nd-prog-cpbar { position: relative; }

/* Clickable milestone markers (ProgressMilestoneButton): each benchmark on the bar is an info button (active)
   or an inert marker (passed). The wrap positions it exactly where the old dot/seal sat; the button carries an
   enlarged transparent hit area (::before) so the 14px dot / 22px seal is easy to tap; the popover reveals the
   marker's message above the bar, edge-aligned so it never spills out of the card. */
.nd-ms-wrap { position: absolute; top: 11px; transform: translate(-50%, -50%); z-index: 2; }
.nd-ms-wrap--end { transform: translateY(-50%); }
.nd-ms-btn { position: relative; display: block; padding: 0; margin: 0; border: 0; background: transparent; cursor: pointer; line-height: 0; }
.nd-ms-btn::before { content: ""; position: absolute; inset: -8px; border-radius: 50%; }
.nd-ms-btn:focus-visible { outline: none; }
.nd-ms-btn:focus-visible .nd-ms-mark { outline: 2px solid var(--rp-accent); outline-offset: 3px; }
.nd-ms-mark { display: flex; align-items: center; justify-content: center; box-sizing: border-box; }
.nd-ms-dot { width: 14px; height: 14px; border-radius: 50%; background: #fff; border: 2px solid #b7c2ce; }
.nd-ms-dot--done { background: var(--rp-accent-bright); border-color: #fff; }
.nd-ms-dot--next { border-color: var(--rp-accent); box-shadow: 0 0 0 3px rgba(16,120,141,.20); }
.nd-ms-report { width: 22px; height: 22px; border-radius: 6px; }
.nd-ms-report svg { width: 13px; height: 13px; }
/* BOTH VALUES FAILED, and the BORDER was the worse of the two (2026-09-15): the icon was 2.57:1 and the
   border 1.81:1, against the 3:1 that 1.4.11 asks of non-text. The border is what makes this 22px marker
   visible at all in its off state, so the one the ledger did not name mattered more than the one it did.
   Non-text and not 4.5:1 because the mark is a 13px SVG inside a 22px box -- a graphic, not a label. */
.nd-ms-report--off { background: #fff; border: 2px solid var(--rp-mark); color: var(--rp-mark); }
.nd-ms-report--on { background: var(--rp-accent); border: 2px solid #fff; color: #fff; }
.nd-ms-pop { position: absolute; bottom: calc(100% + 10px); z-index: 40; width: max-content; max-width: 240px; background: #fff; border: 1px solid var(--rp-line); border-radius: 10px; box-shadow: 0 8px 24px rgba(31,56,100,.18); padding: .5rem .7rem; font-size: .8rem; line-height: 1.4; font-weight: 400; color: #42586a; text-align: left; }
.nd-ms-pop--center { left: 50%; transform: translateX(-50%); }
.nd-ms-pop--start { left: -8px; }
.nd-ms-pop--end { right: -8px; }

.nd-cp-labels { position: relative; height: 1.05rem; margin: .5rem 0 0; }
.nd-cp-label { position: absolute; transform: translateX(-50%); font-size: .72rem; color: var(--rp-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.nd-cp-label.is-next { color: var(--rp-accent); font-weight: 700; }
/* The final marker (passage 20) sits at the track's right end — right-align its label (inline right:0) so it
   stays inside the card instead of overflowing like a centered label would. */
.nd-cp-label-end { transform: none; }
@media (prefers-reduced-motion: reduce) { .nd-prog-fill, .nd-skill-fill { transition: none; } }

.nd-skills-head { font-size: 1.1rem; font-weight: 700; color: var(--rp-head); margin: 0 0 .9rem; }
.nd-skills-wait { font-size: .85rem; color: var(--rp-muted); margin: 0; }
.nd-skills-note { font-size: .8rem; color: #52606d; margin: .7rem 0 0; line-height: 1.35; }
.nd-skills { display: flex; flex-direction: column; gap: .55rem; margin: 0; }
.nd-skill { display: flex; align-items: center; gap: 12px; }
/* FIXED-width label column (not grow) — every label box the same width, so all four bars start at the
   same x and the labels left-align. 170px fits the longest name ("Connecting sources") with a little
   room; the bar then fills the rest of the way to the %/pill on the right. */
.nd-skill-name { flex: 0 0 170px; min-width: 0; }
.nd-skill-friendly { display: block; font-size: .88rem; font-weight: 600; color: var(--rp-head); line-height: 1.15; }
.nd-skill-ccss { display: block; font-size: .72rem; color: var(--rp-muted); }
.nd-skill-meter { position: relative; flex: 1; height: 14px; background: var(--rp-bg-soft); border: 1px solid var(--rp-line); border-radius: 999px; overflow: hidden; min-width: 0; }
.nd-skill-fill { height: 100%; background: var(--rp-accent); border-radius: 999px; transition: width .3s ease; }
.nd-skill-pct { flex: 0 0 40px; text-align: right; font-size: .85rem; font-weight: 600; color: var(--rp-head); font-variant-numeric: tabular-nums; }
/* The booster † must NOT consume width in this fixed 40px right-aligned column, or it shoves the number
   left out of line with the daggerless rows. Zero-width superscript that hangs into the gap on the right. */
/* Booster row shows the dagger ALONE (no number), so it owns the whole column — center it and give it
   normal width/baseline (the zero-width hang above is only for a dagger sitting beside a number). */
/* Fixed pill width + min-width:0 so the longest band ("Approaching") can't push its pill wider than the
   rest (default min-width:auto would), which shifted that row's meter and knocked the % column out of line. */
.nd-skill .band-pill { flex: 0 0 96px; min-width: 0; text-align: center; }
.nd-skill-nopill { flex: 0 0 96px; }
@media (max-width: 520px) {
    .nd-skill-name { flex-basis: 100px; }
    .nd-skill-friendly { font-size: .8rem; }
}
/* Expand/collapse for the completed-mission history. Full-width so it reads as a row in the list
   rather than a stray control, and sized like a row so the list keeps its rhythm. */
.nd-goal { display: flex; align-items: flex-start; gap: .5rem; font-size: .9rem; color: #52606d; background: var(--rp-bg-soft); border: 1px solid var(--rp-line); border-radius: 8px; padding: .55rem .8rem; margin: .55rem 0 .35rem; line-height: 1.45; }
/* Inline-SVG icons replaced the 🔒/🎯/🎉 emoji. They size in `em` and stroke in currentColor, so the
   only thing these rules do is set colour and stop the glyph baseline-hopping inside a flex row. */
.nd-goal-icon { flex: 0 0 auto; display: flex; align-items: center; line-height: 1; padding-top: .05em; color: var(--rp-muted); font-size: 1.15rem; }
.nd-goal-icon.open { color: var(--rp-accent); }
.nd-goal-icon.done { color: var(--rp-good); }
.nd-lvl-lock { display: inline-flex; align-items: center; vertical-align: -.2em; margin-right: .2em; font-size: 1rem; }
/* Stacked lines so the sentence breaks where it is written to, not wherever the row runs out. */
.nd-goal-text { display: flex; flex-direction: column; }
.nd-goal strong { color: var(--rp-head); font-weight: 700; }
.nd-subline { font-size: .9rem; color: #52606d; line-height: 1.5; margin: 0; }
/* Border UNIFORM on all four sides (Ron, 2026-08-24). This carried border-left: 4px solid var(--rp-accent),
   which read as a lopsided card rather than as an accent — the 10px radius makes a heavy left edge look like
   a rendering fault. The card is already distinguished by being lifted out of the list with its own icon and
   button; it does not need an edge stripe as well. */
.nd-continue { margin-top: 1rem; padding: 1rem 1.1rem; background: #fff; border: 1px solid var(--rp-line); border-radius: 10px; display: flex; align-items: center; gap: .85rem; }
.nd-continue-icon { width: 40px; height: 40px; border-radius: 10px; background: #e8f0fe; color: #1a56b8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.nd-continue-info { flex: 1; }
.nd-continue-title { font-weight: 700; color: var(--rp-head); }
.nd-continue-sub { font-size: .8rem; color: #52606d; display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.nd-continue .btn-next { margin-top: 0; }

/* "Up next" card enrichment (dashboard engagement): a topic hook + a genre-colored type pill, so the card
   sells the read. Color keys on GENRE — Fiction warm, Nonfiction cool (two colors, not four archetypes). */
.nd-continue-status { color: #6b7683; }
.nd-genre-pill { font-size: .72rem; font-weight: 700; border: 1px solid; border-radius: 999px; padding: .1rem .5rem; letter-spacing: .01em; }
.nd-genre-nf  { color: #175e96; border-color: #a9cbea; background: #eaf3fb; }
.nd-genre-fic { color: #9a4e2e; border-color: #e5b79f; background: #fbefe8; }
/* Hook: a genre-colored accent bar + faint tint, shown ONLY when the passage has a hook. */
.nd-next-hook { font-size: .86rem; color: #42586a; line-height: 1.45; margin: .3rem 0 .45rem; }
.nd-next-hook--nf  { border-left: 3px solid #7fb0dd; background: #f4f9fe; padding: .35rem .6rem; border-radius: 0 6px 6px 0; }
.nd-next-hook--fic { border-left: 3px solid #dda588; background: #fdf6f2; padding: .35rem .6rem; border-radius: 0 6px 6px 0; }
/* Booster Mission — a forced catch-up step placed in front of the next mission. Warm accent (not the
   mission blue, not the goal green), so it reads as help rather than a new goal or a penalty; the
   "won't change your scores" note is reassurance, deliberately left-aligned under the card.
   The warm accent lives in the ICON ONLY. This used to also carry `border-left: 1px solid var(--rp-line)`,
   added 2026-08-18 to cancel .nd-continue's teal left stripe where it leaked onto this card; the stripe was
   removed outright on 2026-08-24, so the override is gone with it — a rule that cancels something no longer
   there is a trap for whoever reads it next. */
/* LEFT AT #b26a00 DELIBERATELY. This is an ICON on its own amber panel, so the bar is 1.4.11 Non-text
   Contrast at 3:1, not 1.4.3's 4.5:1 for text -- and against #fdf0d5 it measures 3.75:1. The same value was
   changed on .prov-mark in the same pass, because THAT one renders a literal "*" and is text. Recorded so
   the next contrast sweep does not "finish the job" and flatten a distinction that is the standard's. */
.nd-booster .nd-continue-icon { background: #fdf0d5; color: #b26a00; }
.nd-booster-why { text-align: left; font-size: .82rem; color: #7b5300; margin: .45rem 0 0; }
/* One target skill per line, each with a small amber dot — scales cleanly from one skill to four,
   where the old inline comma list overflowed the subline. */
.nd-booster-skills { list-style: none; margin: .3rem 0 0; padding: 0; }
.nd-booster-skills li { position: relative; padding-left: .95rem; font-size: .82rem; color: var(--rp-head); line-height: 1.6; }
.nd-booster-skills li::before { content: ""; position: absolute; left: .2rem; top: .62em; width: 5px; height: 5px; border-radius: 50%; background: #b26a00; }
/* Booster due but its lower-level source content isn't built yet — a calm, NON-blocking placeholder
   (dashed, muted) shown above the next-passage card without gating it. Temporary until that content
   exists; see StudentDashboard.BoosterNoticeOnly. */
.nd-boostersoon { margin-top: 1rem; padding: 1rem 1.1rem; background: var(--rp-bg-soft); border: 1px dashed var(--rp-line); border-radius: 10px; display: flex; align-items: center; gap: .85rem; }
.nd-boostersoon .nd-continue-icon { background: #fdf0d5; color: #b26a00; }
.nd-hint { text-align: center; font-size: .82rem; color: var(--rp-muted); margin: .3rem 0 0; }
/* Inside the progress card, the hint sits under the bar and left-aligns with the card's content. */
.nd-prog-hint { text-align: left; margin: .7rem 0 0; }
/* Mastery-acceleration hint — celebratory (the Challenge unlocked early), so it reads as a reward. */
.nd-accel-hint { color: #1a7f5a; font-weight: 600; }

/* "Progress to your Challenge" header row: title left, passage count right (the approved mockup). */
.nd-prog-goalhead { display: flex; align-items: baseline; justify-content: space-between; gap: .75rem; margin: 0 0 .7rem; }
.nd-prog-goaltitle { font-size: .95rem; font-weight: 700; color: var(--rp-head); }
.nd-prog-goalcount { font-size: .82rem; color: var(--rp-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The one contextual status line + its leading icon, under the bar. */
.nd-prog-msg { display: flex; align-items: flex-start; gap: .4rem; }
.nd-prog-msg-icon { flex: 0 0 auto; font-size: 1rem; line-height: 1.25; color: var(--rp-accent); display: inline-flex; }

/* Dashboard footer actions (report / certificate / sign out) — secondary to the mission itself. */

/* DEV ONLY — the "Reset student data" strip. Renders only in Development (see DevDataReset.cs).
   Muted and tagged on purpose so it never reads as part of the student's journey. */
.nd-devtools { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-top: .9rem; padding-top: .7rem; border-top: 1px dashed var(--rp-line); opacity: .75; }
.nd-devtag { font-size: .62rem; font-weight: 700; letter-spacing: .08em; padding: .1rem .35rem; border-radius: 3px; background: var(--rp-line); color: #52606d; }
.nd-devbtn { color: var(--rp-muted); font-size: .8rem; }
.nd-devbtn:hover { color: var(--rp-bad); }
.nd-devnote { font-size: .72rem; color: var(--rp-muted); }

/* Prototype-only Mode/Support controls, parked below the dashboard (teacher-set in the full program). */
.teacher-settings { max-width: 640px; margin: 1.5rem auto 0; padding: .3rem 1rem; border: 1px dashed var(--rp-line); border-radius: 10px; background: var(--rp-bg-soft); }
.teacher-settings[open] { padding: .3rem 1rem 1rem; }
/* An author `display` on the contents overrides the UA's details-hiding, so hide them ourselves. */
.teacher-settings:not([open]) > *:not(summary) { display: none; }
.teacher-settings > summary { list-style: none; cursor: pointer; font-size: .82rem; font-weight: 700; color: #52606d; padding: .55rem 0; }
.teacher-settings > summary::-webkit-details-marker { display: none; }
.teacher-settings > summary::before { content: "\25B8\00a0"; color: var(--rp-muted); }
.teacher-settings[open] > summary::before { content: "\25BE\00a0"; }
.teacher-settings > summary:focus-visible { outline: 3px solid var(--rp-accent); outline-offset: 2px; border-radius: 4px; }

/* Cumulative report: mastery headline + certificate banner */
.cum-headline { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; margin: 0 0 1.25rem; padding: .8rem 1rem; background: var(--rp-bg-soft); border: 1px solid var(--rp-line); border-radius: 10px; }
.cum-label { font-weight: 700; color: var(--rp-head); }
.cum-pct { font-size: 1.6rem; font-weight: 700; color: var(--rp-head); }
.cum-note { font-size: .8rem; color: var(--rp-muted); margin-left: auto; }
/* "Not final yet" banner — shown until 4 missions feed the cumulative figure. */
.cum-provisional { display: flex; align-items: baseline; gap: .45rem; flex-wrap: wrap; font-size: .85rem; color: #92600b; background: var(--rp-hint-bg); border: 1px solid var(--rp-hint-line); border-radius: 8px; padding: .5rem .8rem; margin: -.75rem 0 1.25rem; }
.cum-provisional strong { color: #7a5009; }
.cert-banner { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin: 0 0 1rem; padding: .7rem 1rem; background: var(--rp-good-bg); border: 1px solid var(--rp-good); border-radius: 10px; color: #14532d; font-weight: 600; }
.cert-banner .btn-next { margin: 0; }

/* Certification Report — in-progress status + the Challenge / Overall Mastery section blocks. */
.report-status { display: flex; align-items: center; justify-content: center; gap: .5rem; margin: -.4rem 0 1rem; flex-wrap: wrap; }
.status-pill { display: inline-flex; align-items: center; gap: .4rem; font-weight: 700; font-size: .8rem; padding: .2rem .65rem; border-radius: 999px; }
.status-progress { background: #e8f0fe; color: #1a56b8; border: 1px solid #c7dbf5; }
.status-dot { width: .5rem; height: .5rem; border-radius: 999px; background: #1a56b8; }
.status-note { font-size: .82rem; color: #52606d; }

/* Reading-support section (Certification Report) — see docs/SupportUsageReport_Spec.md. */
.support-independent { display: flex; align-items: center; gap: .5rem; margin: 1rem 0 .6rem; padding: .55rem .8rem; background: #eef7f0; border: 1px solid #cfe6d6; border-radius: 8px; color: #1f6b3a; font-size: .92rem; }
.support-independent .si-check { font-weight: 700; }
/* Extra top margin sets the Reading-support heading lower, widening the gap to the section above (~30% more). */
.support-sec { margin: 1.6rem 0 .3rem; }
/* Heading matches (a touch larger than) the "Your Reading Skills" strand header. */
/* TWO LINES, NEVER FOUR (Ron, 2026-09-06). This is a flex row, and loose text in one becomes an anonymous
   flex item that wraps mid-phrase — in the ~328px printed column the heading came out as "Reading /
   Support / · Percentage shows how much / support was used". The title is now one nowrap span, and the
   gloss is given a full-width basis so it lands on line two instead of squeezing in beside the title.
   baseline rather than center, since the gloss is set smaller than the title. */
/* Shared by "Reading Support" and "Your Journey" -- the two section headings in the printed right column.
   ITS OWN CLASS, and the prefix is not decoration. Two obvious names were already taken by other things:
   .report-section is the single-mission Skill Report's sub-heading (~line 786), and .sec-head is the
   Challenge / Overall ROWS inside the strand table -- a rule with display:flex on that would have broken
   them. Two components wanting "a section heading" is not the same as wanting the same one.
   SCOPED UNDER .report on purpose: `.report h3` already sets 1.05rem, and a bare `.rp-sec-head` loses to it
   on specificity (0,1,0 against 0,1,1). Both headings quietly rendered a size small until this was noticed. */
.report .rp-sec-head { font-size: 1.15rem; font-weight: 600; color: var(--rp-head); margin: 0 0 .55rem;
                       display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 .4rem; }
.report .rp-sec-head .rs-title { display: inline-flex; align-items: center; gap: .4rem; white-space: nowrap; }
/* NO RULE UNDER "READING SUPPORT" (Ron, 2026-09-08). It carried a 2px border-bottom over 19px of
   padding-bottom, measured at the real 816px page so the rule landed 1px above the strand table's own
   header rule in the left column. Removed for consistency with "Your Journey", the other .rp-sec-head in
   that column, which never had one: two section headings in the same column, one underlined and one not,
   read as two different KINDS of heading rather than a pair.
   The .support-head class went with it from ProgressReport.razor -- both headings are now a bare
   .rp-sec-head, so there is no second rule to drift. Spacing under the heading is .rp-sec-head's own
   .55rem, matching Journey; the ~12px of padding and rule this gives back raises the support table and
   everything under it in the right column. */
/* The gloss is no longer a heading line — it is a note under the table. Kept as a class so the print
   block can space it against the table's last rule. */
/* Indented to the support table's TEXT edge (its cells carry .5rem of side padding), so a note does not
   start left of every figure it annotates. Applies to both notes under that table. */
.support-caveats .report-note { padding-left: 8px; }
.support-lede { margin: 0; }
/* NO BOTTOM MARGIN ON SCREEN (Ron, 2026-09-08). .score-table gives every table 1.25rem/20px below it, which
   pushed "Percentage shows how much support was used." a full line clear of the table's last rule -- far
   enough that it read as free-standing text rather than a note on the figures above it. Print already sets
   this to 0 and paces the note with .support-caveats; screen now matches. Everything below simply moves up
   with it, so the space SEPARATING this note from the footnote block underneath is unchanged. */
.support-table { width: 100%; border-collapse: collapse; margin-bottom: 0; }
.support-table th, .support-table td { text-align: left; padding: .4rem .5rem; font-size: .9rem; }
/* Data columns (everything but the Support label) centre under their headings. */
.support-table th:not(:first-child), .support-table td:not(:first-child) { text-align: center; }
.support-table thead th { font-weight: 600; font-size: .78rem; color: #52606d; border-bottom: 1px solid var(--rp-line); }
.support-table tbody tr { border-top: 1px solid var(--rp-line); }

/* ROW RULES START AT THE TEXT, NOT AT THE TABLE EDGE (Ron, 2026-09-08). In both right-column tables the
   horizontal rules ran the full width, so each one began a cell-padding's worth to the LEFT of the first
   column's text and the block read as ruled paper rather than as a table of labels.
   The line is suppressed on the first cell and redrawn as a background gradient inset by exactly that
   cell's padding-left -- .5rem for .support-table, .6rem for .journey-table, which inherits .score-table's
   padding. Two details make it behave:
     - border-bottom-color: transparent, NOT border-bottom: 0. Under border-collapse the border still
       occupies its 1px, so row heights and the collapsed metrics are untouched; only the paint goes. A
       cell's border beats a row's at the same edge (CSS 2.1 17.6.2.1 precedence: cell > row), which is
       why this also suppresses .support-table tbody tr's border-top above.
     - the replacement line is a BORDER on a pseudo-element, never a background. The first attempt drew it
       as a background gradient and it was invisible on paper: browsers do not print background colours or
       images unless the user ticks "Background graphics", so the suppression landed and the redraw did
       not, and the rules appeared to start at VOCAB / SCORE. Anything that must survive printing has to be
       a border, not a background. bottom: -1px puts it on the collapsed grid line rather than 1px high at
       the padding box; that is the knob if it ever sits a pixel off.
   The footnote separator (.report-notes border-top) is deliberately untouched -- it divides sections
   rather than rows, and Ron asked for it to stay full width. */
.support-table th:first-child, .support-table td:first-child,
.journey-table th:first-child, .journey-table td:first-child { border-bottom-color: transparent; position: relative; }
.support-table th:first-child::after, .support-table td:first-child::after,
.journey-table th:first-child::after, .journey-table td:first-child::after {
    content: ""; position: absolute; right: 0; bottom: -1px;
    border-bottom: 1px solid var(--rp-line);
}
.support-table th:first-child::after, .support-table td:first-child::after { left: .5rem; }
.journey-table th:first-child::after, .journey-table td:first-child::after { left: .6rem; }

/* ONLY THE HEADER RULE AND THE CLOSING RULE (Ron, 2026-09-08). Every one of these tables ruled every row,
   which on a printed record reads as a grid to be scanned cell by cell; the sections are short and their
   rows are already distinguished by their labels, so the interior rules were doing no work. Each block now
   keeps exactly two: the one under its header row and the one that closes it.
     - Reading Support keeps 1 and 4 of 4, Your Journey 1 and 5 of 5, and each strand section 1 and 4.
     - The support table drew its lines TWICE -- .score-table's border-bottom on every cell and its own
       tbody tr border-top -- which collapsed to one line and hid the duplication. The border-top goes
       entirely: the header cell's own border-bottom already draws the first rule.
     - The ::after inset lines from the block above must be switched off in step, or the interior rules
       would survive as short segments under the first column only.
     - tbody:not(.report-sec) keeps this off the Challenge and Overall blocks, whose .sec-head rules are
       structural. .strand-head is excluded because ITS rule is the one being kept.
   colour, NOT border: 0 -- under border-collapse the 1px still occupies its place, so nothing reflows. */
.support-table tbody tr { border-top-color: transparent; }
.support-table tbody tr:not(:last-child) td,
.journey-table tbody tr:not(:last-child) td { border-bottom-color: transparent; }
.support-table tbody tr:not(:last-child) td:first-child::after,
.journey-table tbody tr:not(:last-child) td:first-child::after { content: none; }
/* NO CLOSING RULE ON THE LAST STRAND ROW EITHER (Ron, 2026-09-12) -- the :not(:last-child) that used to
   spare it is gone. The strands are ONE tbody, so that exception never gave each section a closing line the
   way the note above imagines; it gave exactly one line, under the last anchor of the last strand, which is
   R.4 Vocabulary in Context. A rule there closed nothing: the Challenge block follows with its own gap and
   its own 2px header border, so the eye met a hairline, a gap, then a heavy line.
   PRINT HAD ALREADY REMOVED IT, with a :has(+ .report-sec) rule that has now been deleted as dead. Screen
   was deliberately left showing it on the argument that there the table is not the bottom of a page -- Ron
   overruled that, and the two surfaces agreeing is worth more than the distinction was. */
.strand-table tbody:not(.report-sec) tr:not(.strand-head) td { border-bottom-color: transparent; }
/* Print-only line break inside a column label. Inert on screen, where the labels have room to read
   as whole words; see the thead comment in ProgressReport. */
.th-br { display: none; }

.support-pct { color: #52606d; font-size: .9rem; }
.support-na { color: var(--rp-muted); font-size: .85rem; }
/* Caveats below the table — neutral grey, stacked, hairline above. */
/* Holds only the "Not offered" note now: the two support caveats and the border-top that separated them
   from the table were removed 2026-09-06 (see ProgressReport). No rule here any more -- it divided the
   table from the caveats, and with them gone it divided the table from nothing. */
/* 4px under the table's last rule -- close enough to belong to it, the same relationship print gets from
   its own 3px. The flex `gap` is what holds this note apart from the "Not offered" note below it, and it is
   untouched, so tightening above does not close up below. */
.support-caveats { display: flex; flex-direction: column; gap: .5rem; margin-top: 4px; }
.support-caveats .report-note { margin: .1rem 0 0; }
/* Promotion screen: "View your Certificate" + "Continue" side by side, centred and EQUAL height
   (stretch to the taller primary; reset .btn-next's stacked-layout top margin; centre each label). */
.promo-actions { display: flex; gap: .6rem; justify-content: center; align-items: stretch; flex-wrap: wrap; margin-top: .5rem; }
.promo-actions .btn-next, .promo-actions .btn-secondary { margin-top: 0; display: inline-flex; align-items: center; justify-content: center; }

/* Pending status in the headline — a "Not yet completed" pill (same style as "Not yet taken"),
   pushed to the right of the row. */
.cum-lock { margin-left: auto; }

.strand-table .sec-gap td { border: 0; padding: .35rem 0 .1rem; background: transparent; }
/* NO RULE UNDER THE CHALLENGE OR OVERALL MASTERY (Ron, 2026-09-08). .sec-head is worn by exactly those two
   rows, so this border-bottom is exactly the two thin lines on the sheet, and neither had anything left to
   separate: the Challenge's closed off a gloss line that has been removed, and Overall Mastery is the last
   row of the table, so its rule fenced the block off from white space.
   IT MUST BE AN EXPLICIT 0, not a deleted declaration. The first attempt simply dropped `border-bottom`
   from here, which changed nothing -- `.score-table th, .score-table td` (~line 478) sets it for every cell
   in the table, so removing the override just let the base rule through. The 2px accent border-top stays;
   that is what OPENS each block and it still has a job. Screen and print alike. */
.strand-table .sec-head td { border-top: 2px solid var(--rp-accent); border-bottom: 0; background: #eef4fb; font-weight: 700; color: var(--rp-head); vertical-align: middle; }
/* OVERALL MASTERY IS CLOSED BY A HEAVY RULE (Ron, 2026-09-12), the same 2px of --rp-head the class sheets
   use to mark a change of kind, and the same weight as this row's own border-top. It is the last line of the
   record's score table, and the three reports now end the same way.
   IT HAS TO BE STATED, not inherited: .sec-head td sets border-bottom: 0 for the Challenge and Overall rows
   alike, and that rule and this one have equal specificity -- this one wins by sitting later in the file.
   Do not move it above .sec-head. */
.strand-table .overall-head td { border-top-color: var(--rp-head);
                                 border-bottom: 2px solid var(--rp-head); }
.strand-table .sec-title-cell { text-align: left; }
.sec-title { display: inline-flex; align-items: center; gap: .4rem; font-weight: 700; color: var(--rp-head); }
.sec-sub { font-weight: 400; color: #52606d; }
/* The same subtitle on a 1.15rem LABEL — the "Reading Support" section heading and the "Correct 1st Try"
   line. .sec-sub was written for the Challenge row, where it inherits the table's .95rem; on a 1.15rem label
   it would inherit THAT instead and read as a second heading rather than a gloss. .8em lands it at ~.92rem —
   the size it renders at on the Challenge — so all three look like the same device, which is the point.
   Both selectors, or the one that is missed inherits its label and shouts. */

.strand-table .overall-head .overall-num { font-size: 1.05rem; color: var(--rp-head); }
.wt-mark { color: var(--rp-head); font-weight: 700; margin-left: 1px; }

/* Header row doubles as the "Reading skills by strand" heading — match the old h3's font/style
   (rp-head, 1.05rem, weight 500, no uppercase) instead of the small grey caps of a table header. */
.sbs-table { margin-top: 1.1rem; }
.sbs-table .sbs-head th { color: var(--rp-head); font-size: 1.05rem; font-weight: 500; text-transform: none; letter-spacing: normal; }
/* JOURNEY'S HEADER IS A COLUMN-LABEL ROW, NOT A TITLE (Ron, 2026-09-08), so it takes the small grey caps
   of the support table beside it -- .78rem / 600 / uppercase / .04em / #52606d, which is .score-table th
   with .support-table thead th's size and weight over it. It read as a title only because .sbs-head was
   built for a row that WAS one: when "Your Journey" lived in the first header cell, the row had to look
   like the h3 it replaced. The title moved out to its own .rp-sec-head on 2026-09-07 and the styling stayed
   behind, leaving "Level / Score / Date" set like a heading at 1.05rem, unbolded and in sentence case --
   twice the size of the SUPPORT / VOCAB / PASSAGES labels directly above them in the same column.
   Equal specificity to the .sbs-table rule above (two classes + an element), so ORDER is what makes this
   win. Keep it after. */
.journey-table .sbs-head th { color: #52606d; font-size: .78rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
/* "Your Reading Skills" matches the "Reading Support" heading beside it at 1.15rem (Ron, 2026-09-06) — they
   are the two column heads of the printed report and were a step apart in size. SCOPED to .strand-table:
   .sbs-head is shared with the Journey table, whose header is styled as column labels just above. */
/* Size AND weight AND colour, so the two printed column heads are literally the same style. The h3 pins
   its colour above rather than inheriting, or this pair drifts the moment something upstream changes. */
.strand-table .sbs-head th { font-size: 1.15rem; font-weight: 600; color: var(--rp-head); }
/* Only the FIRST cell is the section's title. "No. Items", "Score" and "Mastery" are column labels, and
   they now take the SAME small grey caps as every other column label on the report -- the support table's
   SUPPORT / VOCAB row and Journey's LEVEL / SCORE / DATE. .78rem / 600 / uppercase / .04em / #52606d.
   A size step-down was tried here on 2026-09-07 and reverted, on the grounds that shrinking a header cell
   re-flows the column widths under it. That reasoning was sound and is now spent: in PRINT the table is
   table-layout: fixed at 64/15/21% (see the print block), so the widths cannot move at all; on SCREEN the
   table is still auto-layout, and these three headers are the widest thing in their columns, so those
   columns do narrow -- which gives the width back to the strand names in column 1, where it is wanted.
   The weight goes 400 -> 600 with the rest of the style. The old 400 existed because 600 at 1.15rem made
   all four cells read as headings; at .78rem uppercase grey they read as what they are, so the weight no
   longer has to carry that distinction. The first cell keeps 1.15rem: it is the section title, paired with
   "Reading Support" beside it, and cells align on their baselines so the small caps sit on the title's. */
.strand-table .sbs-head th:not(:first-child) { font-size: .78rem; font-weight: 600; color: #52606d;
                                               text-transform: uppercase; letter-spacing: .04em; }

.band-pending { background: #eef1f5; color: #5a6b82; }

/* Certification report */
.certificate { max-width: 640px; margin: 0 auto; }
.cert-frame { text-align: center; border: 3px double var(--rp-accent); border-radius: 12px; padding: 2rem 1.5rem; background: #fff; }
.cert-frame.cert-honors { border-color: var(--rp-gold); }
.cert-kicker { font-size: .85rem; letter-spacing: .02em; color: var(--rp-accent); margin: 0 0 .5rem; }
.cert-title { font-size: 1.8rem; color: var(--rp-head); margin: 0 0 .3rem; }
.cert-distinction { font-weight: 700; color: var(--rp-gold-text); letter-spacing: .05em; margin: 0 0 .5rem; }
.cert-line { color: #52606d; margin: .6rem 0 .1rem; }
.cert-student { font-size: 1.4rem; font-weight: 700; color: var(--rp-head); margin: .1rem 0; }
.cert-level { font-size: 1.5rem; font-weight: 700; color: var(--rp-accent); margin: .1rem 0 1rem; }
.cert-stats { display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap; margin: 1rem 0; }
.cert-stat { display: flex; flex-direction: column; align-items: center; gap: .25rem; }
.cert-stat-num { font-size: 1.6rem; font-weight: 700; color: var(--rp-head); }
/* The single-verdict certificate: one hero number, so give it a touch more presence + breathing room. */
.cert-stats--single { margin: 1.35rem 0; }
.cert-stats--single .cert-stat-num { font-size: 2rem; }
.cert-stat-label { font-size: .8rem; color: #52606d; }
.cert-date { color: var(--rp-muted); font-size: .85rem; margin-top: 1rem; }

/* ---- roster registration (/roster) ---- */
/* The roster page had NO container rule while /class had .tc (1000px, centred), so the two teacher screens
   rendered at different widths. Match .tc. Deliberately no position/transform here: the print rule below
   absolutely-positions .rr-print-sheet against the initial containing block, and a positioned ancestor would
   re-anchor it and break the printed sign-in cards. */
.rr { max-width: 1000px; margin: 0 auto; }

/* Shared teacher chrome (TeacherIdentity): nav between the two teacher screens + back to the student app,
   which neither page had — you had to type "/". */
.tp { max-width: 1000px; margin: 0 auto; }
.tp-tabs { display: flex; gap: .4rem; margin: 0 0 1rem; }
.tp-tab { padding: .3rem .9rem; border-radius: 999px; font-weight: 600; font-size: .9rem; cursor: pointer;
          color: var(--rp-head); border: 1px solid #cfd8dc; background: #fff; }
.tp-tab:hover { border-color: var(--rp-accent); }
.tp-tab.on { background: var(--rp-accent); border-color: var(--rp-accent); color: #fff; }
.tp-home { margin-left: auto; color: var(--rp-accent); text-decoration: none; font-weight: 600; font-size: .9rem; }

/* Class list */
.tp-title { text-align: center; font-size: 1.25rem; font-weight: 700; color: var(--rp-head); margin: 0 0 .6rem; }
/* Letterhead: who is looking (left) and where she is (right). Larger and bolder than body text — this is
   the identity block on a product whose output is records, not a caption. */
.tp-id { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem;
         max-width: 1000px; margin: 0 auto .9rem; }
.tp-id-who { display: flex; flex-direction: column; font-size: 1.05rem; font-weight: 700; color: var(--rp-head); line-height: 1.35; }
.tp-id-where { display: flex; flex-direction: column; text-align: right; font-size: 1.05rem; font-weight: 700; color: var(--rp-head); line-height: 1.35; }
.tp-panel { border: 1px solid var(--rp-line); border-radius: 10px; overflow: hidden; }
.tp-empty { text-align: center; color: #52606d; font-size: .92rem; margin: 0; padding: .9rem 1rem; border-bottom: 1px solid var(--rp-line); }
.tp-table { width: 100%; border-collapse: collapse; font-size: .92rem; }
/* Alignment is set by COLUMN CLASS, on the header and the cell together, so a header always sits over its
   own data. The previous pair was a specificity trap: `th:not(.tp-num)` outranks a plain `th`, so the
   `text-align: center` below it never applied to an ordinary header, and the one numeric column ended up
   centred above right-aligned figures (Ron, 2026-08-24). */
.tp-table th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; color: var(--rp-muted);
               font-weight: 700; padding: 8px 12px; background: var(--rp-bg-soft); border-bottom: 1px solid var(--rp-line); }
.tp-table th.tp-mid, .tp-table td.tp-mid { text-align: center; }
.tp-table td { padding: 10px 12px; border-bottom: 1px solid var(--rp-line); vertical-align: middle; }
.tp-num { width: 92px; text-align: center; }
.tp-prompt { color: var(--rp-muted); }
.tp-attn { background: #fff6e5; color: #8a5a00; border-radius: 999px; padding: .1rem .55rem; font-weight: 600; font-size: .8rem; }

/* Inside a class */
.tp-crumb { font-size: .9rem; color: #52606d; margin: 0 0 .7rem; display: flex; gap: .45rem; align-items: center; }
.tp-crumb-now { font-weight: 700; color: var(--rp-head); }
.tp-classacts { display: flex; gap: .5rem; flex-wrap: wrap; margin: 1rem 0 .75rem; }
/* The removal row inside the class-settings dialog. Pushed to the FOOT, below Save/Cancel and set off by
   a rule, so the destructive action is never adjacent to the one a teacher came here to use. */
.tp-home:hover { text-decoration: underline; }
.tp-school { display: flex; align-items: center; gap: .5rem; margin-bottom: .35rem; }
.tp-school input { flex: 1 1 auto; }
/* The identity badge is a button now (it opens the profile), but must look like the old static badge. */

.rr-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; }
.rr-cross { color: var(--rp-accent); text-decoration: none; font-weight: 600; font-size: .9rem; white-space: nowrap; }
.rr-cross:hover { text-decoration: underline; }
.rr-classes { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1rem; }
.rr-cls { display: inline-flex; align-items: center; gap: .4rem; border: 1px solid #cfd8dc; background: #fff; color: var(--rp-head);
    border-radius: 999px; padding: .35rem .85rem; font-size: .9rem; cursor: pointer; }
.rr-cls:hover { border-color: var(--rp-accent); }
.rr-cls.on { background: var(--rp-accent); border-color: var(--rp-accent); color: #fff; }
.rr-cls-add { border-style: dashed; color: #52606d; }
.rr-src { font-size: .7rem; text-transform: uppercase; letter-spacing: .04em; background: #eef2f4; color: #52606d; border-radius: 4px; padding: 0 .3rem; }
.rr-cls.on .rr-src { background: rgba(255,255,255,.25); color: #fff; }
.rr-src-big { font-size: .8rem; text-transform: none; letter-spacing: 0; padding: .35rem .6rem; }
.rr-inline { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: 1rem; }
.rr-clsbar { display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: .75rem; }
.rr-clsmeta { display: flex; gap: 1rem; align-items: baseline; flex-wrap: wrap; }
.rr-count { font-weight: 700; color: var(--rp-head); }
.rr-code { color: #52606d; font-size: .9rem; }
.rr-code strong { font-family: ui-monospace, Consolas, monospace; letter-spacing: .05em; color: var(--rp-head); }
.rr-clsactions { display: flex; gap: .5rem; flex-wrap: wrap; }
.rr-note { color: #8a5a00; background: #fff6e5; border: 1px solid #f0dca8; border-radius: 8px; padding: .5rem .75rem; font-size: .88rem; margin: 0 0 .75rem; }
.rr-table td, .rr-table th { vertical-align: middle; }
.rr-mono { font-family: ui-monospace, Consolas, monospace; font-size: .85rem; }
/* SUSPENSION IS SAID, NOT DIMMED (Ron, 2026-09-01). This was `opacity: .55` on the row, and it failed
   three different ways.
     1. CONTRAST. Fading a row fades the DATA. It dragged the level strip's quiet chips to ~2.1:1 even
        after those were rebuilt to 5.2:1, so the accessibility fix would have been silently undone for
        exactly the students a teacher is most likely to be checking on.
     2. IT SAID NOTHING TO A SCREEN READER. Suspension appeared nowhere in the markup -- not in the row,
        not in the cell, only in the row menu's Suspend/Restore label, which has to be OPENED to be
        heard. A suspended student read as identical to an active one. WCAG 1.4.1: information carried
        by visual presentation alone.
     3. IT SAID THE WRONG THING TO EVERYONE ELSE. A faded control reads as DISABLED, and the row menu
        holds Restore -- so the one row where those actions matter most was the one row where they
        looked switched off. That was patched once with `td:not(.tc-mid)`, which is a workaround for the
        whole approach rather than a bug in it: opacity creates a compositing group at the ancestor, and
        no descendant can climb back out of it with `opacity: 1`.
   The replacement is a word in the name cell and NOTHING ELSE CHANGED, so every other figure in the row
   stays at full contrast and readable. It reuses the License-expired badge that already sits there. */
.rr-status { font-size: .75rem; font-weight: 600; border-radius: 999px; padding: .1rem .55rem; }
.rr-status-active { background: #e3f4ea; color: #1c6b3f; }
.rr-status-suspended { background: #f1eef0; color: #7a6a72; }
/* Applied to a DIV inside the cell, never to the <td> itself — display:flex on a table cell removes it
   from table layout, so it no longer stretches to the row height and its border draws high. */
/* .rr-actions is GONE (Ron, 2026-08-24). It held the three action buttons in the class table's last column;
   they are now a single ⋯ menu (.rr-rowmenu), which is what removed both the row-height problem its
   flex-wrap caused and the overflow its nowrap caused. Deleted rather than left behind, because a rule that
   styles nothing is a rule someone later assumes is load-bearing. */
.rr-link { background: none; border: none; padding: 0; color: var(--rp-accent); font: inherit; font-size: .85rem; cursor: pointer; }
.rr-link:hover { text-decoration: underline; }
.rr-link-danger { color: #b3261e; }

/* add/edit modal */
/* The focus-trap sentinels (see rpFocusEdge in js/menu.js). ABSOLUTE so they leave the backdrop's flex
   flow entirely and cannot nudge the centred panel; 1px rather than 0 because a zero-sized element is
   treated as hidden by getClientRects, which is the very test the helper uses to find focusable controls.
   NOT display:none and NOT visibility:hidden -- either would make them unfocusable, which is the one thing
   they exist to be. */
.rr-focus-edge { position: absolute; top: 0; left: 0; width: 1px; height: 1px; opacity: 0; }
.rr-modal-back { position: fixed; inset: 0; background: rgba(20,32,44,.45); display: flex; align-items: center; justify-content: center; z-index: 50; padding: 1rem; }
/* max-height + overflow on the BASE modal, not just .rr-modal-wide (Ron, 2026-09-09). The backdrop centres
   its child with flex, and a flex-centred box taller than its container overflows in BOTH directions with
   no scrollable ancestor -- so a tall dialog loses its title off the top and its Save button off the
   bottom, unreachably. Edit Student had grown to ~660px, against ~650px of usable height on a 1366x768
   Chromebook. This is the floor under every dialog, whatever any one of them is laid out like. */
/* 24px ON ALL FOUR SIDES (Ron, 2026-09-14). Was 1.4rem/1.5rem -- 22.4px top and bottom against 24px at the
   sides, a 1.6px difference nobody chose and nobody could see, but which meant the dialog box had two
   numbers instead of one. 24px is Material's and Fluent's dialog padding (Bootstrap uses 16, macOS 20), so
   this is a rounding to the conventional number rather than a redesign. It is also the distance BELOW the
   buttons, and .rr-modal-actions sets the same 24px above them -- see there. */
.rr-modal { background: #fff; border-radius: 12px; padding: 1.5rem; width: min(420px, 100%);
            max-height: 88vh; overflow-y: auto; box-shadow: 0 12px 40px rgba(0,0,0,.25); }
.rr-modal-title { margin: 0 0 1rem; font-size: 1.2rem; color: var(--rp-head); }
.rr-field { display: flex; flex-direction: column; gap: .3rem; font-size: .85rem; color: #52606d; margin-bottom: .9rem; }
.rr-field input, .rr-field select, .rr-field textarea { font-size: 1rem; padding: .5rem .6rem; border: 1px solid #cfd8dc; border-radius: 8px; color: var(--rp-head); }
/* THE OPEN LIST, NOT THE CLOSED CONTROL (Ron, 2026-09-16). Chrome paints a select's popup at the select's
   own font-size, so a 1rem control opened a list in what reads as large print next to a dense dialog.
   Setting it on <option> shrinks the LIST while the closed control stays 1rem -- which it must, because
   Grade sits in a row beside First name and Last name, and a smaller closed select would be visibly out of
   step with the two inputs next to it.
   NOT the control's height: `.rr-field input/select` are pinned to one height on purpose (see the rule
   below) because a select sizes itself from its own metrics and three controls in a row otherwise came out
   three heights. Raising it here alone would undo that. */
.rr-field select option { font-size: .9rem; }
/* SAME HEIGHT for every control in a field, whatever its type. Matching font-size and padding is not enough:
   a <select> sizes itself from its own internal metrics, and date/number inputs add picker and spinner
   furniture, so three controls sharing a row came out three heights — Term visibly shorter than Licenses and
   Purchased beside it (Ron, 2026-08-24). An explicit height with border-box makes them agree; checkbox and
   radio are excluded because a fixed height would deform them, and textarea keeps its own (it must grow). */
.rr-field input:not([type="checkbox"]):not([type="radio"]), .rr-field select {
    box-sizing: border-box; height: 2.5rem; line-height: 1.25;
}
/* TWO TEXT ROLES IN A DIALOG, and they were one class until 2026-09-15 (Ron, found in QA on the Sign Out
   dialog -- "the text appears small").

   .rr-modal-body is the dialog's OWN MESSAGE -- the sentence it exists to say. Full size, dark, no negative
   top margin because it sits directly under the title. Both delete confirms were using the hint style for
   this, so the text explaining an irreversible action was set as a footnote: smaller AND fainter than the
   body of the student's routine "Are you sure?" (which is 1rem at 13.5:1).

   .rr-modal-hint is the SMALL ANNOTATION UNDER A FIELD -- "Leave blank for no limit", "At least 8
   characters". 13px and grey are right for that and wrong for anything carrying the message.
   Its -.2rem top margin is what tucks it under the field above; that is the tell that it was never meant
   to lead.

   COLOUR: #7b8794 was 3.66:1 on white and FAILED WCAG 2.2 AA 1.4.3, which needs 4.5:1 for normal text --
   and at .82rem this is normal text (large starts at 24px, or 18.7px bold). #68737f is 4.83:1 and close
   enough in tone that nothing reads as redesigned. The same #7b8794 is used ~43 more times across the
   stylesheet at similar sizes; that sweep is logged separately in docs/Outstanding.md, not done here. */
/* 15px, not 16 (Ron, 2026-09-15, reading the delete confirm on screen): at 1rem the body read HEAVY in a
   420px box -- a short, blunt sentence set at reading size fills the dialog and starts to shout, which is
   the wrong tone for a question. 15px still sits above Material's and Fluent's 14px body and is nowhere
   near the 13px caption it replaced. Expressed in rem so it grows with the reader's default font size
   (WCAG 1.4.4 Resize Text); .9375rem is exactly 15px at a 16px root.
   ONE SIZE FOR ALL EIGHT, not a special case for the confirms: a per-dialog exception here is how the
   button sizes became three numbers with no rule. */
.rr-modal-body { font-size: .9375rem; color: #52606d; line-height: 1.5; margin: 0 0 1rem; }
.rr-modal-hint { font-size: .82rem; color: #68737f; margin: -.2rem 0 1rem; }
.rr-modal-err { font-size: .85rem; color: #b3261e; margin: 0 0 1rem; }
/* 24px OF CLEAR AIR ABOVE THE BUTTONS, on every dialog (Ron, 2026-09-14). It matches the 24px of padding
   below them in .rr-modal, so the footer sits in a symmetrical band: the buttons are anchored to the bottom
   of the card rather than trailing the last field.

   It replaces a gap nobody chose. The old distance was ~32px built from two unrelated rules stacking --
   .rr-field's margin-bottom (.9rem) plus .btn-next's margin-top (1.1rem, which SURVIVES here because flex
   items do not collapse their margins). That 1.1rem is meant for stacked page layouts, and it is zeroed
   below.

   MARGIN, NOT PADDING, and this reverses the 2026-09-12 note that used to sit here. Margin-top was tried
   then and "did nothing" -- true at the value tried: adjacent siblings COLLAPSE their vertical margins to
   the larger of the two, so anything under .rr-field's .9rem was discarded silently. At 1.5rem the collapse
   becomes the FEATURE. It absorbs whatever bottom margin the preceding element happens to carry -- a field
   (.9rem), a hint (1rem), an error (1rem) -- and the gap is 24px in every case. Padding would ADD to those
   instead, giving three different gaps in three dialogs. That is the difference between a number and a
   standard, and it is why .rr-actions-split no longer needs a rule of its own. */
.rr-modal-actions { display: flex; gap: .5rem; justify-content: flex-end; flex-wrap: wrap;
                    margin-top: 1.5rem; }
/* Destructive LEFT, primary RIGHT. space-between pins the two groups to opposite ends, so the
   right-most control is Cancel — the safe one — rather than the delete. */
/* The 20px this rule used to add (Ron, 2026-09-12) is GONE, and so is its copy of the margin-top reset:
   .rr-modal-actions now owns both, at 24px, for every dialog (2026-09-14). The reasoning that earned the
   gap still stands and is quoted where it now lives -- the buttons are not another field, and the gap
   separates "here is what you are changing" from "here is what you do about it". What changed is only that
   it stopped being true of one dialog. All this rule owns now is the LAYOUT it was always about:
   destructive left, primary right. */
.rr-actions-split { justify-content: space-between; align-items: center; }
.rr-actions-left, .rr-actions-right { display: flex; gap: .5rem; }
/* The rule sits BETWEEN the buttons and this note (Ron, 2026-08-22) — the note reads as a closing
   footnote to the whole dialog rather than a caption on the Delete button. */
.rr-dangernote { margin: .9rem 0 0; padding-top: .9rem;
                 border-top: 1px solid var(--rp-line, #e3e8ef); }

/* Bulk paste ("Paste a list") — wider modal, the textarea, and the parsed preview. */
/* 680px, up from 560 (Ron, 2026-09-16). The Add Students dialog did not fit -- ~827px of content against a
   max-height of 88vh, which is ~676px on a 1366x768 Chromebook -- and width is what lets the two radio
   fieldsets sit side by side instead of stacked, worth ~104px of the ~270 recovered.

   IT HAD TO APPLY TO BOTH DIALOGS, not just the paste one, and that is not scope creep: Add Students and
   Add/Edit Student are two SHAPES OF ONE FLOW toggled by a radio inside them. Giving them different widths
   would make the dialog jump size when the teacher flips the radio. The extra width suits Edit Student
   anyway -- it lays out a three-column name row and two two-column rows. */
.rr-modal-wide { width: min(680px, 100%); max-height: 88vh; overflow-y: auto; }

.rr-paste-ta { font-family: ui-monospace, Consolas, monospace; font-size: .9rem; line-height: 1.5; resize: vertical; width: 100%; box-sizing: border-box; }
/* THE LABEL SITS ABOVE THE BOX (Ron, 2026-09-16), the way .rr-field's label sits above its input. A <legend>
   straddles the top BORDER, so these two groups were labelled in a way nothing else in the dialog was --
   "One student per line" was already right and they were the odd ones.
   The fix is to move the BORDER, not the legend: the fieldset gives up its box to an inner .rr-fieldset-box,
   so the legend renders as a plain block above it. The fieldset and legend STAY, because they are what names
   the radio group for a screen reader; only the painting moved.
   .3rem below the legend is .rr-field's own label-to-control gap, and the type matches it too -- that is
   what "consistent" means here. */
.rr-order, .rr-choice { border: 0; padding: 0; margin: 0 0 .9rem; }
.rr-order legend, .rr-choice legend { display: block; width: 100%; padding: 0; margin: 0 0 .3rem;
                                      font-size: .85rem; color: #52606d; }
.rr-fieldset-box { border: 1px solid #dbe3e7; border-radius: 8px; padding: .6rem .8rem .7rem; }
.rr-order label, .rr-choice label { display: flex; align-items: center; gap: .45rem; padding: .18rem 0; color: var(--rp-head); cursor: pointer; }
.rr-order-eg { font-family: ui-monospace, Consolas, monospace; font-size: .82rem; color: #52606d; background: #f3f6f8; border-radius: 4px; padding: .05rem .3rem; }
/* .rr-addone HAS NO STYLES OF ITS OWN as of 2026-09-09 and is kept only as a markup hook. It capped these
   blocks at 22rem/352px, which was right while they were columns of stacked fields -- a lone input
   stretched to the full dialog looks broken. They hold ROWS now, and a cap narrower than the dialog would
   squeeze two fields into the space one used to have. .rr-form-row's flex-basis does the sizing instead. */
/* Credentials sit apart from name/grade: those describe the person, these are how they get in. */
.rr-creds { margin-top: 1rem; padding-top: .9rem; border-top: 2px solid var(--rp-line, #e3e8ef); }
/* Reading support reads as its own group, ruled off like the credentials above it -- three unrelated things
   (who they are / how they sign in / how content reaches them) in one dialog need the seams drawn. */
.rr-support { margin-top: 1rem; padding-top: .9rem; border-top: 2px solid var(--rp-line, #e3e8ef); }
/* The two delivery selects stop ~24px short of their column (Ron, 2026-09-09). At a full half-row they are
   ~248px for a longest option of "Passages and questions", which measures ~200px including the padding and
   the dropdown arrow -- so the control was wider than anything it can ever show.
   Capped on the SELECT, not on the field: the field keeps its half of the row, so these two controls stay
   left-aligned with Username/Password and First/Last above them. Capping the field would pull both columns
   inward and break that vertical line. 14rem rather than a pixel value so it tracks the font -- the margin
   over the longest option is about 25px, not a rounding error. */
.rr-support .rr-field select { max-width: 14rem; }
.rr-creds input[disabled] { background: #f3f6f8; color: #52606d; cursor: not-allowed; }
/* "Not started" spanning the eight outcome columns. Muted and italic so a row with no work reads as a
   state rather than as data that failed to load. */
.rr-nowork { color: var(--rp-muted); font-style: italic; }
/* The merged class table is wide; keep the strand columns tight and stop names wrapping. */
.rr-table td, .rr-table th { white-space: nowrap; }
.rr-table .band-pill { min-width: 3.1rem; text-align: center; }
.rr-order-note { font-size: .82rem; color: var(--rp-muted); margin: .45rem 0 0; line-height: 1.5; }
.rr-paste-count { color: var(--rp-head); margin: 0 0 .75rem; }
/* The heading over the HELD names in the paste preview (Ron, 2026-09-13). Plain text, no box: the amber
   .rr-note it replaces was a warning bolted onto one list, and this is a heading over a list of its own --
   the split is what carries the meaning now, so the type only has to name it. The top margin is the gap
   between the two lists and is the only thing separating them, so it is larger than the bottom. */
.rr-held-lede { font-size: .88rem; color: #52606d; line-height: 1.5; margin: 1.25rem 0 .6rem; }
.rr-preview-head { display: grid; grid-template-columns: 1fr 1fr 4.5rem; gap: .4rem; padding: 0 0 .25rem 1.9rem;
                   font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; color: var(--rp-muted); font-weight: 700; }
.rr-preview { margin: 0 0 .5rem; padding-left: 1.9rem; max-height: 42vh; overflow-y: auto; }
.rr-preview li { padding: .18rem 0; color: var(--rp-head); display: grid; grid-template-columns: 1fr 1fr 4.5rem; gap: .4rem; align-items: center; }
.rr-preview-off { opacity: .5; }
.rr-preview-in { font-size: .92rem; padding: .3rem .45rem; border: 1px solid #cfd8dc; border-radius: 6px; color: var(--rp-head); width: 100%; box-sizing: border-box; }
.rr-preview-flags { grid-column: 1 / -1; display: flex; gap: .35rem; flex-wrap: wrap; }
.rr-preview-flags:empty { display: none; }
.rr-preview-grade { font-size: .82rem; padding: .3rem .2rem; border: 1px solid #cfd8dc; border-radius: 6px; color: var(--rp-head); background: #fff; width: 100%; box-sizing: border-box; }
.rr-preview-flag { font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; background: #eef2f4; color: #52606d; border-radius: 4px; padding: .05rem .35rem; }
.rr-preview-dupe { background: #fff6e5; color: #8a5a00; }

/* printable sign-in cards */
/* 1.75rem, not 1rem: the toolbar needs clear air under it before the sheet begins, or the buttons read
   as part of the document rather than as controls over it (Ron, 2026-08-22 — +12px). */
/* Back on the LEFT, Print on the RIGHT margin — the same split the Class Settings footer uses: leaving
   is on one side, the action you came for is on the other. */
.rr-print-bar { display: flex; gap: .5rem; justify-content: space-between; align-items: center;
                margin-bottom: 1.75rem; }
/* The card picker sits between Back and Print, which space-between then centres for free. Sized to sit level
   with the 32px teacher buttons beside it, so the bar reads as one row of controls rather than three heights.
   It prints nothing: .rr-noprint is on the bar itself. */
.rr-cardpick { display: inline-flex; align-items: center; gap: .4rem; font-size: .85rem; color: #52606d; }
.rr-cardpick select { font: inherit; color: var(--rp-ink); padding: .35rem .5rem; border-radius: 8px;
                      border: 1px solid var(--rp-line); background: #fff; max-width: 16rem; }
/* THE TEACHER BUTTON SIZE: 32px tall (Ron, 2026-08-22) = 1rem line-height + .5rem padding top and bottom.
   line-height is PINNED because a button otherwise inherits `normal`, which varies by font — the height
   would be approximate rather than a number you can rely on.
   Expressed in rem, never a fixed height: the target is WCAG 2.2 AA, which includes 1.4.4 Resize Text, and
   a px height clips its own label when a low-vision user raises their default font size. Padding grows with
   the text; a fixed height cannot.
   32px sits well above the 24px floor in 2.5.8 Target Size (Minimum). The student app's full-size .btn-next
   is untouched — that is what a child taps on a Chromebook. */
/* Icon and label on one baseline. The print bar shares .btn-sm with the rest of the teacher buttons,
   which does not set a flex box — only the buttons carrying an icon need one. */
.rr-print-go { display: inline-flex; align-items: center; gap: .35rem; }
/* .rr-print-title is GONE (2026-09-09). All four printed sheets now head themselves with the certification
   report's centred .report-title / .report-title-1, so a second, left-aligned title style had no users left. */
.rr-print-sub { color: #52606d; font-size: .9rem; margin: 0 0 1.2rem; }

/* THE SHEET LETTERHEAD -- teacher and class left, district and school right (Ron, 2026-09-03). Borrows the
   SHAPE of the app letterhead (.tp-id) and not the rule: that one carries `max-width: 1000px; margin: 0 auto`
   for the app shell, which would centre and cap a printed sheet inside its own page margins.

   A RULE UNDERNEATH, and it is load-bearing rather than decoration. Four unlabelled lines of names running
   straight into a big heading read as one jumbled block; the border is what makes them a letterhead and the
   heading a title. It is a border for the same reason the band outlines are: a printer with "background
   graphics" off drops fills and keeps borders, and this sheet exists to be printed.

   align-items: flex-start so the two columns hang from the same top line whether or not either has both of
   its lines -- an unset district must not float the school up out of alignment with the class name. */
/* NO RULE UNDER THE LETTERHEAD ON ANY SHEET (Ron, 2026-09-12). It came off Class Strands and Class Standards
   a few hours apart, each time via the .rr-sheet-head wrapper; the sign-in cards were the last sheet still
   drawing it, because they carry no third letterhead line and so never needed the wrapper.
   TAKEN OFF THE BASE RULE rather than overridden a third time -- all three sheets now want the same thing, so
   the agreement belongs in the one rule they share. The .7rem of padding goes with it: it existed to keep the
   text off that border, so it has nothing left to clear. The 1rem margin stays, and is now the whole of the
   gap under the block. */
.rr-sheet-id { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem;
               margin: 0 0 1rem; }
.rr-sheet-who, .rr-sheet-where { display: flex; flex-direction: column; font-size: 1.05rem; font-weight: 700;
                                 color: var(--rp-head); line-height: 1.35; }
.rr-sheet-where { text-align: right; }
/* The class name sits UNDER the teacher and is the same size, but it is the second fact, not the first. */
.rr-sheet-who span + span, .rr-sheet-where span + span { font-weight: 600; color: #52606d; }
/* A THIRD LETTERHEAD LINE: the count at the left, the date at the right (Ron, 2026-09-11). Only Class
   Strands wears it, which is why it wraps the shared fragment rather than living inside it -- the cards
   sheet's line under the letterhead is an instruction ("cut along the lines"), not metadata, and belongs
   where it is.
   NO RULE UNDER IT (Ron, 2026-09-11). The letterhead briefly kept .rr-sheet-id's border by moving it down
   to this row; it came off entirely a few minutes later. The block does not need a line to read as a block
   -- it is four lines of larger, darker type over a table of small centred numbers, and the table's own
   2px heading border is already the strongest horizontal on the sheet. A second rule 30px above it was
   competing with the one that means something.
   TAKING THE RULE OFF IS ALSO WHAT MOVED THE TABLE UP. The .7rem padding under this row existed only to
   keep the text off that border, so it left with it: the gap from this line to the column headings goes
   2.4rem -> 1.7rem, a 29% reduction, which is the "about 30%" that was asked for and arrives without a
   number being tuned. The 1rem margin below stays -- it is the gap itself, not clearance for anything. */
/* THE THIRD LINE IS SPACED LIKE THE FIRST TWO, which took removing one thing and stating another
   (Ron, 2026-09-11). The two stacks above set line-height: 1.35 explicitly; this row is a separate flex
   container, so it was inheriting the document's default (~1.2) and its line box was a different height --
   and .rr-sheet-id's .3rem of padding-bottom added ~5px on top of that, a gap lines one and two do not have
   between them. Zero the padding, state the same line-height, and the three lines step evenly.
   The .95rem size stays: it is smaller than the names on purpose, and size is not what was uneven. */
.rr-sheet-head .rr-sheet-id { margin-bottom: 0; }
.rr-sheet-meta { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
                 margin: 0 0 1rem; font-size: .95rem; font-weight: 600; color: #52606d;
                 line-height: 1.35; }

/* ---- CLASS SKILLS REPORT (docs/ClassSkillsReport_Plan.md) ------------------------------------------------
   Every student against the four reading skills. It lives inside .rr-print-sheet with the sign-in cards,
   which is what makes it printable: the print block above hides `body *` and reveals that container only. */
.cs-table { width: 100%; border-collapse: collapse; font-size: .9rem; margin: 0 0 1.6rem; }
/* THE THREE HEAVY RULES, the same treatment Class Standards uses (Ron, 2026-09-12): 2px of --rp-head under
   the headings and above each of the two summary rows. They mark the seams between KINDS of row -- headings,
   students, class average, total -- where the 1px --rp-line between students groups rows of the same kind.
   Matching the two sheets is the point: they are printed together, read together and filed together, and a
   reader should not have to learn two rule systems to know which lines mean a change of kind. The weight is
   COLOUR, not thickness -- --rp-head against --rp-line -- so it reads as emphasis in greyscale too. */
.cs-table th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
               color: #52606d; font-weight: 700; padding: 6px 10px; border-bottom: 2px solid var(--rp-head); }
/* 6px, NOT 7 (Ron, 2026-09-11) -- two pixels a row, bought to keep a 34-student class on two pages.
   MEASURED BEFORE IT WAS SPENT, because the obvious candidate was the wrong one: dropping the 1px rule
   between every student looks like the way to reclaim height, but 34 rules are 34px, under one row, while
   2px of padding across 34 rows is 68px and invisible. The rules earn their keep on a nine-column matrix
   read horizontally -- and the usual alternative, zebra striping, is unavailable here because browsers do
   not print background colours, which is the same reason the bands on this sheet are borders.
   WHAT MADE THE ROOM NECESSARY was the repeating letterhead: page two never carried one until 2026-09-11,
   and it costs that page ~160px, about four rows. The capacity did not shrink by accident.
   Screen as well as print, deliberately: this sheet IS a print preview, and it should not fit differently
   from the paper it is previewing. Class Standards is unaffected -- it wears .cs-table but sets its own
   cell padding. */
.cs-table td { padding: 6px 10px; border-bottom: 1px solid var(--rp-line); vertical-align: middle; }
/* A RULE EVERY SECOND STUDENT, not every one (Ron, 2026-09-12). Seventeen lines instead of thirty-four, on a
   sheet whose remaining busyness was the rules themselves once the coloured pills came off.
   LIGHTENING THEM WAS THE OTHER WAY AND IS NOT AVAILABLE: --rp-line is #d9e2ec, already near the floor at
   which a hairline survives a printer. Push it paler and it stops reading as quiet and starts disappearing,
   so thinning the COUNT is the only lever left.
   BLOCKS OF TWO, NOT FIVE. Five rows is ~185px with no horizontal anchor, and slipping a line is the
   characteristic error on a nine-column table that is read across rather than down. Two keeps an anchor
   within one row of everywhere. The cost is that a pair can read as a GROUPING -- two adjacent things look
   related in a way five do not -- which is the thing to watch for on paper; three per block is the fallback
   if it bites, and it is one digit here.
   ODD ROWS LOSE THEIR BORDER, so the rule falls under rows 2, 4, 6 and the blocks are (1,2), (3,4). The
   summary rows cannot be miscounted into this: they live in <tfoot>, so this tbody holds student rows and
   nothing else. A last block of one is fine either way -- the heavy 2px --rp-head above Class average
   follows it, and at equal collapse the wider, darker rule wins.
   Fenced off .cst-table, which wears .cs-table too and rules its own rows by strand. */
.cs-table:not(.cst-table) tbody tr:nth-child(odd) td { border-bottom: 0; }
/* THE RUNHEAD: a full-width row holding the sheet's letterhead, so that a thead repeats it on every printed
   page (2026-09-11). It is a LAYOUT cell and must not inherit any of the grid's own cell styling -- the 1px
   rule under every td above all, which would draw a line across the sheet under the letterhead.
   Its own bottom spacing is the gap before the column headings; .cs-table's 1.6rem bottom margin is the gap
   after the table, and the two are unrelated. */
.cs-runhead > td { padding: 0 0 .7rem; border-bottom: 0; }
/* The letterhead block was the sheet's first child until it moved in here, so it arrived with the margin
   collapsing it used to rely on. A table cell establishes a new block formatting context, so the h1's top
   margin no longer collapses out of the sheet -- it is zeroed here instead of being left to look like a
   deliberate gap above the title. */
.cs-runhead > td > .report-title { margin-top: 0; }
/* THE HEADER SELECTOR HAS TO OUT-SPECIFY .cs-table th (Ron, 2026-09-02). That rule is (0,1,1) and a bare
   .cs-skill is (0,1,0), so it lost regardless of order -- the five value headers sat LEFT over centred
   values, and no amount of reordering would have fixed it. Scoping to .cs-table th.cs-skill makes it (0,2,1).
   Student stays left: it is the only column read as text rather than scanned as a value. Gr and Level are
   untouched. */
.cs-table th.cs-skill { text-align: center; }
/* THE HAND-SPLIT HEADING (Ron, 2026-09-11). Two of these inside one th put INTEGRA over TION; see the
   comment on the th in ClassManage for why that word is split at all and why it carries no hyphen.
   display: block is the whole mechanism -- the spans are inline in the markup with no whitespace between
   them, so removing this rule restores INTEGRATION on one line and nothing else changes. Screen as well as
   print: the heading should not rearrange itself between what a teacher previews and what she prints. */
.cs-head-line { display: block; }
.cs-cell { text-align: center; }
.cs-name { color: var(--rp-head); white-space: nowrap; }
.cs-table td:not(.cs-name) { white-space: nowrap; }

/* PROVISIONAL: same number, NO PILL. A band on four passages of evidence would claim a confidence the data
   does not support, and the pill is the thing that reads as a verdict. Shape rather than shade, so it holds
   up in greyscale and on a printer -- the rule the level strip already follows.
   THE WORDS ARE IN THE PASSAGE COUNT, not in a tag. This used to say the row also carried "just started";
   that sticker was retired on 2026-09-03 once the stickers were defined on whether a student has BEGUN
   rather than on how much evidence her row holds. "2 of 20", in its own column, is what now says why these
   numbers are not yet a verdict. */
.cs-prov td { background: #fbfcfd; }
.cs-plain { color: #52606d; font-variant-numeric: tabular-nums; }
.cs-none { color: #616e7c; }
.cs-tag { display: inline-block; margin-left: .45rem; font-size: .72rem; font-weight: 600; color: #616e7c;
          background: #eef2f5; border-radius: 999px; padding: 1px 8px; text-indent: 0; }

/* THE BOOSTER MARK -- the †, the same glyph it is everywhere else in the app: "this figure blends own-level
   and supported work". That is literally true here since the cells moved to DisplayedScore on 2026-09-02, and
   the mark is only rendered when the capped booster actually beat the own score. (It was briefly a glyph of
   its own, while the cells showed the unblended own score and there was no blend to disclose.) No count -- a
   "2" beside a 58 invites a reconciliation that does not exist. Defined in the footnote and given an sr-only
   text equivalent, so it is never the sole carrier. */
/* A FIXED-WIDTH SLOT, present in every cell whether or not it holds the mark. The cells are centred, so a
   mark that only appears on some rows shifts those pills off the column's axis -- the width has to be spent
   everywhere or nowhere. inline-block for the width to apply at all; left-aligned inside so the glyph sits
   against the pill rather than drifting to the far edge of its own box. */
.cs-boost { display: inline-block; width: .9rem; text-align: center;
            color: #52606d; font-weight: 700; }

/* NO BANDS IN THIS TABLE AT ALL (Ron, 2026-09-11): no pills, and no legend to decode them. The cells hold a
   bare percentage on screen and on paper alike.
   THE ARGUMENT WAS ALREADY WRITTEN IN THIS FILE, against the pills rather than for them. Measured on the
   built report, the four fills sat at relative luminance .847-.935 -- a spread of .088 -- so on a mono
   printer all four bands were the same pale grey. Two rounds of mitigation followed: unfilled pills below
   Achieving so that ONE distinction survived greyscale, and a legend under the table giving the cut-offs.
   Both were ways of keeping a colour cue on a document defined by being printed in black and white.
   THE SECOND REASON IS THE STRONGER ONE: a band is a JUDGEMENT, and this sheet is read by the person whose
   judgement it is. Ron: "the percentage score is fine, they have their own interpretation and expectation."
   The Certification Report keeps its pills and deserves to -- there the pill carries the band NAME, so colour
   is reinforcement rather than the carrier, and it is one student's record for a parent who has no threshold
   of their own. The ROSTER keeps its pills too: that is the daily screen scan, and it is where "show me the
   class in colour" belongs.
   SCREEN AND PRINT TOGETHER, not print-only. Keeping pills on screen was considered and rejected: a pill is
   ~22px tall against ~17px for a bare number, so the preview would have fitted about four fewer rows per page
   than the printer and paginated differently from the paper it previews -- the exact defect the card sheet
   was restructured to remove earlier the same day, on a report whose two-page fit is tight.
   WHAT THE NUMBER HAD TO PICK UP: weight, so it holds the cell without a pill around it, and tabular figures,
   so a column aligns the way the pills used to align on their edges. Provisional rows keep .cs-plain and stay
   grey -- that distinction is LIGHTNESS, not hue, so unlike the bands it survives the printer this is for.
   Only the mastery cut-off outlived the legend, as a footnote: "Reached mastery" is still counted against it.
   .band-pill itself is untouched and still used by the roster and the student reports. */
.cs-score { color: var(--rp-head); font-weight: 600; font-variant-numeric: tabular-nums; }

/* The Average column carries the level score -- the figure promotion is decided on -- so it is ruled off from
   the four strands that feed it. */
.cs-avgcol { border-left: 1px solid var(--rp-line); }
/* The class average sits above the mastery total -- mastery is the benchmark the program acts on, the
   average is the class's position.
   BOTH SUMMARY RULES ARE NOW THE SAME WEIGHT. This one was 2px --rp-line and the total's 1px, set lighter so
   the pair would not compete. Class Standards had reached the opposite answer a day earlier and kept it:
   lightening the rule above its Average was tried and reverted, because the Average is the row a teacher
   reads across to and a heavy rule is what lifts it off the data rather than letting it read as one more row
   of it. Same argument, same table shape, so the two sheets now agree. */
.cs-avg th, .cs-avg td { border-top: 2px solid var(--rp-head); border-bottom: none; padding-top: 10px;
                         color: #52606d; font-variant-numeric: tabular-nums; }
.cs-avg th { text-align: left; font-size: .82rem; text-transform: none; letter-spacing: 0; font-weight: 700; }

/* THE MASTERY TOTAL, a real <tfoot> row so it sits under the columns it counts. */
.cs-total th, .cs-total td { border-top: 2px solid var(--rp-head); border-bottom: none;
                             padding-top: 8px; font-weight: 700; color: var(--rp-head); }
.cs-total th { text-align: left; font-size: .82rem; text-transform: none; letter-spacing: 0; }
.cs-total td { font-size: .95rem; font-variant-numeric: tabular-nums; }

/* The footnotes, as a list with a hanging marker column. list-style:none because the markers are authored
   (a dagger that has to match the one in the grid, and dashes that have to line up with it) -- a native
   bullet could not carry the dagger, and mixing a native bullet with an authored one would misalign them. */
.cs-foot { list-style: none; margin: 0; padding: 0; font-size: .8rem; color: #52606d; line-height: 1.55; }
.cs-foot li { display: flex; align-items: baseline; gap: .1rem; margin-bottom: .2rem; }
.cs-foot-mark { flex: 0 0 .9rem; text-align: center; font-weight: 700; }


/* CLASS-WIDE CERTIFICATION REPORTS (2026-09-08): many .report blocks in one .rp-print-doc. On screen it is
   a scrollable preview, so the pages need a visible boundary -- without one the stack reads as a single very
   long report and a teacher cannot tell whose page she is looking at. A rule and space, not a card: these
   ARE the printed pages, and framing them would show her something she is not about to get. */
.rr-certs > .report + .report { margin-top: 2.2rem; padding-top: 2.2rem; border-top: 2px solid var(--rp-line); }
/* Its own line under the toolbar now, so it is a block with margins rather than a flex child that had to
   align itself against the buttons. */
.rr-cert-status { color: #52606d; font-size: .85rem; margin: .1rem 0 .9rem; }
.rr-cert-empty { color: #52606d; max-width: 46rem; }


/* ---- CLASS STANDARDS REPORT (2026-09-09) --------------------------------------------------------------
   The roster against every CCSS anchor. Wide and short -- thirteen numeric columns against ~18 rows -- so
   everything here is about buying horizontal room at portrait width (748px printable): a narrow row-label
   column, small type, and tight cell padding. */
.cst-page { break-inside: auto; }
/* ONE HEADER ON SCREEN, ONE PER PAGE ON PAPER (Ron, 2026-09-11). Every section carries a header in the
   markup -- that is what makes each PRINTED page identify itself -- but a screen has no pages, so repeating
   the title, letterhead, date and "Page 2 of 3" three times down a scroll is previewing a pagination the
   reader cannot act on. Page numbers in particular mean nothing there.
   So the repetition is expressed in CSS rather than by rendering different content: one place holds the
   header, and the medium decides how often it appears. Class Strands reaches the same behaviour a different
   way -- its header is a thead row, which browsers repeat per printed page on their own.
   The rule replaces what the header was doing on screen: showing where one block of students ends. Same
   screen-rule / print-break pairing the class-wide Certification sheet uses (.rr-certs > .report+.report). */
.cst-page + .cst-page .cst-head { display: none; }
.cst-page + .cst-page { margin-top: 2rem; padding-top: 2rem; border-top: 2px solid var(--rp-line); }
/* The title repeats per page now, so it cannot keep .report-title's 1.25rem bottom margin -- that was sized
   for one title at the top of a sheet, and three of them would spend the space the extra header costs. */
.cst-page .report-title { margin: 0 0 .5rem; }
/* Date and page mark, under the letterhead: the quietest line on the page, because it is the one a reader
   only consults when something has gone wrong -- a page out of order, or a report she cannot date. */


/* THE KEY, laid out as a wrapping grid rather than a 12-row table. Twelve rows of name/grade/level would
   cost more vertical space than the matrix it explains; in three or four columns it is a few lines. */
/* THE KEY IS A TABLE (Ron, 2026-09-09): four fixed columns, three rows for a full page of twelve, codes
   running left to right so 01-04 is the first row. table-layout: fixed is what gives every cell exactly a
   quarter of the sheet whatever it holds -- an auto table would hand its width to whichever cell has the
   longest name and leave the other three cramped.
   TWO LINES PER CELL, name above grade and level. At ~187px a cell cannot hold code, name, grade and level
   on one line without cutting the name to a few characters, and the name is the field the key exists for.
   Splitting them gives the name nearly the whole cell and truncates only the genuinely long ones. */
/* --cst-code-w is the code column's width, and it is a VARIABLE because two rules depend on it: the code
   itself, and the indent that lines the grade/level line up under the NAME rather than under the code.
   Two hand-tuned numbers would drift apart the first time the type size moved. */
/* THE KEY IS BOXED (Ron, 2026-09-12). It is a lookup object rather than data -- read once, then consulted --
   and a rule around a legend is the conventional way to say so. This is not in tension with the coloured
   pills coming off the Strands sheet the day before: those were thirty-four repetitions of a cue that decoded
   nothing on paper, this is one rectangle around one distinct thing.
   IT ALSO REPLACES A BOUNDARY THAT WAS LOST. The letterhead's own rule came off both sheets on 2026-09-11, so
   the key was left floating between a header with no edge and a matrix with heavy ones, reading as a tail of
   the header rather than as a thing of its own.
   THE BORDER IS ON THIS WRAPPER, NOT THE TABLE, so the <caption> falls inside it -- a caption renders outside
   the table's border box, so bordering .cst-key would have stranded its own label above the box.
   The 6px top is Ron's, measured by eye against the letterhead above it. The 1rem bottom is the margin the
   table used to carry; it moved out here with the border. */
.cst-keybox { margin: 6px 0 1rem; padding: .45rem .6rem .3rem; border: 1px solid var(--rp-line);
              border-radius: 6px; }
.cst-key-cap { caption-side: top; text-align: left; font-size: .7rem; text-transform: uppercase;
               letter-spacing: .04em; font-weight: 700; color: var(--rp-muted); padding-bottom: .25rem; }
.cst-key { --cst-code-w: 1.45rem; table-layout: fixed; width: 100%; margin: 0;
           font-size: .9rem; color: #52606d; }
/* NO WIDTH ON THE CELLS (2026-09-11). The column count is now computed from how many students are on the
   page — see StdKeyRows — so a hardcoded 25% would be right only at four columns and would squeeze or
   strand the rest. table-layout: fixed divides the width equally among however many cells the first
   row holds, which is exactly the behaviour wanted: the rule lives in C# and the CSS follows it. */
.cst-key td { padding: .1rem .6rem .35rem 0; vertical-align: top; }
.cst-key-top { display: flex; align-items: baseline; gap: .35rem; min-width: 0; }
.cst-code { flex: 0 0 var(--cst-code-w); font-family: ui-monospace, Consolas, monospace; font-weight: 700;
            color: var(--rp-head); }
/* min-width: 0 is what lets a flex child shrink below its content and actually ellipsis -- without it the
   name pushes the cell wider and nothing truncates. */
/* flex: 0 1 auto -- SHRINK but do not GROW (Ron, 2026-09-12). It was 1 1 auto, so the name filled the whole
   key row and pushed the substitution mark after it out to the right-hand edge, a centimetre from the name it
   belongs to. Shrinking is the half that matters: with min-width: 0 it is what lets a long name ellipsis
   instead of widening the column. Growing bought nothing, because the text is left-aligned either way. */
.cst-key-name { flex: 0 1 auto; min-width: 0; color: var(--rp-head);
                white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Indented to the NAME's left edge -- the code's width plus the flex gap above it — so each entry reads
   as a name with its details beneath, rather than two lines that happen to share a cell. */
/* The substitution mark in the key: this column is scored on the level the student just completed. Raised and
   small, like the booster dagger it sits alongside in the reader's mind, and line-height: 0 so a superscript
   cannot stretch the key's rows. */
.cst-sub-mark { font-size: .7em; line-height: 0; vertical-align: super; color: #52606d; margin-left: .1em; }
.cst-key-meta { display: block; padding-left: calc(var(--cst-code-w) + .35rem);
                white-space: nowrap; font-size: .82rem; }

/* .8rem below, half the 1.6rem .cs-table gives every table (Ron, 2026-09-09). Scoped to .cst-table and
   not changed on .cs-table itself: Class Strands has one table on a page with room, this one has a
   matrix and its footnotes competing for the same sheet. Later in the file than .cs-table, which is
   what makes it win at equal specificity — do not move it above that rule. */
.cst-table { table-layout: fixed; width: 100%; font-size: .92rem; margin-bottom: .8rem; }
/* BIGGER TYPE PAID FOR BY NARROWER NUMBER COLUMNS (Ron, 2026-09-09). The cells were never short of room --
   ten columns splitting ~576px gave each ~57px to hold a two-digit percent about 24px wide, so two thirds
   of every cell was blank and the numbers sat far apart while the type stayed small to fit a problem it did
   not have.
   The width is fixed, so tightening the numbers means the surplus has to go somewhere: it goes to the row
   labels, on the left, where it reads as a margin rather than as gaps between figures. Number columns are
   ~41px now against ~33px of content -- close enough to scan across a row, which is how this table is read.
   ONE KNOB, NOT TWO: only the LABEL column is sized, and the number columns split what is left evenly.
   Pinning them individually would tie this file to StdPerPage -- change the page size and the percentages
   would silently stop totalling 100 -- so the label width is the whole lever. At 36% that is ~269px for the
   labels and ~37px a number across twelve students plus Class, against ~31px of content. A page with fewer
   students shares the same remainder between fewer columns, so the LAST page runs slightly wider. */
.cst-table th.cst-rowhead { width: 36%; }
.cst-table th:not(.cst-rowhead), .cst-table td { text-align: center; padding: .22rem .06rem; }
/* THE STANDARD NAMES TAKE THE TABLE'S OWN SIZE AND CASE (Ron, 2026-09-09). This table also wears .cs-table,
   whose `th` rule is written for Class Strands' column headings -- .72rem, uppercase, letter-spaced. Correct
   there; wrong here, where the `th`s are ROW headers carrying "R.3 Individuals, events & ideas". They were
   rendering a third smaller than the numbers beside them, and shouting. The thead codes were already
   exempted; these were not, which is why only half the table looked right. Sentence case also matches how
   the Certification Report sets the same anchor names. */
.cst-table th.cst-rowhead, .cst-table th[scope="row"] { text-align: left; padding: .22rem .35rem .22rem 0;
                                                        font-size: .92rem; text-transform: none;
                                                        letter-spacing: normal; }
/* THREE HEAVY RULES (Ron, 2026-09-11): under the headings, and both sides of the Average row. They mark the
   seams between KINDS of row -- headings, data, total -- where the strand rules between them group data of
   the same kind. Lightening the one above Average was tried and reverted the same day: the Average is the
   row a teacher reads across to, and a heavy rule is what lifts it off the strands rather than letting it
   read as one more of them. Same 2px throughout; the weight is --rp-head against --rp-line. */
.cst-table thead th { font-family: ui-monospace, Consolas, monospace; font-size: .8rem; letter-spacing: 0;
                      text-transform: none; color: #52606d; border-bottom: 2px solid var(--rp-head); }
/* The first strand row would otherwise put its own --rp-line border-top against the heading rule, and a
   collapsed pair of equal width differing only in colour has no defined winner -- so the heavy line would
   be heavy or not depending on the browser. Take the duplicate away rather than hope. */
.cst-table tbody tr:first-child th, .cst-table tbody tr:first-child td { border-top: 0; }
/* The corner cell is a WORD, not a code: it takes the sheet's own face at full size rather than the
   monospace the two-digit student columns are set in, and it names the column beneath it -- plural, because
   there are ten of them under it. */
.cst-table thead th.cst-rowhead { font-family: inherit; font-size: 1.02rem; font-weight: 700;
                                  color: var(--rp-head); }

/* A strand OPENS its block: it is the rollup of the anchor rows beneath it, so it reads as their heading
   rather than as a peer. The anchors are indented under it for the same reason. */
.cst-strand th, .cst-strand td { font-weight: 700; color: var(--rp-head); border-top: 2px solid var(--rp-line); }
.cst-anchor { padding-left: .7rem !important; color: #52606d; font-weight: 400; }

/* The class column is the one a teacher reads across to, so it is separated rather than merely last. */
.cst-table .cst-avg { border-left: 1px solid var(--rp-line); font-weight: 700; color: var(--rp-head); }

/* Provisional figures. The Certification Report greys these; at this density grey is too quiet to notice,
   so the asterisk does the work and the italic only supports it. */
/* NO RULE BETWEEN THE STANDARDS IN A STRAND (Ron, 2026-09-11). .cs-table rules every row -- right for Class
   Strands, where each row is a different student and the line is what keeps them apart. Here three or four
   consecutive rows are one strand's anchors, and ruling between them fences off things that belong
   together, on the densest table in the product.
   What separates blocks now is the 2px border-top the strand rows already carry, and the total's own rule
   above it. The thead keeps its line -- that one divides the headings from the data, which is a real seam.
   IT ALSO FIXES A MISMATCH nobody asked about: .cs-table th draws 2px and .cs-table td draws 1px, so every
   standard row had a thicker segment under its label than under its numbers. */
.cst-table tbody th, .cst-table tbody td { border-bottom: 0; }

.cst-prov { font-style: italic; }

/* The per-student average closes the table, so it takes the 2px rule the strand rows use to OPEN theirs --
   the same device saying "a block ends here" rather than a new weight invented for one row. */
/* Heavy on BOTH sides, so the Average sits apart from the strands above it rather than closing them. The
   bottom one also has to be stated rather than inherited: .cs-table's default gave it 1px under the numbers
   and 2px under the label, so the row's foot disagreed with itself across its own width. */
.cst-total th, .cst-total td { border-top: 2px solid var(--rp-head); border-bottom: 2px solid var(--rp-head);
                               font-weight: 700; color: var(--rp-head); padding-top: .3rem; }

/* THE CARD SHEET IS A TABLE (2026-09-11) -- see the markup for why a grid could not carry a repeating
   letterhead or survive a page break. table-layout: fixed with no column widths splits the width equally,
   which is the whole requirement: three cards of identical size.
   The .4rem cell padding is what used to be the grid's .8rem gap -- two adjacent cells contribute one each.
   The outer edge gets .4rem it did not have before, which is inside the sheet's own 9mm inset and reads as
   part of it. */
.rr-cardtable { width: 100%; table-layout: fixed; border-collapse: collapse; }
.rr-cardtable td { padding: .4rem; vertical-align: top; }
/* A CARD IS NEVER CUT BY THE PAGE, only by the scissors. On the row rather than the cell: a row is what a
   browser actually fragments, and three cards of differing height must break as one unit or the sheet
   develops a step. */
.rr-cardtable tbody tr { break-inside: avoid; }
/* height: 100% so three cards in a row are the same height whatever their name length -- the cell stretches
   to the tallest and the card fills it. Cut lines that do not line up across a row are hard to cut. */
.rr-card { border: 1px dashed #9fb0b8; border-radius: 10px; padding: .8rem 1rem; break-inside: avoid;
           height: 100%; box-sizing: border-box; }
.rr-card-name { font-weight: 700; color: var(--rp-head); margin-bottom: .5rem; }
.rr-card-creds { display: grid; grid-template-columns: auto 1fr; gap: .2rem .6rem; margin: 0; font-size: .9rem; }
.rr-card-creds dt { color: var(--rp-muted); }
.rr-card-creds dd { margin: 0; font-family: ui-monospace, Consolas, monospace; color: var(--rp-head); }

/* NAME THE PAGE MARGIN rather than inherit whatever the print dialog was last set to (2026-09-04). Every
   fit calculation on these reports is against the printable height, and Chrome's "Default" margin is ~10mm
   while a machine set to 0.5in gives away another ~13mm top and bottom -- enough on its own to decide whether
   a Certification Report is one sheet or two.

   THE MARGIN IS ZERO SO THE BROWSER'S OWN HEADER AND FOOTER GO AWAY (2026-09-06). Chrome and Edge print the
   URL, the date and "1/2" into the PAGE MARGIN, and there is no CSS property that turns them off -- but a
   zero page margin leaves them nowhere to print, which suppresses them. These are formal records a parent
   and a teacher hold; a localhost URL across the top of a certification report is wrong on all of them.

   The 9mm inset moves to PADDING on the print containers below, so nothing about the fit changes: a Letter
   page is 1056px, less 2 x 9mm of padding = the same 988px of printable height every measurement in
   docs/Outstanding.md is against. Do not "simplify" this back to @page { margin: 9mm } -- that is the same
   geometry with the browser furniture switched back on.

   LIMITS, so this is not mistaken for a guarantee: the print dialog's own "Headers and footers" checkbox
   still wins if a user ticks it, CSS cannot force it off, and Safari honours this less consistently than
   Chrome and Edge. Printers keep an unprintable edge of ~4-6mm, well inside the 9mm inset, so nothing
   clips. */
@page { margin: 0; }


/* ---- assessment / benchmark ---- */
.head-actions { display: flex; gap: .5rem; flex-wrap: wrap; }
.q-option.picked { border-color: var(--rp-accent); background: #eef4ff; }
.note { background: var(--rp-hint-bg); border: 1px solid var(--rp-hint-line); border-radius: 10px; padding: .6rem .9rem; font-size: .92rem; }

/* ---- accessibility utilities ---- */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
.skip-link { position: absolute; left: -999px; top: 0; z-index: 200; background: var(--rp-accent); color: #fff; padding: .5rem 1rem; border-radius: 0 0 8px 0; }
.skip-link:focus { left: 0; }
a:focus-visible, button:focus-visible, [tabindex]:focus-visible, summary:focus-visible { outline: 3px solid #1a56b8; outline-offset: 2px; }
/* The passage title is auto-focused only for the screen-reader "new story" announcement (tabindex=-1,
   never keyboard-reached), so suppress its visible focus ring — it reads as a stray box to sighted students. */
.passage-title:focus, .passage-title:focus-visible { outline: none; }

/* Source controls: an English 🔊 + a "Show in Spanish" toggle, on their own LEFT-aligned row below the
   title (mirrors the passage's listen-bar). */
.source-actions-bar { display: flex; align-items: center; gap: .45rem; flex-wrap: wrap; margin: 0 0 .5rem; }
.source-listen { flex: 0 0 auto; border: 1px solid var(--rp-line); background: #fff; cursor: pointer; font-size: .9rem; line-height: 1; padding: .2rem .45rem; border-radius: 8px; }
.source-listen:hover { background: #eef4ff; border-color: var(--rp-accent); }
.source-listen.on { background: var(--rp-accent); color: #fff; border-color: var(--rp-accent); }
.source-es-toggle { flex: 0 0 auto; border: 1px solid var(--rp-line); background: #fff; color: var(--rp-accent); cursor: pointer; font-size: .8rem; font-weight: 600; line-height: 1; padding: .25rem .5rem; border-radius: 8px; }
.source-es-toggle:hover { background: #eef4ff; border-color: var(--rp-accent); }
.source-es-toggle.on { background: var(--rp-accent); color: #fff; border-color: var(--rp-accent); }
.source-speaking { box-shadow: 0 0 0 2px var(--rp-accent); }

/* ---- Student sign-in (prototype, browser-local) ---- */
.login-panel { max-width: 34rem; margin: 1.5rem auto; background: #fff; border: 1px solid var(--rp-line); border-radius: 14px; padding: 1.6rem 1.8rem; }
.login-panel h2 { color: var(--rp-head); margin: 0 0 .3rem; }
.login-panel h2:focus { outline: none; }
.login-lede { color: #52606d; margin: 0 0 1rem; }
.login-roster { list-style: none; padding: 0; margin: 0 0 1rem; display: grid; grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); gap: .6rem; }
.roster-card { display: flex; align-items: center; gap: .6rem; width: 100%; text-align: left; border: 1px solid var(--rp-line); background: var(--rp-bg-soft); border-radius: 10px; padding: .55rem .7rem; cursor: pointer; font: inherit; color: var(--rp-ink); }
.roster-card:hover { border-color: var(--rp-accent); background: #eef4ff; }
.roster-avatar { flex: 0 0 auto; width: 2rem; height: 2rem; border-radius: 50%; background: var(--rp-accent); color: #fff; font-weight: 700; display: grid; place-items: center; }
.roster-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.login-or { display: flex; align-items: center; gap: .7rem; color: var(--rp-muted); font-size: .85rem; margin: 1rem 0; }
.login-or::before, .login-or::after { content: ""; flex: 1; height: 1px; background: var(--rp-line); }
.login-new label { display: block; font-weight: 600; color: #52606d; margin-bottom: .3rem; }
.login-new-row { display: flex; gap: .5rem; flex-wrap: wrap; }
.login-new input { flex: 1 1 12rem; border: 1px solid var(--rp-line); border-radius: 8px; padding: .5rem .7rem; font: inherit; }
.login-new input:focus-visible { outline: 3px solid #1a56b8; outline-offset: 1px; border-color: var(--rp-accent); }
.login-err { color: var(--rp-bad); font-size: .88rem; margin: .5rem 0 0; }
/* Guidance after a failed ADULT sign-in — muted, not an error: it is the way out, not a second complaint. */
.login-help { color: #52606d; font-size: .84rem; line-height: 1.45; margin: .45rem 0 0; }
.who-label { align-self: center; color: #52606d; font-size: .85rem; font-weight: 600; margin-right: auto; }

/* Credentialed sign-in (username + password the teacher generated) — the primary path. */
.login-creds .login-field { margin-bottom: .8rem; }
.login-creds label { display: block; font-weight: 600; color: #52606d; margin-bottom: .3rem; }
.login-creds input { width: 100%; border: 1px solid var(--rp-line); border-radius: 8px; padding: .55rem .7rem; font: inherit; box-sizing: border-box; }
.login-creds input:focus-visible { outline: 3px solid #1a56b8; outline-offset: 1px; border-color: var(--rp-accent); }
.login-opt { font-weight: 400; color: var(--rp-muted); font-size: .85rem; }

/* ---- Pretest completion (names the start level) ---- */
.pretest-done-lede { color: #52606d; margin: .2rem 0 1.1rem; }
.pretest-start { color: #52606d; font-size: .95rem; margin: .4rem 0 .2rem; }
/* The start level is a LABEL, not a control. Keep it typographic: a filled blue pill here is nearly
   identical to .btn-next and reads as a dead/disabled button sitting above the real one. This leaves
   exactly one filled blue element on the screen — the actual action. */
.placed-level { display: block; font-size: 2.2rem; font-weight: 800; color: var(--rp-head); margin: .1rem 0 .3rem; }
/* Sits under the big level name: where this rung is on the ladder, and what is waiting there. */
.placed-level-sub { color: #52606d; font-size: .9rem; margin: 0 0 1.1rem; line-height: 1.5; }

/* Pause is a quiet secondary action — the passage checkpoint is what actually protects progress. */
/* Styled as a real (secondary) button, not bare text: it sat beside a solid primary and read as
   unstyled/broken. Quiet and broken are not the same thing — the outline keeps it clearly
   subordinate to .btn-next while still looking like a control. */
.btn-pause { margin-left: auto; }

/* Per-word "Show Spanish" reveal in the vocab preview (on-demand, so usage reflects a real choice). */
.vocab-es-toggle { display: inline-block; margin-top: .35rem; font-size: .85rem; }

/* ---- Teacher Class View (prototype) ------------------------------------------------------------- */
.tc { max-width: 1000px; margin: 0 auto; padding: .5rem 0 1.5rem; }
.tc-head { margin: .5rem 0 1rem; }
.tc-eyebrow { font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--rp-muted); margin: 0 0 2px; }
.tc-title { font-size: 1.5rem; font-weight: 700; color: var(--rp-head); margin: 0; display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.tc-sub { color: #6b7280; font-size: .9rem; margin: .25rem 0 0; }
.tc-rename-btn { font-size: .78rem; font-weight: 600; color: var(--rp-accent); background: none; border: none; padding: 0; cursor: pointer; }
.tc-rename-btn:hover { text-decoration: underline; }
.tc-rename { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.tc-name-input { font-size: 1.15rem; font-weight: 600; color: var(--rp-head); padding: .3rem .5rem; border: 1px solid var(--rp-line); border-radius: 8px; min-width: 280px; }
.tc-scroll { overflow-x: auto; }
.tc-table { width: 100%; border-collapse: collapse; font-size: .9rem; }
.tc-table th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; color: var(--rp-muted); font-weight: 700; padding: 6px 10px; border-bottom: 2px solid var(--rp-line); white-space: nowrap; }
.tc-table td { padding: 8px 10px; border-bottom: 1px solid var(--rp-line); vertical-align: middle; }
/* The Actions cell is last, and .rr-actions is nowrap, so its final label ran to the table's right edge and
   the last letter was clipped. Extra right padding gives it room and leaves white space after it. */
.tc-table td:last-child, .tc-table th:last-child { padding-right: 20px; }
/* CENTRED, except the name (Ron, 2026-08-22). Every column after Student holds a badge or a short
   value, so centring lines them into clean vertical bands — which is what lets a red cell jump out when
   scanning 35 rows. The NAME stays left: it is the one column you read rather than compare, and a
   centred name column is genuinely harder to scan down.
   Scoped to .rr-clstable so the sign-in-card sheet, which shares .rr-table, stays left-aligned.
   MUST stay BELOW ".tc-table th", which sets text-align at equal specificity — source order is what
   decides this one, so do not move it back up the file. */
/* A RULE EVERY SECOND STUDENT (Ron, 2026-09-12), the treatment Class Strands took first and for the same
   reasons. Half the lines, and an anchor still within one row of everywhere -- which matters on a table this
   wide, where the eye travels a long way from a name to a strand score and slipping a line is the
   characteristic error.
   Lightening them instead is not available: --rp-line is #d9e2ec, already near the floor at which a hairline
   survives a printer, so thinning the COUNT is the only lever.
   SCOPED TO .rr-clstable, the class roster. The class LIST above it wears .tc-table too and is a handful of
   rows, where every line is cheap and the pairing would read as grouping.
   Odd rows lose their border, so the rule falls under rows 2, 4, 6. The empty-state row is nth-child(1) and
   loses its own, which is right: there is nothing under it to separate. */
.rr-clstable tbody tr:nth-child(odd) td { border-bottom: 0; }
.rr-clstable th, .rr-clstable td { text-align: center; }
.rr-clstable .tc-name, .rr-clstable th:first-child { text-align: left; }
/* justify-content, not text-align: these are flex children, and text-align has no effect on them.
   Without this the buttons stayed left-packed while every other column centred. */
.tc-row { cursor: pointer; }
.tc-row:hover { background: var(--rp-bg-soft); }
.tc-row:focus-visible { outline: 2px solid var(--rp-accent); outline-offset: -2px; }
/* THE ROW SHE JUST CAME BACK FROM (Ron, 2026-09-01) — added and removed by rpMarkReturn, never by Blazor.
   AN EDGE MARKER, NOT A FILL, and that is forced rather than chosen: .tc-row:hover already tints the row with
   --rp-bg-soft, so a background highlight would VANISH under the pointer — on the one row the pointer is most
   likely to be near. An inset shadow occupies a different axis, composes with the hover tint, and costs no
   layout, so the row does not shift when the mark appears or goes.
   It is deliberately quiet: this says "you were here", not "look at this". The loud half is the focus ring,
   which lands on the same row and is what a keyboard user actually needs. */
.tc-row-return { box-shadow: inset 3px 0 0 var(--rp-accent); }
/* NOT bold. Bold is wider, so a bold name hit the column wrap point sooner; unbolding buys width in the
   widest column of the table and wraps less often. The chevron already marks the row as clickable, so weight
   was not carrying that job (Ron, 2026-08-24). */
.tc-name { font-weight: 400; color: var(--rp-head); }
/* A long name must WRAP, not widen the column. With nowrap, "Donald DuckMater Loiacono Senior" stretched the
   table until .tc-scroll scrolled horizontally and the Edit button left the viewport — the one control that
   could fix the name was unreachable because of the name (Ron, 2026-08-24).
   TWO corrections after the first attempt (same day):
   1. SPECIFICITY. `.rr-table td` sets white-space: nowrap at (0,1,1) and a bare `.tc-name` is (0,1,0), so the
      first fix lost the cascade and never wrapped anything. Scoped to `.rr-table .tc-name` = (0,2,0).
   2. break-word, NOT anywhere. `overflow-wrap: anywhere` is the one value that also shrinks the element's
      min-content width, so table auto-layout narrowed this column until SHORT names broke too — "Micky Mouse"
      came out on two lines. `break-word` breaks a long token only when it must and leaves min-content alone;
      min-width holds a column wide enough for an ordinary two-word name. */
/* The student name is a real <button> (2026-08-31), so the row can go back to being a row -- see
   ClassManage. Styled to look exactly like the text it replaced: a table of thirty buttons that LOOK like
   buttons would be unreadable, and the affordance was always the chevron plus the row hover, not a chrome
   button. Inherits font and colour, keeps the hanging indent working by not introducing a second box. */
.tc-namebtn { background: none; border: 0; padding: 0; margin: 0; font: inherit; color: inherit;
              text-align: left; cursor: pointer; }
.tc-namebtn:focus-visible { outline: 3px solid var(--rp-accent-bright); outline-offset: 2px; border-radius: 4px; }

.rr-table .tc-name {
    white-space: normal;
    overflow-wrap: break-word;
    min-width: 9rem;
    max-width: 15rem;
    /* HANGING INDENT (Ron, 2026-08-24). The chevron is inline before the name, so line one started after it
       while a wrapped line two started at the cell edge — "Duck," and "Donald" came out misaligned by the
       width of the chevron. Negative text-indent pulls line one back so the chevron sits in the padding, and
       every line of the name then begins at the same x. Done this way rather than with flex on the cell:
       display:flex on a <td> takes it out of the table layout, which is a trap this file already documents. */
    /* THE ARITHMETIC, because these two numbers are not independent (Ron, 2026-08-30):
         chevron indent  = padding-left + text-indent   -> where line one starts
         wrapped lines   = padding-left                 -> where line two starts
       The hanging indent works only while text-indent equals the chevron's own width plus its 6px
       margin (~.75rem) — that is what puts "Duck," and "Donald" on the same x. padding-left then sets
       how far the whole block sits from the cell border, and it was .75rem, which cancelled the indent
       exactly and left the chevron flush against the border. 1.1rem gives it ~6px of air without
       touching the alignment: raise or lower this one freely, but move text-indent only if the chevron
       itself changes size. */
    padding-left: 1.1rem;
    text-indent: -.75rem;
}
/* Icon sitting left of a button label. */
.btn-ico { flex-shrink: 0; margin-right: .1rem; }
/* DECORATION, and raised to 3.20:1 anyway (2026-09-15). The chevron is aria-hidden and adds nothing the
   row does not already say -- the student's name is right beside it and the whole row is clickable -- so
   1.4.11 does not bind. It was at 2.01:1 and simply too faint to do the one job it has, which is to say
   "this row opens". Passing the non-text bar costs nothing and settles the argument either way. */
.tc-chev { color: var(--rp-mark); margin-right: 6px; }
.tc-prog { white-space: nowrap; }
.tc-bar { display: inline-block; width: 64px; height: 8px; background: var(--rp-bg-soft); border: 1px solid var(--rp-line); border-radius: 999px; overflow: hidden; vertical-align: middle; margin-right: 6px; }
.tc-fill { display: block; height: 100%; background: var(--rp-accent); border-radius: 999px; }
.tc-progn { font-size: .82rem; color: #6b7280; font-variant-numeric: tabular-nums; }
/* "Last worked" — same weight as the passage counters beside it, since it belongs to the same block of
   how-much-and-how-recently columns. NOT emphasised when the gap is long: a stall flag was declined, and
   colouring a cell by age would be that flag under another name. */
.tc-lastworked { font-size: .82rem; color: #6b7280; white-space: nowrap; }
/* THE EMPTY-CELL DASH IS DECORATION, and that is the decision this rule rests on (2026-09-15).
   It was 1.72:1, and the placed-student variant 1.30:1 -- invisible, and failing every bar anyone could
   argue for. The fix is not simply to darken it, because darkening it to the 4.5:1 that TEXT needs would
   make six dashes in a row as loud as six scores, which is the exact reading the lighter variant existed to
   prevent.
   So: the dash conveys nothing the EMPTY CELL does not already convey. Its absence says "no score" just as
   well as its presence, which is what makes it decoration rather than content -- and the markup now says so
   with aria-hidden, which also stops a screen reader announcing "em dash" six times per row. Being
   decoration, 1.4.11 does not bind; it is raised to 3.20:1 regardless, because a dash nobody can see is not
   doing the column-tracking job it exists for.
   THE PLACED-ROW VARIANT IS GONE, deliberately. It carried a real intent -- six dashes reading as "not
   started yet" rather than "data that failed to arrive" -- but it carried it with a difference of 0.4:1
   between two invisible greys, which nobody could perceive. If that distinction still matters it needs a
   mechanism someone can actually see. */
.tc-dash { color: var(--rp-mark); }
.tc-ft { font-size: .85rem; font-weight: 600; color: var(--rp-head); font-variant-numeric: tabular-nums; }

.tc-status { font-size: .82rem; font-weight: 600; }
.tc-status-distinction { color: #7c3aed; }
.tc-status-promoted { color: #2e7d32; }
.tc-status-progress { color: #2563a6; }
.tc-status-idle { color: var(--rp-muted); }
/* A score column with nothing in it yet. This is a table HEADER -- text -- so it takes the 4.5:1 bar and
   now shares --rp-muted with every other header; it was 1.59:1.
   ITS "DORMANT" LOOK IS LOST, and that is the same casualty as the two-tier grey: the effect was "lighter
   than the normal header grey", and no value lighter than --rp-muted can pass. A dormant column that still
   wants marking needs weight, style or a mark rather than a fainter ink. */
.tc-col-idle { color: var(--rp-muted); }
/* A TWO-LINE column heading: first word on top, second below (Ron, 2026-08-30). The words are what make
   a heading descriptive, and a table this wide cannot spend the horizontal room on them — so it spends
   vertical room instead. flex-column rather than a <br> so the two lines centre on each other and the
   line-height is set once here, not left to the browser's default leading. */
.rr-h2 { display: inline-flex; flex-direction: column; align-items: center; line-height: 1.15; }

/* TOTAL AND CURRENT PASSAGES, ONE COLUMN (Ron, 2026-09-12). Two columns became one; the heading is two lines
   ("Total and Current" over "Passages") and the cell beneath holds both numbers.
   THE SLOTS ARE THE POINT. Each value gets a fixed-width box, so the digits line up down the column exactly as
   they did when these were separate columns -- losing that vertical scan was the one real cost of merging, and
   it only applies if the numbers are left to flow inside the cell. Both are right-aligned so the totals agree
   on their ones digit and the level counters agree on their last.
   ch UNITS, WITH tabular-nums FROM .tc-progn: 1ch is the width of a "0" and the figures are tabular, so 3ch
   holds "100" exactly and the slot cannot drift with the font. .rr-passb gets 5.5ch rather than 5 because
   "20/20" carries a slash, which is not a digit and is not covered by the ch measure.
   THE GAP IS NOW A NUMBER WE OWN. As two columns it was set by the auto-layout distribution and could not be
   moved -- see the attempt recorded and reverted the same day. Here it is margin-left, and it is whatever it
   is set to. */
.rr-passcell { white-space: nowrap; }
.rr-passa, .rr-passb { display: inline-block; text-align: right; }
.rr-passa { width: 3ch; }
.rr-passb { width: 5.5ch; margin-left: 16px; }

/* The Levels column, header and strip alike. .rr-clstable already centres every cell but the name; this
   states it on the column that must not drift, because the strip is an inline-flex child and would sit
   wherever the cell's text-align left it. */
/* AUDIO & SPANISH, CENTRED ON WHATEVER THE CELL HOLDS (Ron, 2026-09-12). ENG alone, SP alone, the pair, the
   pair with either plus, or the dash -- each sits on the cell's centre line.
   IT WAS FIXED SLOTS FIRST, 34px each and left-aligned, so ENG began at the same x on every row and the codes
   read as a column. That is the better answer for SCANNING and the worse one for looking at: a student with
   only ENG sat visibly left of centre, pinned there by an empty box where SP would have been. Ron chose
   centred; the trade is that the codes no longer line up down the table.
   THE EMPTY SPAN IS NOT RENDERED, rather than collapsed -- see the markup. A zero-width flex item still takes
   the container's gap, so a lone code would have hung a few pixels off centre, which is exactly what centring
   is meant to fix. Nothing here can centre a cell whose markup carries an invisible passenger.
   The dash needs no rule of its own: .rr-clstable centres every cell, and a lone inline element obeys it. */
.rr-delivery { white-space: nowrap; }
.rr-dlv { display: inline-flex; gap: 6px; font-size: .82rem; color: #52606d; font-weight: 600;
          letter-spacing: .02em; }
/* Raised and small, which is what sends a reader to the legend. vertical-align keeps it off the cap line
   without the browser's default sup lifting the row's line box and stretching every row. */
.rr-dlv sup { font-size: .7em; line-height: 0; vertical-align: super; }
.rr-clstable th.rr-lvlcol, .rr-clstable td .lvstrip { text-align: center; }
.rr-clstable td .lvstrip { justify-content: center; }

/* The onboarding next-step prompt above the roster: print sign-in cards. Accent-tinted rather than the
   warning colours of .rr-note beside it — this is the next thing to do, not something wrong. It disappears
   for good once any student signs in, so it never becomes furniture. */
/* UNIFORM BORDER, no accent edge (Ron, 2026-09-16) -- the standing rule: a bordered box takes the same
   border on all four sides, and louder is a border COLOUR, never one side's width.
   The 3px left edge was carrying the whole box. The other three were 1px of --rp-accent-soft on a fill of
   --rp-accent-soft -- the same value, 1.00:1, invisible -- so dropping the left one alone would have left a
   tinted panel with no border at all. The colour is what moved: one accent border on all four sides,
   5.13:1 against the page and 4.33:1 against its own fill. */
.rr-nextstep { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
               margin: 0 0 .9rem; padding: .6rem .9rem; border: 1px solid var(--rp-accent);
               border-radius: 10px; background: var(--rp-accent-soft); }
.rr-nextstep p { margin: 0; color: var(--rp-head); font-size: .9rem; }
.rr-nextstep .btn-next { margin-top: 0; flex: 0 0 auto; }

/* ---- Column legend above the class table (Ron, 2026-08-30) --------------------------------------
   A <details>, so the disclosure is keyboard-operable and screen-reader-announced with no ARIA of our
   own — the same idiom the vocabulary glossary and the teacher settings block already use. It exists
   because every explanation on this table used to live in a `title`, which no keyboard user can reach.
   Closed by default: it is reference material, not something to read every visit. */
.rr-legend { margin: 0 0 .9rem; border: 1px solid var(--rp-line); border-radius: 10px;
             background: var(--rp-bg-soft); font-size: .85rem; }
.rr-legend > summary { cursor: pointer; padding: .55rem .9rem; font-weight: 600; color: var(--rp-head);
                       border-radius: 10px; list-style-position: inside; }
.rr-legend > summary:focus-visible { outline: 3px solid var(--rp-accent-bright); outline-offset: -3px; }
.rr-legend[open] > summary { border-bottom: 1px solid var(--rp-line); border-radius: 10px 10px 0 0; }
/* TWO COLUMNS THAT FLOW, not a grid (Ron, 2026-09-01). Stacked and single-column this ran to about 500px —
   most of the space above the fold on a laptop, which on the first visit (when it opens by itself) pushed
   the table off screen and presented a wall of text. A legend that fits without scrolling gets read.

   MULTICOL RATHER THAN GRID, because grid always ALIGNS ROWS: whatever sits beside the Levels entry gets a
   row as tall as Levels, and Levels is a nested list of five states, several times taller than anything
   else. That left an unavoidable hole in the right column — unavoidable in the literal sense that no grid
   property fixes it, since the row height is the point of a grid. Multicol fills instead of aligning, and
   balances the two columns' heights on its own.

   TERM ABOVE DEFINITION HERE, not beside it. Beside works at the panel's full 1000px; inside a ~470px
   column a 10rem label gutter leaves barely 300px for the definition and everything wraps twice. The
   beside-layout survives only in the single-column fallback below, where the width is there for it. */
.rr-legend-list { margin: 0; padding: .8rem .9rem .4rem; column-count: 2; column-gap: 2.2rem; }
/* The bottom padding is short because every entry carries its own bottom margin, including the last
   one in each column; 1rem of padding under that read as a gap the panel did not need. */
/* An entry must not be split down the middle by the column boundary. */
.rr-legend-list > div { break-inside: avoid; margin: 0 0 .7rem; }
/* WHERE COLUMN TWO STARTS, chosen rather than left to the balancer (Ron: Correct 1st Try at the top of the
   right column). It also splits the legend along a real seam — WHERE and HOW MUCH on the left (the level
   strip, the two passage counts, recency), HOW WELL on the right (correctness, the score, how it is
   scored, and the support mark) — and the two halves happen to be about the same height, so nothing is
   given up to get the order. */
.rr-legend-break { break-before: column; }
.rr-legend-list dt { font-weight: 700; color: var(--rp-head); display: flex; align-items: center; gap: .35rem; }
.rr-legend-list dd { margin: .1rem 0 0; color: #52606d; line-height: 1.55; }
/* The one legend entry that defines a SET of codes rather than a column, so it is the one that is a list.
   No bullets: the code at the head of each line is already the marker, and a disc in front of it would be a
   second one saying less. Four codes are four things to look up, which is why this is a real list and not one
   paragraph with <br /> between the lines -- the defect already recorded on the class sheet footnotes. */
.rr-legend-codes { list-style: none; margin: 0 0 .4rem; padding: 0; }
/* THE CODE GETS ITS OWN COLUMN (Ron, 2026-09-12). The last line is a bold em dash for the CODE, and run
   together with what followed it read as a typo rather than as two different things; a column each fixed
   that. The separator it was being distinguished FROM is gone as of 2026-09-16 — once the code had a column,
   the line was separating twice, and the dash that had to go was the one that was not a code.
   flex, so a meaning that wraps lines up under itself instead of returning to the left margin under the code.
   5rem clears the widest code, ENG+ / SP+, at this size. */
.rr-legend-codes li { display: flex; gap: .45rem; margin: 0 0 .15rem; }
.rr-legend-codes li > strong { flex: 0 0 5rem; }

/* REFLOW (WCAG 1.4.10, docs/Accessibility_Rules.md): usable at 200% zoom and at 320px with no horizontal
   scrolling. One column, and the forced break must go with it — a break-before:column in a single-column
   context is ignored by every engine, but stating it keeps the intent legible. The term moves BESIDE the
   definition here, which is the width where that layout pays. */
@media (max-width: 640px) {
    .rr-legend-list { column-count: 1; }
    .rr-legend-list > div { display: grid; grid-template-columns: 10rem minmax(0, 1fr);
                            gap: .1rem .8rem; align-items: baseline; }
    .rr-legend-list dd { margin: 0; }
}

/* The spoken name of a mark, for anyone who cannot see the glyph beside it and for anyone who can see it
   but would not know what to call it. */
.rr-legend-mark { font-weight: 400; font-style: italic; color: var(--rp-muted); font-size: .82rem; }
.rr-legend-states { list-style: none; margin: .45rem 0 0; padding: 0; display: grid; gap: .3rem; }
/* TWO flex items per row, never three (Ron, 2026-09-01). Flex makes every contiguous run of text its own
   anonymous item, so a bare `<b>Completed early</b> ... rest of the sentence` was TWO items with a gap between
   them, and the label broke away from the definition it belongs to. Wrapping the whole definition in one span
   puts the label and its sentence back in a single run that wraps as ordinary prose, hanging-indented under
   itself rather than under the chip. */
.rr-legend-states li { display: flex; align-items: baseline; gap: .5rem; }
.rr-state-text { flex: 1 1 auto; }
/* The chips are the table own classes, so the legend cannot drift from what the rows draw. flex-shrink
   keeps them square when a definition wraps. */
.rr-legend-states .lvl { flex: 0 0 auto; }

/* ---- The five-level strip: B S G P D ------------------------------------------------------------
   All five levels, always, in ladder order. POSITION carries the timeline and SHAPE carries the state,
   so the strip is readable in greyscale and to colour-blind readers; colour only reinforces. The letters
   are aria-hidden and the cell carries one spoken sentence — see LadderSpoken in ClassManage. */
.lvstrip { display: inline-flex; gap: 3px; }
.lvl { width: 19px; height: 19px; line-height: 19px; border-radius: 5px; font-size: .69rem;
       font-weight: 700; text-align: center; position: relative; }
/* Cleared with every passage done. */
.lvl-done { background: var(--rp-head); color: #fff; }
/* Cleared EARLY on mastery acceleration. Same solid chip plus a pip: presence/absence, not a second
   colour, so "early" is still legible with no colour at all. This mark is the only place acceleration
   is visible on the roster, and it is the whole evidence the start-low model is doing its job. */
.lvl-early { background: var(--rp-head); color: #fff; }
.lvl-early::after { content: ""; position: absolute; top: -3px; right: -3px; width: 8px; height: 8px;
                    border-radius: 50%; background: var(--rp-warm); border: 1.5px solid #fff; }
/* Where the student is now — the one OPEN shape in the strip, so the eye lands on it while scanning a
   column of thirty. On promotion this moves to the level they are promoted INTO, so a strip always has
   exactly one ring; the sole exception is promotion from the top level, which is graduation and has
   nowhere to point. */
.lvl-now { background: #fff; color: var(--rp-accent); box-shadow: inset 0 0 0 2px var(--rp-accent); }
/* CONTRAST, MEASURED (Ron, 2026-09-01). These two chips were the quiet end of the strip and had been
   tuned by eye until they were quiet enough to be GONE: "Ahead" ran at 2.29:1 and "Not assigned" at
   1.27:1 against the white row, where WCAG 1.4.3 wants 4.5:1 for text this size. 1.27:1 is essentially
   invisible — white is 1.0. The 1.4.3 exemption does not apply: it covers INCIDENTAL text that conveys
   nothing, and these letters carry which levels a student was routed past.

   THE FILL COULD NOT BE RESCUED, and that is why the fix changes shape rather than shade. Separating
   these two states by fill LIGHTNESS is unfixable in principle: no grey pale enough to read as quiet
   ever reaches 3:1 against white — the darkest tried, #c2ced8, is 1.6:1 and already competes with
   "Completed". So the distinction moved off fill and onto PRESENCE OF A BOX, which is free and survives
   greyscale.

   QUIET BY WEIGHT AND FORM, NOT BY CONTRAST. Contrast is spoken for by legibility, so de-emphasis has to
   come from somewhere else. "Not assigned" ends up the only letter in the strip with no box and no bold,
   which reads as quiet while measuring 5.21:1 — four times its old legibility. */

/* Ahead of them. Present, unremarkable — the ladder must never change length, or the column would
   reflow as students advance. The ring is an inset SHADOW, not a border, so the chip stays exactly 19px
   and the five letters keep their baseline; a real border would grow the box by 2px and shunt the strip.
   Letter 5.74:1 on the fill, ring 3.66:1 on white — the fill alone was never perceivable as a box. */
.lvl-next { background: #eef2f5; color: #52606d; box-shadow: inset 0 0 0 1px #7b8794; }
/* Below where they started: placement routed them past it and it was never assigned. NO CHIP AND NO LETTER
   (Ron, 2026-09-12) -- the pip is empty. It still occupies its 19px slot, because .lvstrip is inline-flex and
   a flex item keeps its box with no content, so the five levels stay in the same five positions on every row
   and a column can be scanned straight down. The first letter a row shows IS its starting level, which is how
   the strip states a starting point without spending a column on one.
   It rendered a bare grey letter until 2026-09-12. That was halfway: lvl-next means "not yet" and a skipped
   level means "never", and two letters in one strip let a teacher read the second as the first.
   No colour or weight here any more — there is no glyph left for them to style. */
.lvl-skip { background: transparent; }

/* Row action menu. Chrome is the shared .acct-menu / .acct-item; this positions it and styles the dots. */
.rr-rowmenu { position: relative; display: inline-block; }
.rr-dots { background: none; border: 0; cursor: pointer; font-size: 1.15rem; line-height: 1; color: #52606d; padding: .15rem .45rem; border-radius: 6px; }
.rr-dots:hover { background: var(--rp-bg-soft); color: var(--rp-head); }
.rr-rowmenu-pop { position: absolute; top: calc(100% + 4px); right: 0; min-width: 170px; text-align: left; }
/* Desktop: the row menu is FIXED and placed by rpAnchorMenu (menu.js), because its panel is
   `tc-scroll rr-panel` — overflow:hidden for the rounded corners plus overflow-x:auto for a wide
   roster — and either one clips an absolutely-positioned child. Relaxing a single axis cannot fix
   it: a non-visible value on one axis forces the other to auto. `right: auto` matters, or the
   inherited right:0 from .acct-menu fights the left the script sets.
   Below 641px .acct-menu is already a fixed bottom sheet, which escapes the clip on its own, so
   this override is deliberately scoped to the desktop breakpoint. */
@media (min-width: 641px) {
    .rr-rowmenu-pop { position: fixed; top: 0; left: 0; right: auto; }
}
/* Placed = tested, routed, not yet begun. Teal rather than the grey of idle: it is an actionable state (hand
   over the card), not an absence of activity. */
.tc-status-placed { color: #0f766e; }
.tc-attn { display: inline-block; margin-left: 8px; font-size: .72rem; font-weight: 700; color: #9a3412; background: #fde9dd; border-radius: 999px; padding: 1px 8px; white-space: nowrap; }
/* The same pill, put on its OWN line under the status word. width:fit-content + auto margins because the
   cell centres with text-align, which has no effect on a block box — without it the pill would stretch
   the full column and its background would read as a banner rather than a badge. */
.tc-attn-line { display: block; width: fit-content; margin: 3px auto 0; }
/* The same pill under the student's NAME. The name cell is the one left-aligned column, so this one is
   left-aligned too rather than centred, and it clears the hanging indent the chevron needs. */
.tc-attn-name { display: block; width: fit-content; margin: 3px 0 0; margin-left: 0; text-indent: 0; }
/* SUSPENDED, and it must sit AFTER .tc-attn: both are single-class selectors, so specificity is a tie and
   source order decides. Declared earlier (with the .rr-suspended block it replaced) it lost silently and the
   badge rendered in the amber attention palette -- which says "a problem to fix" about a state the teacher
   chose deliberately. Muted grey instead, 6.11:1 on its own ground.
   text-indent:0 IS THE WHOLE FIX, and it is not cosmetic. `.rr-table .tc-name` carries
   `text-indent: -.75rem` for the chevron's hanging indent; text-indent INHERITS, and an inline-block is a
   block container, so the badge applied that -12px to its OWN first line and dragged "Suspended" left inside
   its own pill -- the background looked shoved to the right and the "S" sat on the left cap. That is what
   `.tc-attn-name` has always ended in `text-indent: 0` for; making this badge inline dropped that class and
   lost the reset with it. Padding was widened to 11px first, which was 3px of paper over a 12px hole -- so
   the padding goes back to the shared 8px and this badge is geometrically identical to License expired
   again. Any future .tc-attn placed inline in this cell needs the same reset. */
.tc-attn-susp { color: #63555c; background: #f1eef0; text-indent: 0; }
.tc-foot { color: var(--rp-muted); font-size: .82rem; margin: .75rem 0 0; }
/* SCREEN ONLY, and the scoping is the bug fix (Ron, 2026-09-12). This is a single class, so it ties with
   .rp-print-doc { padding: 0 9mm 9mm } in the print block -- and @media print adds NO specificity, so the tie
   broke on source order, which this rule wins by sitting ~790 lines later. The teacher drill is
   <div class="tc-drill rp-print-doc">, so printing any of its four reports lost the 9mm side inset and most
   of the bottom (.5rem = 8px against 34px). With @page { margin: 0 } -- deliberate, it is what keeps the
   browser URL and page numbers off a formal record -- there is no other margin in the system, so the content
   sat inside the printer unprintable band and the left and right edges CLIPPED.
   The class-wide certification sheet was fine because .rr-certs declares no padding, which is what made this
   look like a bug in the individual report rather than in the wrapper it shares.
   "not print" rather than out-specifying inside the print block: that would have fixed this one instance
   and left the trap armed for the next class added to the wrapper. NOTE THE CATEGORY -- any single-class rule declared
   after line 2106 that lands on a print container beats it the same way. */
@media not print { .tc-drill { padding: .5rem 0; } }
.tc-drill-bar { display: flex; gap: .6rem; flex-wrap: wrap; margin: .25rem 0 1rem; }

/* THE REPORT'S ACTION ROW. Back has always been here; ExtraActions puts the way ACROSS to the other report
   beside it rather than in a bar above the header — navigation belongs with navigation, and you choose where
   to go after reading, not before. space-between so a second control sits opposite Back instead of crowding
   it, and the row collapses to a stack on a narrow screen rather than scrolling (WCAG 1.4.10). */
.report-actions { display: flex; flex-wrap: wrap; gap: .6rem; justify-content: space-between;
                  align-items: center; margin-top: 1.25rem; }
/* TWO CONTROLS IN ONE SLOT, on the Certification Report: Print and the cross-link to the Progress Report.
   Grouped rather than passed as siblings, because .report-actions is space-between -- three loose children
   would spread across the row with the cross-link stranded in the middle, instead of Back at one margin and
   the pair at the other. */
.report-actions-group { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; }
/* KILL .btn-next's 1.1rem top margin for EVERY button in this row, not just the grouped pair (Ron,
   2026-09-06). It was zeroed only inside .report-actions-group, so Back — a direct child of .report-actions
   — kept it and sat ~18px below Print and Progress Report. align-items:center does not save it: a margin
   still offsets a flex item within its line, so centring happened around a box that had already been pushed
   down. The margin exists for a button that ENDS a column of content; in a horizontal action row every
   button is a sibling and none of them wants it. */
.report-actions .btn-next,
.report-actions-group .btn-next { margin-top: 0; }

/* THE TEACHER BAND (Ron, 2026-09-01) -- adults-only context, rendered INSIDE the report through its
   AdultContext slot: under the header block, above the scores. It went through two homes first, and both were
   wrong for the same reason. It began as a lone span in a button bar, where a readout sat at button weight
   and looked like a control. It then became a band ABOVE the report -- still a strip of chrome in front of a
   record, which is what made the header stop reading as the top of the document. It then sat under the
   header, which still cut the record in half. It now renders at the END of the report, before the buttons:
   the record runs unbroken from its header to its last score, and the annotation follows what it annotates.

   NO LEFT RULE and NO "Teacher view" caption (Ron, 2026-09-01). Both were doing the same job -- announcing
   that this block is different in kind -- and at the foot of a finished report neither earns its keep:
   position says it. The accent border in particular read as a callout on a document that has no other
   callouts.

   QUIET ON PURPOSE. It sits above a formal record that a parent may be looking at over the teacher's
   shoulder, so it is a soft panel rather than a callout -- it must not compete with the report's own title.
   Every colour clears 4.5:1 on the panel (label 11.1, value 8.4, note 6.2, tag 5.0), because the note carries
   the meaning that used to live in a tooltip and a low-contrast gloss would just move the problem.

   BUILT TO GROW: pass 2 of docs/PerStudentSupport_Plan.md adds the delivery settings and the AT flag as
   further <div> pairs in the same <dl>. The grid wraps rather than scrolls. */

/* NO PRINT RULE HERE, deliberately. The band is screen-only in intent, but the one @media print block in this
   file hides `body *` and reveals only .rr-print-sheet -- the sign-in cards and the Class Strands report -- so
   none of the STUDENT reports prints today, and this band lives on one of those. Printing them is still
   backlog (docs/Outstanding.md). A rule written now would be dead, and worse, would read as evidence that the
   print path exists. Hide it when that path is built. */
/* Static teacher badge in the shared header's actions outlet (mirrors the student account badge slot). */

/* Empty roster: the table still renders so the teacher sees the shape of what it will hold. */
.rr-emptyrow { text-align: center; color: var(--rp-muted); padding: 1rem !important; }

/* Teacher account menu — chrome comes from the STUDENT .acct-menu / .acct-item rules; only the placement
   and the width differ (two short items instead of the student's three long ones). */
.tp-acct { position: relative; }
.tp-acct-menu { position: absolute; right: 0; top: calc(100% + .4rem); min-width: 190px; }
.rr-opt { font-weight: 400; color: var(--rp-muted); font-size: .8rem; }

/* Class rows are the control — same idiom as the student table in the class view. */
.tp-row { cursor: pointer; }
.tp-row:hover { background: var(--rp-bg-soft); }
.tp-row:focus-visible { outline: 2px solid var(--rp-accent); outline-offset: -2px; }
.tp-chev { color: var(--rp-accent); font-size: 1.1rem; margin-right: .45rem; }

/* Teacher badge — reuses the student account-menu oval; the avatar stands in for the level emblem. */
.tp-badge { text-align: left; }
.tp-avatar { flex: 0 0 auto; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center;
             justify-content: center; background: rgba(255,255,255,.18); font-size: 13px; font-weight: 700; color: #fff; }

/* Invite / set-password landing — a single centred panel, no chrome around it. */
.tp-invite { padding: 1.5rem 1.75rem; margin-top: 1.5rem; }
.tp-invite .tc-title { margin-top: 0; }
.tp-invite form { max-width: 380px; }

/* My Profile — a single column of label/value rows. Deliberately NOT .report-meta: that is a two-column
   grid with a fixed 5.25rem label column, sized for a wide report, and "Organization" does not fit it. */
.tp-profile { margin: 0 0 1rem; display: flex; flex-direction: column; gap: .1rem; }
.tp-profile > div { display: flex; gap: .75rem; align-items: baseline; padding: .4rem 0; border-bottom: 1px solid var(--rp-line); }
.tp-profile > div:last-child { border-bottom: none; }
.tp-profile dt { flex: 0 0 6.5rem; margin: 0; color: #52606d; font-size: .85rem; }
.tp-profile dd { margin: 0; font-weight: 600; color: var(--rp-ink); overflow-wrap: anywhere; }

/* Admin create form + confirmation. */
.tp-form { padding: 1.25rem 1.5rem; }
.tp-form .tc-title { margin-top: 0; }
/* align-items: end so the INPUTS line up even when one label wraps to two lines. Without it, a label that
   carries an "(optional)" tag is taller than its neighbour, and its control sits a line lower — which reads
   as a broken form rather than as a longer label (Ron, 2026-08-24). */
/* Shared with the roster dialogs as .rr-form-row -- same behaviour, and one definition so the two cannot
   drift. The 9rem basis is what makes a three-up row WRAP on a narrow viewport instead of crushing. */
.tp-form-row, .rr-form-row { display: flex; gap: .9rem; flex-wrap: wrap; align-items: end; }
.tp-form-row .rr-field, .rr-form-row .rr-field { flex: 1 1 9rem; }
/* A narrow field in a row sizes to its content instead of claiming an equal column, so School takes the
   space it needs and State sits beside it rather than halving the row. */
.tp-form-row .rr-field-sm, .tp-form-row .rr-field-md,
.rr-form-row .rr-field-sm, .rr-form-row .rr-field-md { flex: 0 0 auto; }
/* SHORT-VALUE fields. .rr-field is a full-width flex column, so a two-letter state code or a 5-digit cap
   stretched the whole row and invited a sentence. Cap the CONTROL, not the field: the label still wraps
   normally and the field keeps its place in the flow. */
.rr-field-sm > select, .rr-field-sm > input { max-width: 7rem; }
.rr-field-md > select, .rr-field-md > input { max-width: 12rem; }
/* flex-start, NOT center (Ron, 2026-09-09). Centring is invisible on a one-line label and wrong on a
   wrapping one: the box floats in the gap BETWEEN the two lines instead of sitting against the first. The
   2px lifts it off the line's top edge so it reads as optically centred on that first line rather than
   flush to it. Two call sites only — the Edit Student dialog and AdminApp. */
.rr-check { display: flex; align-items: flex-start; gap: .5rem; font-size: .9rem; color: var(--rp-head); margin: .3rem 0 1rem; }
.rr-check input { margin-top: 2px; }
.tp-sub { display: block; font-size: .8rem; color: var(--rp-muted); font-weight: 400; }
/* The invite link is meant to be copied, so it is a selectable field rather than text. */
.tp-invite-link input { width: 100%; box-sizing: border-box; font-family: ui-monospace, Consolas, monospace;
                        font-size: .82rem; padding: .5rem .6rem; border: 1px solid var(--rp-line);
                        border-radius: 8px; background: var(--rp-bg-soft); color: var(--rp-head); }

/* Class roster header — title optically centred on the PAGE, count right on the same line. The empty first
   cell is what keeps the title centred rather than centred in the space left beside the count. */
/* align-items:end drops the count to the bottom of the row, so it sits low against the title and close to
   the roster's top border rather than riding the title's baseline. */
.rr-clshead { display: grid; grid-template-columns: 1fr auto 1fr; align-items: end; gap: 1rem; margin: 0 0 .35rem; }
/* THE CLASS LINE IN THE LETTERHEAD (Ron, 2026-09-12). It was the page TITLE at 1.4rem centred; it now sits
   under the teacher in the app letterhead, so it takes that block's size and weight -- .tp-id-who is 1.05rem
   /700 -- and reads as the second line of an identity rather than a heading. The roster has a real title of
   its own now. */
.rr-clstitle { margin: 0; text-align: left; font-size: 1.05rem; font-weight: 600; color: #52606d; }
/* The menu is absolute, so SOMETHING has to be its containing block. It was .rr-clstitlewrap in the old
   header; here it is this span, and without it the menu would position against the page. */
.tp-id-class { position: relative; display: inline-flex; }
/* Left-aligned under the teacher, not centred on the name: in a letterhead the two lines share a left edge. */
.tp-id-class .rr-clsmenu { left: 0; transform: none; }
/* THE TEXT LINES UP WITH THE TEACHER'S, not the button's box. .rr-clspick carries .3rem of side padding so the
   hover target is bigger than the words; pulling the button left by exactly that much puts the class name on
   the same left edge as the name above it while the padded hit area is kept. */
.tp-id-class .rr-clspick { margin-left: -.3rem; }
.rr-clshead .rr-count { grid-column: 3; text-align: right; font-weight: 400; font-size: .82rem; color: var(--rp-muted); }
/* Actions follow the table they act on. */
.rr-clsactions-below { justify-content: flex-end; margin-top: .9rem; }
/* The roster reads as one object, the way the Your Classes table does. */
.rr-panel { border: 1px solid var(--rp-line); border-radius: 10px; overflow: hidden; }

/* Class page title = the class name, with its settings directly beneath so the control sits with the thing
   it changes. Kept out of the action row below the table, which is all student actions. */
.rr-clstitlewrap { grid-column: 2; display: flex; flex-direction: column; align-items: center; gap: .1rem;
                   position: relative; }
/* The class NAME is the switcher when there is more than one class. It must look like the heading it
   replaced — same size and weight — so the page title does not change shape with the class count. */
.rr-clspick { display: flex; align-items: center; gap: .3rem; background: none; border: 0; padding: .1rem .3rem;
              border-radius: 8px; cursor: pointer; font-family: inherit; }
.rr-clspick:hover { background: #eef2f5; }
.rr-clsmenu { position: absolute; left: 50%; transform: translateX(-50%); top: calc(100% + .3rem);
              min-width: 240px; max-height: 60vh; overflow-y: auto; text-align: left; }
.rr-clsadd { font-weight: 600; color: var(--rp-accent); }
.rr-clsback { position: fixed; inset: 0; z-index: 999; }
/* Billing block on the admin form — appears only for a purchase order, so set it off from the fields
   that apply to every sale. */
.rr-billing { border-left: 3px solid var(--rp-line, #e3e8ef); padding-left: .9rem; margin: .2rem 0 1rem; }
/* License ended — a NOTICE, not an error. Warm rather than alarming: nothing is broken and nothing is
   lost; she simply cannot start anything new until her teacher renews. */
.rp-expired { display: flex; align-items: center; justify-content: space-between; gap: 1rem;
              max-width: 1000px; margin: 0 auto 1rem; padding: .7rem 1rem;
              background: #fff6e5; border: 1px solid #f0d9a8; border-radius: 10px;
              color: #8a5a00; font-size: .95rem; }
/* .rr-field is a flex COLUMN, so every direct child is blockified onto its own line -- which is why an
   (optional) tag placed directly in the label sat UNDER it instead of beside it, and why widening the column
   and adding a non-breaking space changed nothing (Ron, 2026-08-24, twice). A label whose text carries a tag
   wraps BOTH in .rr-lab, so they are one flex item and the tag is inline within it. */
.rr-field > .rr-lab { display: block; }
/* THE WAIT CUE — see Components/BusyCue.razor for the rule and the yield it depends on. The hourglass FLIPS
   rather than spins: a spin reads as indeterminate progress, while a flip reads as "still measuring", which is
   what a two-second wait actually is. Honours prefers-reduced-motion, where the glyph simply sits still. */
.busy-cue { display: inline-flex; align-items: center; gap: .4rem; }
.busy-glass { display: inline-block; line-height: 1; animation: busy-flip 1.4s ease-in-out infinite; }
.busy-text { white-space: nowrap; }
@keyframes busy-flip {
    0%, 45%   { transform: rotate(0deg); }
    55%, 95%  { transform: rotate(180deg); }
    100%      { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) { .busy-glass { animation: none; } }

.rr-optional { font-weight: 400; font-size: .78rem; color: var(--rp-muted); margin-left: .35rem; }
/* Class Reports lives in the action row beside Add Students.
   ANCHORED LEFT, never right (Ron, 2026-08-22). Right-anchoring makes a menu wider than its button grow
   LEFTWARD, straight over Add Students. Left-anchoring pins it to the button's left edge so it can only
   grow right, past the button's right edge, where there is nothing to cover.
   min-width:100% keeps it at least as wide as the button; width:max-content stops it being wider than its
   longest item. Both are needed: .acct-menu sets min-width:380px for the account menu, which is far too
   wide here. */
/* THE MARGIN LIVES ON THE WRAPPER, NOT THE BUTTON (Ron, 2026-09-02), and it is a positioning fix rather than
   a spacing one. .btn-next carries margin-top:1.1rem; a flex item's margin counts toward its container's
   height, so this wrapper was 17.6px taller than the button with all of it ABOVE. `bottom: 100%` therefore
   meant "the wrapper's top", not "the button's top", and the menu floated ~23px clear of a button it was
   supposed to be attached to. Halving the .35rem gap moved 2.8px of that and was invisible, which is how the
   real cause was found.
   Moving the margin up one level is layout-neutral -- the wrapper now occupies exactly the space the button's
   margin used to, so Reports stays aligned with Add Students beside it -- and it makes `bottom: calc(100% +
   .35rem)` mean what it says. */
.rr-clsreports { position: relative; display: inline-flex; margin-top: 1.1rem; }
.rr-clsreports > .btn-next { margin-top: 0; }
/* OPENS UPWARD (Ron, 2026-09-02) -- `bottom`, not `top`. This toolbar sits BELOW the roster table, so on a
   class of any size the button is near the bottom of the page: downward was off the viewport, and the moment
   Class Skills made this two items the second became unreachable.
   Up is the CORRECT direction here rather than a fallback, because the geometry is not a coincidence -- the
   button is at the page bottom by construction.

   IT STAYS `absolute`, and that matters more than it looks. A first attempt made it `fixed` and placed it
   from JS with the row menu's rpAnchorMenu. Two things broke. `min-width: 100%` means "at least as wide as
   the button" only while the containing block IS the button's wrapper; under `fixed` the containing block
   becomes the VIEWPORT, so the menu went full-screen-width. And rpAnchorMenu's last-resort clamp keeps a
   menu on screen by abandoning its trigger, so near the page bottom it detached and sat at the top of the
   window. A fixed direction in CSS has neither failure, needs no interop, and cannot flash at the wrong
   position before a script moves it. */
.rr-repmenu { position: absolute; left: 0; right: auto; bottom: calc(100% + .35rem); top: auto;
              min-width: 100%; width: max-content; text-align: left; }
/* Line the item text up with the BUTTON text above it. The button pads .88rem; the item sits inside
   .acct-menu's own 6px padding, so its own padding must be .88rem MINUS that 6px or the two disagree by
   exactly the menu's inset. Expressed as a calc so it holds if the root font size changes. */
.rr-repmenu .acct-item { padding-left: calc(.88rem - 6px); }
/* Size comes from .btn-sm in the markup. All this rule owns is the flex box that keeps the Class Reports
   caret on the text's baseline. */
.rr-rosteractions .btn-next { display: inline-flex; align-items: center; gap: .25rem; }
/* Brief confirmation, bottom-centre, self-dismissing. Above the modal layer so a toast raised as a
   dialog closes is never painted behind its backdrop. */
/* BELOW the header, not on it. At top:1.2rem this dark pill (#22303c) sat on the navy app header (#1b3a57)
   — two darks stacked, so it was present but camouflaged. Clearing the header puts it on white content where
   it has real contrast, and keeps it in the path the eye takes when a centred dialog closes. */
.rr-toast { position: fixed; left: 50%; top: 6.5rem; transform: translateX(-50%);
            background: #22303c; color: #fff; padding: .6rem 1.15rem; border-radius: 999px;
            font-size: .95rem; font-weight: 600; box-shadow: 0 8px 24px rgba(0,0,0,.28);
            z-index: 1100; animation: rr-toast-in .18s ease-out; }
@keyframes rr-toast-in {
    from { opacity: 0; transform: translate(-50%, -.5rem); }
    to   { opacity: 1; transform: translate(-50%, 0); }
}
@media (prefers-reduced-motion: reduce) { .rr-toast { animation: none; } }
/* First run: no classes yet, so there is nothing to switch to and no table to show. */
.tp-firstrun { text-align: center; padding: 2.2rem 1.5rem; }
.tp-firstrun .lede { margin: .4rem 0 1.2rem; }
/* Bigger and closer to the title than it was as a footnote under a 1.4rem heading (Ron, 2026-09-12): the
   class name left this block, so Class Settings is the only thing in it and no longer reads as a caption to
   something above it. */
.rr-clsset { font-size: .92rem; }
/* Half the title's usual gap. .report-title gives every report 1.25rem of air before its body, which is right
   on a document and too much here, where what follows is a single control rather than a letterhead. */
.rr > .report-title { margin-bottom: .6rem; }

/* ============================================================================================================
   EVERY PRINT RULE LIVES BELOW THIS LINE, AND THAT IS THE POINT (Ron, 2026-09-12).

   @media print ADDS NO SPECIFICITY. A rule inside it ties with an equally specific rule outside it, and a tie
   is broken by SOURCE ORDER -- so a print rule declared halfway up this file loses to any screen rule of the
   same weight declared below it. Not a hypothetical: the print containers' padding rule is one class, and
   .tc-drill is one class and sat ~790 lines later, and it won. Every teacher report printed with no side inset
   and clipped left and right edges, for days, because the failure is invisible on screen -- it shows only on
   paper, at the edges, where a reader blames their printer.

   Keeping the three blocks last means nothing can out-position them. It closes the CATEGORY rather than the
   instance: scoping .tc-drill fixed one rule and left the next one armed.

   THE THREE BLOCKS KEEP THEIR ORIGINAL ORDER relative to each other, because print rules still override each
   other on source order. Do not reorder or merge them.
   ADD NEW PRINT RULES HERE, at the bottom -- not in a block of their own further up.
   ============================================================================================================ */

/* PAGE BREAKS. docs/ReportsAndPromotion_Spec.md §170 flags these as owed for every paginating report; this is
   the first one that needs them. A repeating header is the difference between page two being readable and
   being a wall of unlabelled numbers, and a row split across a page break puts a student's name on one sheet
   and their scores on the next. */
@media print {
    /* NOTHING HERE SHRINKS THIS TABLE, deliberately. Tightened gutters (6px) and a capped name column (26%)
       lived here for one evening while the printed page was 720px wide; both were compensations for that,
       and both came out with it. At 748px the nine columns fit as they always did.
       If the page is ever narrowed again, this is where the room would have to come from -- and it should
       be chosen, not discovered when the Average column silently leaves the sheet. */
    /* 4px off each side of the FOUR STRANDS AND AVERAGE -- columns five through nine (Student, Gr, Level and
       Passages are one to four), 10px on screen down to 6px on paper. 8px a column, 40px across the five.
       Print only: on screen the table has the window and the roomier gutters read better.
       WHERE THE 40px GOES is now decided rather than discovered, because the columns are sized by rule below
       and not by appetite: it is spent on STUDENT, the one column that wraps. Under the old auto layout this
       rule did nothing visible -- .cs-table is width: 100%, so trimming a floor that was not binding changed
       the distribution and not the edges. That is why an earlier 1px version of this rule appeared to have no
       effect; it was measuring the wrong thing, not failing.
       6px is the floor for this table and not a number to keep shaving: the cells are centred, so the gutter
       a reader sees between two columns is 12px of combined padding with no rule between them. */
    .cs-table th:nth-child(n+5), .cs-table td:nth-child(n+5) { padding-left: 6px; padding-right: 6px; }
    /* FIXED LAYOUT, WIDTHS BY RULE (Ron, 2026-09-11) -- the same thing that makes Class Standards look
       composed, and for the same reason.
       Auto layout sizes by APPETITE. This table's floor is ~697px and its preferred width ~870px, so given
       748px it spends every pixel distributing the surplus, and no amount of trimming content changes that:
       shortening VOCABULARY by 38px lowered the floor and moved the outer edge not at all, because
       width: 100% was the binding constraint, never the content.
       Fixed layout sizes by RULE. The columns get what they are given, the content is drawn inside, and
       cells keep the internal slack that reads as margin.
       The percentages come from measured floors -- max(header, widest data) + padding -- with the ~51px of
       headroom going mostly to STUDENT, where a wrapped name costs a row. They total 100, and they are
       percentages rather than px so the sheet's width can change without re-deriving them.
       THE FOUR STRANDS ARE EQUAL AT 9.6%, which is the point of the exercise: three of them were always at
       the CELL floor -- the ~57px slot+pill+slot line plus 12px of padding -- and only INTEGRATION stood out,
       because it is one word with no break opportunity and a browser will not wrap it however narrow the
       column is. Splitting it by hand into INTEGRA / TION (see .cs-head-line, and the th in ClassManage) makes
       its widest fragment ~59px, under the cell floor, so the heading stops sizing the column at all.
       KEY IDEAS is the one strand heading still wider than the cell floor, at ~78px. It has a space in it, so
       it can be allowed to wrap whenever those last 6px are wanted -- and the header row is already two lines
       tall, so the wrap would now cost nothing vertically. Left alone because nothing needs the 6px yet. */
    .cs-table { table-layout: fixed; }
    /* THE NAME WRAPS ON PAPER. .cs-name is white-space: nowrap, which was invisible under auto layout --
       the column simply grew to fit the longest name. Under FIXED layout the column is what it is told, so a
       31-character name cannot fit 170px and overflows across Gr and Level instead. Wrapping costs a second
       line on a few rows; overflowing costs a readable table. */
    .cs-table td.cs-name { white-space: normal; }
    /* THE STRAND CELLS ARE WIDER THAN THEIR PILL. Each carries a fixed .9rem slot on BOTH sides -- one
       reserving space, one holding the booster dagger -- so the pill is centred on the cell's axis whether
       the dagger is there or not (see ClassManage for why that matters). At .9rem the line is
       14.4 + ~38 + 14.4 = ~67px BEFORE padding, and Craft and Vocab were given a 49px content box. The line
       could not fit, and an overflowing centred line does not read as centred: Ron saw the numbers as
       right-justified. Those two showed it first because they are the narrowest.
       The slots go to .6rem on paper. Two per cell across five columns is ~48px returned, which is what
       makes the true minimums total 724px instead of 758px -- over the sheet. The dagger still reads at
       .6rem; it is a single character. */
    .cs-boost { width: .6rem; }
    /* Widths from the TRUE floor -- max(header, full cell line) + padding -- not from the header alone,
       which is what the first pass measured and why it came up short. STUDENT gives up the slack: it is
       the one column that wraps, and its floor is its longest single word. */
    /* SCOPED AWAY FROM .cst-table, which wears BOTH classes (2026-09-11). Class Standards is
       `class="cs-table cst-table"`, so every one of these nth-child widths reached it too: it declares only
       `th.cst-rowhead { width: 36% }` and lets fixed layout split the rest equally between the twelve
       students and the class column, and eight stray widths totalling 87% landed on the first eight of those.
       :not(.cst-table) is the smallest fence that holds. Any width rule added here from now on has to carry
       it, or it lands on a fourteen-column matrix that was never measured for these numbers. */
    /* ON THE <col> ELEMENTS, NOT THE HEADER CELLS (2026-09-11). Fixed layout reads <col> widths first and
       only falls back to the first row's cells, so declaring them here frees the first row to be something
       other than the column headings -- which is what lets the letterhead repeat (see the thead comment in
       ClassManage). It also removes the need for :not(.cst-table): Class Standards wears .cs-table as well
       and was picking these up through th:nth-child(), but it has no <col> elements for them to land on. */
    .cs-table col:nth-child(1) { width: 28.4%; }  /* Student   ~212px, wraps */
    .cs-table col:nth-child(2) { width: 4.8%; }   /* Gr         ~36px */
    .cs-table col:nth-child(3) { width: 9.4%; }   /* Level      ~70px */
    .cs-table col:nth-child(4) { width: 11.0%; }  /* Passages   ~82px */
    .cs-table col:nth-child(5) { width: 10.4%; }  /* Key Ideas  ~78px, header-bound */
    .cs-table col:nth-child(6) { width: 9.6%; }   /* Craft      ~72px, cell-bound */
    .cs-table col:nth-child(7) { width: 9.6%; }   /* Integra/tion ~72px, cell-bound */
    .cs-table col:nth-child(8) { width: 9.6%; }   /* Vocab      ~72px, cell-bound */
    .cs-table col:nth-child(9) { width: 7.2%; }   /* AVG        ~54px, no slots */
    /* STATED, not left to the UA default, on both sheets that repeat a letterhead. This is the property that
       makes a browser re-render the thead at the top of each printed page, and it is the whole mechanism the
       runhead depends on -- worth being explicit about rather than inheriting. */
    .cs-table thead, .rr-cardtable thead { display: table-header-group; }
    .cs-table tr, .cs-foot li { break-inside: avoid; }
    /* The header repeats per page; the TOTAL must not. `table-footer-group` renders at the bottom of EVERY
       page, so on page one a mastery row would sit under a partial list and read as that page's total. */
    .cs-table tfoot { display: table-row-group; }
    .cs-prov td { background: transparent; }
}

/* IN A PRINT BLOCK, and it has to be. These undo the two screen rules above, at the same specificity and
   later in the file -- unscoped they would simply cancel them, and the screen would get its header three
   times again with no rule between the sections. The page break does the separating on paper, so the rule
   and its spacing come off; the header returns on every page, which is why it lives in the markup at all. */
@media print {
    .cst-page + .cst-page { break-before: page; margin-top: 0; padding-top: 0; border-top: 0; }
    .cst-page + .cst-page .cst-head { display: block; }
}

@media print {
    /* RELEASE THE SCROLLBAR GUTTER ON PAPER (2026-09-07). html carries `scrollbar-gutter: stable` (see the
       top of this file) to stop the centred 1120px container nudging sideways between a long screen and a
       short one -- a SCREEN problem, but the rule was never scoped away from print. Printing, it reserved
       ~15px on the inline-end edge, so the page's content box lost that width on the RIGHT only: the
       bordered header blocks sat tight to the left margin with a visible gap on the right, and the
       printable width was 733px rather than the 816 - 2x9mm = 748px the layout is built for.
       Measured, not guessed: .report came back 733px wide at an 816px page. */
    html { scrollbar-gutter: auto; }
    /* And never carry the dialog scroll-lock onto paper: a print started while anything modal is open must
       not clip the sheet to one viewport. */
    html:has(.rr-modal-back), html:has(.rp-modal-backdrop) { overflow: visible; }

    body * { visibility: hidden; }
    .rr-print-sheet, .rr-print-sheet * { visibility: visible; }
    /* FULL WIDTH AT left: 0. The sheet spans the page and the inset comes entirely from the 9mm padding on
       the rule below, so the printable width is 816 - 2x9mm = 748px on Letter -- the number every measured
       figure in this file is against, the column percentages included.
       A DESIGNED WIDTH (720px, centred) WAS TRIED HERE ON 2026-09-11 AND REVERTED. The idea was sound: a
       width is identical on Letter and A4 (794px), where an inset is not, so the layout would not depend on
       the paper. Both ways of expressing it failed, and the record matters more than the idea:
         - left: 0 + right: 0 + width: auto + margin-inline: auto -- the textbook centring for an absolutely
           positioned box -- printed hard against the RIGHT edge with a wide gap on the left. Only one of the
           two auto margins resolved.
         - symmetric percentage padding, calc((100% - 720px) / 2), was worse and is the more instructive
           failure: PERCENTAGES RESOLVE AGAINST THE CONTAINING BLOCK, which for an absolutely positioned
           element in print is not reliably the page box. See the 9mm comment below, where that is written up
           properly; it broke every report on the sheet, not just this one.
       So: no auto margins, and no percentage of this box. If the designed-width idea is ever revisited it
       needs a containing block that is known, not assumed. 748px is settled in the meantime -- the margins
       it gives were checked on paper and accepted. */
    .rr-print-sheet { position: absolute; left: 0; top: 0; width: 100%; }

    /* The page inset, moved off @page (see the comment above it) so the browser has no margin to print its
       URL/date/page-number furniture into. Applied to BOTH print containers, so every report loses the
       furniture -- the four student reports under .rp-print-doc and the sign-in cards under
       .rr-print-sheet -- not just the Certification Report. border-box keeps width:100% honest. */
    /* THIS NUMBER IS SETTLED AT 9mm -- two ways of widening it were tried on 2026-09-08 and both were
       wrong. Taking the side inset to 12mm cut BOTH edges and by more than the original defect (more
       padding on a border-box container cannot widen its content, so the page was never simply too wide),
       and a 3% zoom shrank the whole document to solve a defect visible in two boxes. The fix is on the
       two boxes themselves, further down; see "THE TWO FULL-WIDTH BOXES PULL IN". */
    /* THE TOP INSET TRAVELS WITH WHATEVER REPEATS; the sides and the foot stay on the container.
       Padding belongs to a BOX, applied once at its start and end -- not to each page the box spills
       across. With @page { margin: 0 } (deliberate: it leaves the browser nowhere to print the URL, the
       date and "1/2" onto a formal record) a continuation page therefore has no top inset at all, and
       page two of any multi-page report began at the paper's edge. Left and right are safe because the
       box is one column: every page shares them.
       So the top comes off here and is re-applied to the thing that renders at the top of each page --
       .cs-runhead (a thead row, which browsers repeat), .cst-head (one per section, one section per page),
       and .report (each one starts a page in the class-wide certification sheet). */
    /* 9mm A SIDE, AN ABSOLUTE LENGTH -- and that is the point, not an accident (2026-09-11).
       This was briefly `calc((100% - 748px) / 2)`, to express the sheet as a designed width rather than an
       inset, so it would be the same on A4 as on Letter. It broke every printed report, progressively and
       confusingly: PERCENTAGE PADDING RESOLVES AGAINST THE CONTAINING BLOCK, and for an absolutely
       positioned element in print that is not reliably the page box. Resolved against anything wider, the
       left padding balloons and the content walks off the right edge -- which is exactly what was seen,
       and which no amount of shrinking the tables could fix, because the tables were never the problem.
       An absolute length has no such dependency. If the designed-width idea is ever revisited, it must NOT
       be built on a percentage of an absolutely positioned box. */
    .rr-print-sheet, .rp-print-doc { box-sizing: border-box; padding: 0 9mm 9mm; }
    /* THE TOP INSET FOR BOTH .rr-print-sheet SHEETS NOW RIDES ON THEIR RUNHEAD, which is a thead row and so
       renders at the top of EVERY page rather than once per box (2026-09-11). This replaces
       .rr-sheet-flat { padding-top: 9mm }, which could only inset page one -- the class named the sheets that
       had no repeating header to hang the inset on, and now neither of them is one, so the class is gone.
       Printers keep an unprintable edge of ~4-6mm, so a continuation page starting at the paper's edge was
       not merely tight: the first line of it was being clipped. */
    .cs-runhead > td { padding-top: 9mm; }
    .cst-head { padding-top: 9mm; }
    .rp-print-doc .report { padding-top: 9mm; }
    .rr-noprint { display: none !important; }
    .rr-card { border-color: #000; }

    /* ---- THE STUDENT REPORTS (2026-09-03) ------------------------------------------------------------
       A second print container, applied by Components/StudentReports to the wrapper around all four report
       screens. Until now none of them could be printed at all: this block revealed .rr-print-sheet and
       nothing else, so the Certification Report a parent conference wants, and the certificate a student had
       just earned, existed only on screen.
       It is the TEACHER's host that marks the wrapper. The same components render in the student app with no
       wrapper and no Print button -- printing is a teacher and parent product. */
    .rp-print-doc, .rp-print-doc * { visibility: visible; }
    .rp-print-doc { position: absolute; left: 0; top: 0; width: 100%; }

    /* 800px is 8.33in at the CSS print resolution -- WIDER THAN THE PRINTABLE AREA of a Letter page with
       ordinary margins, so the on-screen reading measure would have clipped the right edge of every report.
       The page's own margins are the measure on paper. */
    .rp-print-doc .report, .rp-print-doc .story-report { max-width: 100%; margin: 0; }

    /* ONE STUDENT PER SHEET (2026-09-08). break-before on every report after the first, rather than
       break-after on every report: break-after on the last one emits a trailing blank page in Chrome, which
       is the classic version of this bug and it only shows up at the printer.
       The screen separator above is undone here -- its rule and 2.2rem would print at the top of every page
       after the first, where the page break already does that job.
       DELIBERATELY NOT break-inside: avoid on .report. One page per student holds because the worst case
       (Diamond, full support, full Journey) was measured at 988px of a 988px page -- but if a report ever
       does overflow, `avoid` turns it into a blank page followed by a broken one. Better it spills and is
       visibly one page long than silently disappears. Note this promise is now FLEET-WIDE: anything added to
       the report breaks thirty pages at once, not one. */
    .rr-certs > .report + .report { break-before: page; margin-top: 0; padding-top: 0; border-top: 0; }

    /* The buttons are inside the container, so they need hiding by name rather than by .rr-noprint -- and
       hiding them is what lets the Print button live in the bottom row, where the "nothing above the report
       header" rule puts it. */
    .rp-print-doc .report-actions, .rp-print-doc .summary-actions { display: none !important; }

    /* PAGE BREAKS (ReportsAndPromotion_Spec §170). The Progress Report lists up to twenty passages and is the
       one that certainly paginates; the others can, at a large print scale or a small page. A repeating
       header is the difference between page two being readable and being unlabelled numbers, and a row split
       across a break puts a passage title on one sheet and its scores on the next. */
    .rp-print-doc thead { display: table-header-group; }
    .rp-print-doc tr { break-inside: avoid; }
    .rp-print-doc .report-meta, .rp-print-doc .journey { break-inside: avoid; }

    /* THE CERTIFICATE IS ONE PAGE OR IT IS NOTHING -- an award split across a break is not an award. */
    .rp-print-doc .cert-frame { break-inside: avoid; }

    /* ---- VERTICAL BUDGET ON PAPER (Ron, 2026-09-04) --------------------------------------------------
       A 12-passage Progress Report ended a page short and trailed both footnotes onto page two, which is
       the worst possible break: a sheet carrying two lines. Shortening the footnotes bought ~47px and was
       not enough, so the space comes from leading rather than from words -- every rule below is print-only
       and the screen is untouched, because screen spacing is set for reading on a page that scrolls, where
       none of it costs anything.

       THE HEADER KEEPS ITS TWO-COLUMN LAYOUT (Ron, 2026-09-04). A three-across version was tried and read
       as crowded, and it was answering the wrong question anyway: HORIZONTAL spacing buys nothing on a page
       that is short of HEIGHT. Tightening the label gutter or the column gap frees width the header does not
       need and returns zero vertical pixels -- the only way a horizontal change can save height is by
       un-wrapping a value that was on two lines, and nothing in this block wraps.
       What CAN come out of the header without touching its layout is its own vertical padding: the row
       padding and the box padding, below. Worth ~19px against the three-column version's ~29px, and it
       leaves the block looking like itself. Shared by all three student reports. */
    /* 15px below the identity block, up from the base .45rem/7.2px (Ron, 2026-09-08): the two column
       heads sat too close under it. Taken HERE rather than on the headings themselves, because
       "Your Reading Skills" and "Reading Support" are levelled against each other by two separately
       measured numbers -- the strand table's -6px lift and .support-sec's 11px. Pushing the row above them
       moves both by the same amount and leaves that pairing untouched; adding 8px to each would have meant
       re-deriving both. */
    /* No padding left to tighten -- the card came off this block entirely and its rows are spaced by
       line-height now (see .rp-print-doc .report-meta above), so only the gap below it is print's business. */
    .rp-print-doc .report-meta { margin-bottom: 15px; }

    /* The leading around the title, the date and the legend. Each is set for a scrolling screen; on paper
       they are ~43px of white the page cannot spare. */
    .rp-print-doc .report-title { margin: 0 0 .6rem; }
    .rp-print-doc .mhr-legend { margin-bottom: .5rem; padding: .35rem .7rem; }
    .rp-print-doc .mhr-t { margin-bottom: .5rem; }

    /* THE BIGGEST SINGLE LEVER, and the least visible: .1rem off each row's vertical padding is ~3px a row,
       and a full report is fifteen rows -- two header rows, twelve passages, the Skill Average and the
       mastery tally. ~48px, more than the footnotes needed on their own. Horizontal padding is untouched:
       that is the air Ron just added to the score columns. */
    /* .22rem, down from .3rem (Ron, 2026-09-12). With twenty passages this report ran a few lines onto a
       second page and the only thing on it was the two footnotes -- the worst way to spill, since a reader
       turns a page for a sentence. 24 rows carry the table (20 passages, the all-passages row, the mastery
       tally and two header rows), so 1.3px off each side of each is ~60px back. Print only: the screen has
       room, and rows this tight read worse where nothing is competing for space. */
    .rp-print-doc .mhr-t th, .rp-print-doc .mhr-t td { padding-top: .22rem; padding-bottom: .22rem; }

    /* ---- THE CERTIFICATION REPORT ON ONE PAGE (Ron, 2026-09-04) --------------------------------------
       A teacher printing a class set wants 35 single sheets, and this report was running a little past one.
       The strand table is where the page goes: four strands plus ~10 anchor rows plus the Challenge and
       Overall is ~16 rows, and at .5rem of vertical padding on .95rem text that is ~590px of a ~980px page.
       A fifth of a rem off each row is ~102px -- the largest saving available anywhere on the page and the
       least visible, since it touches leading and not one number. Horizontal padding is untouched.

       WHAT THIS CANNOT PROMISE. Report height varies by ~200px student to student: a Journey table appears
       once there is a prior level, and Reading Support is one green line for a student who read
       independently but a headed table for everyone else -- which is most of a class, since English passage
       audio is ON by default. This makes the typical report fit; a Diamond student with three prior levels
       and full Spanish + audio may still run over, and when one does it is now the FOOTNOTES that fall off
       the bottom rather than the support table or the first-try figure. */
    /* ROW TIGHTENING (Ron, 2026-09-07) — 3px of cell padding and a 1.3 line-height across EVERY table on the
       sheet, worth a measured 71px of headroom (988 -> 917 against a 988px page) and costing not one number.
       It is what makes dropping the ten anchor rows unnecessary: 293px of CCSS detail was going to be traded
       for space that whitespace could give up instead.

       IT HAS TO BE EVERY TABLE, and that is the part worth knowing. The page is as tall as its TALLER column,
       and the two are tied: left is header 258 + strand table 730, right is support 281 + Journey 284 +
       footnotes 143, both landing on 988. Tightening the strand table ALONE takes it from 730px to 516px and
       moves the page height by exactly 0px — measured, twice, before this was understood. Anything aimed at
       page height must shrink both columns or it shrinks nothing.

       4px / 1.4 after QA (2026-09-07). 3px / 1.3 measured 71px of headroom and read too tight on paper --
       these are documents a parent reads, not tables a machine scans. The measured ladder, so the next
       adjustment starts from numbers rather than a guess:
           3px   / 1.3   -> 71px headroom   (too tight)
           3.5px / 1.35  -> 50px
           4px   / 1.4   -> 30px            <-- here
           4px   / 1.45  -> 20px
           4.8px / 1.4   -> 14px
           4.8px / 1.45  ->  4px
       30px still absorbs a long school name, printer variance, or the "Not offered" footnote appearing. */
    .rp-print-doc .score-table th, .rp-print-doc .score-table td { padding-top: 4px; padding-bottom: 4px; }
    .rp-print-doc .score-table, .rp-print-doc .support-table { line-height: 1.4; }
    .rp-print-doc .score-table { margin-bottom: .6rem; }
    .rp-print-doc .report-note { margin-bottom: .3rem; }
    /* ==> THE ONE NUMBER THAT LEVELS THE TWO COLUMN HEADINGS. Raise it to push "Reading Support" DOWN,
       lower it to lift it. <==
       The two headings start at the same y and still do not look level, because they are different objects:
       "Your Reading Skills" is a table header cell sitting below .score-table's top padding (.3rem in print,
       not the .5rem on screen), while "Reading Support" is an h3 whose text starts at its own top edge, in
       larger type, with a heading's tighter line box. Every one of those pushes the two baselines apart by a
       different amount, which is why this is a measured number and not a derived one — arithmetic put it at
       3px and then 6px and both sat high on paper.
       Nowhere near the screen rule's 1.6rem, which separates the section from what precedes it in a single
       column — a problem that does not exist once nothing is above it. */
    /* -5px, MEASURED at the real 816px page width (2026-09-07): the "Reading Support" text and the
       strand table's "Your Reading Skills" text then start at the same y, delta 0. It went negative
       when Journey left the full-width band -- the row it leads no longer begins where it used to --
       so the old 14px, tuned by eye against a different layout, sat it 19px low. */
    /* 16px, RE-MEASURED 2026-09-07 after `.report h3` was found to be beating the heading rule: both
       section headings had been rendering a size small, and every offset tuned against them was tuned
       against the wrong type. At the correct 1.15rem the two column headings' text starts at the same
       y, delta 0. */
    /* 5px (16 measured -> 12 -> 11 -> 5, all by eye on the sheet). The 16px was MEASURED to delta 0 between
       the two heading texts, and that measurement no longer describes this page: at 11px Reading Support
       should have sat 5px ABOVE level and instead it read BELOW it, so something between those two headings
       has moved since 2026-09-07 -- the likeliest candidate is the strand table's header row, whose three
       right-hand cells dropped from 1.15rem to .78rem the same day. The number below is Ron's eye, not a
       derivation; if this pairing is ever tuned again it is worth RE-MEASURING both heading tops rather
       than trusting either recorded value. The note
       above still describes that measurement -- it is kept because it is true of the TEXT baselines. Ron
       reading the finished sheet judged the whole right column to sit low against the strand table beside
       it, which is a different question from where two baselines land. (That reason as first written --
       "the support heading carries a rule the strand table header does not" -- was wrong twice over: the
       strand table DOES rule its header, and the support heading's own rule was removed on 2026-09-08.
       The 5px stands on Ron's eye, which is what it always stood on.) This
       is the deliberate 4px, not a correction of the measurement. .support-sec leads column 2, so its top
       margin moves Correct 1st Try, Journey and the footnotes with it. */
    .rp-print-doc .support-sec { margin-top: 5px; margin-bottom: 7px; }

    /* THE LAST 10px, and it is the difference between one sheet and two (measured 2026-09-07: 998 -> 988
       against a 988px page). Both are dead space rather than anything a reader sees:
         - the strand table's bottom margin is the last thing in the LEFT column, so it pads the foot of the
           page and nothing follows it;
         - the footnote block's top margin sat above its own separator rule, which already provides the
           break; the 3px under the rule is what actually keeps the small print off it.
       Shrinking anything in the right column alone does NOT help: the strand table spans those rows, so the
       column cannot end higher than the table does. That is why this comes off the left column and the
       block below it. */
    .rp-print-doc .strand-table { margin-bottom: 0; }
    /* 10px GIVEN BACK (Ron, 2026-09-08). The note above explains why this was taken to 0 -- it was the
       last 10px of the one-page fit, 998 -> 988. It is affordable again because the rule under "Reading
       Support" was removed the same day, returning ~12px to this column, and Journey gave back ~6px more.
       Ron reading the finished sheet wanted the separator and its three footnotes carried down off the
       block above them. Do NOT reclaim this without re-measuring the whole sheet. */
    /* 28px above (10px -> 22px -> 28px) and indented to the TABLE'S TEXT EDGE (Ron, 2026-09-08), now that the block
       spans the sheet under both columns rather than riding in the right column.
       .6rem is not a guess: it is the left padding `.score-table th` gives every cell, so the notes start
       on the same vertical as "Your Reading Skills" and every strand name under it. Padding, not margin,
       so only the TEXT moves -- the separator rule keeps the full width, exactly as the strand table's own
       header rule runs left of the words it heads. */
    .rp-print-doc .report-notes { margin-top: 28px; padding-top: 3px; padding-left: .6rem; }
    /* 12px OFF THE RIGHT (Ron, 2026-09-08). Same defect the two header boxes had: this pill draws a
       VERTICAL border at the content edge, which is past where the printer lays ink down, so its right
       border was cut. Right in every layout; the left is added below for the wide one only. */
    .rp-print-doc .support-independent { margin: .6rem 12px .4rem 0; }
    /* THE LEFT 12px IS FOR THE WIDE LAYOUT ONLY (Ron, 2026-09-08). With no Journey either, the report
       collapses to one column and this pill spans the sheet -- its left border then sits on the content
       edge like the header boxes, and takes their inset to match. In the TWO-column layout its left edge
       starts mid-page under the Journey table, where an inset would only pull it out of line with the
       table above it. Same selector as the single-column rule, so the two cannot disagree about which
       layout is in play. */
    .rp-print-doc .report:not(:has(.support-table)):not(:has(.journey)) .support-independent { margin-left: 12px; }
    .rp-print-doc .support-table th, .rp-print-doc .support-table td { padding-top: 4px; padding-bottom: 4px; }
    /* Keep each of the three tail blocks whole; if one has to move to a second sheet, it moves entire. */
    .rp-print-doc .report-notes, .rp-print-doc .support-sec { break-inside: avoid; }

    /* Half the leading above each footnote on paper. Screen spacing is set for reading on a scrollable page;
       in print those two gaps are the difference between a 12-passage report ending on page one and trailing
       a single line onto page two. */
    .rp-print-doc .mhr-foot-note { margin-top: .25rem; }
    /* THE CHEVRON IS A CLICK AFFORDANCE, so it does not print (Ron, 2026-09-12). It says "this row opens the
       passage report", which is true on screen and meaningless on paper -- the same reason the action rows and
       the "Expand a skill" note are already suppressed here.
       display: none rather than visibility: hidden, because the point is the WIDTH: the glyph plus its 3px
       margin come out of the title column on every row, which is where two titles were wrapping. It is already
       aria-hidden, so nothing is lost to a screen reader. */
    .rp-print-doc .mhr-chev { display: none; }

    /* The matrix's scroll wrapper must not clip on paper. It exists so an enlarged table scrolls instead of
       overflowing the page; on a printed sheet there is nothing to scroll, and an overflow container clips
       what a screen would have let you reach. The columns are rem-sized and fit a Letter page at print size,
       so this is a guard rather than a fix -- but a silently truncated score column is the kind of failure
       nobody notices until a parent is holding it. */
    .rp-print-doc .mhr-scroll { overflow: visible; }

    /* WHAT SHE EXPANDED IS WHAT SHE GETS. The anchor drill-down rows are conditionally RENDERED, not hidden
       with CSS, so a collapsed anchor is absent from the page and no rule is needed to keep it off paper --
       and an expanded one prints, which is the right way round for someone who opened it before pressing
       Print. Only the caret goes: a disclosure triangle on paper points at an interaction that is not there.
       The toggle itself is already borderless and inherits its font, so it prints as the text it looks like. */
    .rp-print-doc .anchor-caret { display: none; }

    /* ---- TWO-COLUMN PRINT BODY: 55/45, STANDARDS LEFT (2026-09-06) -----------------------------------
       The worst printed case measured 1471px against a 988px page -- half a page over -- and no spacing
       change closes it (docs/Outstanding.md). This is the layout answer, and unlike dropping the anchor
       rows it costs NO information: every row still prints.

       PRINT ONLY. On screen the report is untouched: one column, full skill names, Mastery pill, and the
       anchor drill-down reading as a single scannable table.

       LEFT (55%) is the whole strand table, top to bottom. RIGHT (45%) is the context: Journey, Reading
       Support, notes. Keeping the standards in ONE continuous column matters -- an earlier attempt paired
       the strand tbodies across two grid cells (Key Ideas beside Craft) and broke the strand -> anchor
       hierarchy, so the eye had to jump columns to read four related scores.

       Rows are assigned EXPLICITLY rather than left to auto-placement. In DOM order Journey precedes the
       strand table, so auto-placement puts them on different rows and leaves a hole in column 1. An absent
       section (no certificate banner, no Journey on a single-level student) simply collapses its row to
       zero height, so the fixed row numbers stay safe. */
    /* ROW 7 TAKES THE SLACK (2026-09-08). The strand table spans rows 5-7 and grid shares a spanning
       item's extra height EQUALLY across the tracks it spans. That was harmless while the right column had
       three items filling those rows -- until the footnotes moved to row 8 today and left row 7 empty, so
       a third of the table's height began being injected between the blocks beside it. Making the last
       spanned row 1fr collects that leftover at the FOOT of the column, where it reads as empty space
       instead of as gaps punched between sections. Row 8 (the footnotes) stays implicit and auto. */
    .rp-print-doc .report { display: grid; grid-template-columns: minmax(0, 55fr) minmax(0, 45fr);
                            grid-template-rows: auto auto auto auto auto auto 1fr;
                            column-gap: 18px; align-items: start; }

    /* ONE COLUMN WHEN THE REPORT IS LIGHT (Ron, 2026-09-07). Two columns exist to fit a HEAVY report on one
       sheet. A student who read independently has no support table, and usually no Journey either, so the
       right column holds a line or two against a 730px strand table -- and because that table SPANS the
       column's rows, the grid distributes its height across them and pins the survivors to the top of each:
       the "Read independently" line at the top, the footnotes 700px below it, blank in between. That blank
       is injected space, not leftover space, which is why it looks broken rather than merely empty.
       Such a report fits one column comfortably, so it takes one. The support TABLE is the test rather than
       the section: .support-independent is the collapsed stand-in for it, and its presence is exactly the
       case this is for. */
    /* ...AND NO JOURNEY EITHER (Ron, 2026-09-08). The paragraph above rests on "a student who read
       independently has no support table, and usually no Journey either". Wen-Li Chen is the case
       "usually" was hiding: she read independently and has cleared Gold, so she carries Bronze and Silver
       in her Journey. One column gave her a report that ran onto a second sheet. The test is now the
       RIGHT COLUMN having nothing in it, which is what the single column was ever about. */
    .rp-print-doc .report:not(:has(.support-table)):not(:has(.journey)) { display: block; }

    /* JOURNEY LEADS THE COLUMN WHEN THERE IS NO SUPPORT TABLE (Ron, 2026-09-08). Otherwise the right
       column opens with the one-line "Read independently" pill and the Journey table hangs below it with
       the column's whole slack above. Journey goes to the top, level with "Your Reading Skills", and the
       pill follows it. 5px is .support-sec's own levelling number -- both headings are .rp-sec-head, so
       what levels one levels the other. */
    .rp-print-doc .report:not(:has(.support-table)) > .journey { grid-row: 5; margin-top: 5px; }
    .rp-print-doc .report:not(:has(.support-table)) > .support-independent { grid-row: 6; }
    .rp-print-doc .report > .report-title  { grid-column: 1 / -1; grid-row: 1; }
    /* .cert-banner and .report-status are the two halves of one either/or: the certificate banner once the
       level is complete, the calm blue "In progress" status until then. BOTH must be named here -- leaving
       the status unplaced auto-flowed it into a 45%-wide cell, which is what wrapped its pill. */
    .rp-print-doc .report > .cert-banner,
    .rp-print-doc .report > .report-status { grid-column: 1 / -1; grid-row: 2; }
    .rp-print-doc .report > .report-meta   { grid-column: 1 / -1; grid-row: 3; }
    /* ROW 4, ACROSS BOTH COLUMNS -- the row this grid left free. Without a placement it was auto-placed into
       ONE column, which halved its width and doubled its height on a page measured to the line. Full width is
       also where it belongs: it qualifies the entire report, not one column of it. */
    .rp-print-doc .report > .report-promoted { grid-column: 1 / -1; grid-row: 4; margin-bottom: 6px;
                                               padding: .3rem .6rem; font-size: .88rem; }

    /* THE TWO FULL-WIDTH BOXES PULL IN 12px A SIDE (Ron, 2026-09-08). Printed, their right border was
       cut off; nothing else on the sheet was.
       WHY ONLY THESE TWO, and why that is not a patch: they are the only elements on the page that draw a
       VERTICAL BORDER at the content edge. Everything else either sits inside a column (the strand table
       is column 1) or ends in horizontal row rules, where losing a pixel or two off the end is invisible.
       So the sheet does not need to be narrower or smaller -- the two drawn edges need to stop sitting on
       the boundary. This costs no layout width, so nothing re-wraps and the one-page budget is untouched.
       LEFT AND RIGHT ONLY: .report-status carries a -.4rem top margin and .cert-banner a 1rem bottom, and
       both are load-bearing. .report-status is borderless and would not have shown the clipping, but it
       is the other half of the banner's either/or and the two must sit on the same lines. */
    /* .report-meta LEFT THIS LIST on 2026-09-12 and the rule above is why: it no longer draws a border, so
       it no longer has an edge to keep off the boundary -- and an inset block with nothing drawn on it just
       reads as 12px of misalignment against the title above it. The other two still draw theirs. */
    .rp-print-doc .report > .cert-banner,
    .rp-print-doc .report > .report-status { margin-left: 12px; margin-right: 12px; }
    /* JOURNEY STAYS WHERE THE SCREEN PUTS IT (Ron, 2026-09-06): full width across the top of the body,
       between the header block and the two columns, which is its DOM position and where a reader who has
       seen the report on screen expects it. It had been pushed to the foot of the right column, purely
       because that column had room.
       Full width also suits it better than a 45% column — five short cells (level, how cleared, score,
       challenge, date) in ~328px wrap; across the sheet they do not. */
    /* -6px lifts "Your Reading Skills" to sit level with "Reading Support" (Ron, 2026-09-06). It raises
       the whole left column with it, which also buys back 6px of page height — the table is the tallest
       thing on the sheet, so what it saves at the top it saves outright. */
    .rp-print-doc .report > .strand-table  { grid-column: 1; grid-row: 5 / span 3; margin-top: -6px; }
    /* READING SUPPORT LEADS THE RIGHT COLUMN (Ron, 2026-09-06), so its heading sits level with the strand
       table's "Your Reading Skills" header and the two columns start on the same line. Journey drops below
       it. The support block is also the one a teacher reads ALONGSIDE the skills — the conditions the scores
       were earned under — where Journey is retrospective and reads perfectly well further down. */
    .rp-print-doc .report > .support-sec,
    .rp-print-doc .report > .support-independent { grid-column: 2; grid-row: 5; }
    /* Correct 1st Try follows Reading Support directly (Ron, 2026-09-06). It was below Journey, and
       .journey carries a 1.5rem bottom margin, so ~24px of empty column sat between the support table and
       this line. It still comes after the scores it annotates and still precedes the footnotes, so the
       2026-09-01 placement rule holds. */
    /* Footnotes sit directly under Correct 1st Try (Ron, 2026-09-06); Journey takes the last slot. The
       small print now closes the annotation block -- first-try figure, then its footnotes -- and Journey,
       which is retrospective and self-explanatory, ends the column. */
    /* Journey OUT of the full-width band and into the right column under Correct 1st Try (Ron,
       2026-09-07). Across the top it cost its whole height on a sheet already over; in the column it
       fills space the 730px strand table was leaving blank beside it. */
    /* 6px above, -1px below (Ron, 2026-09-08, by eye on the sheet, bottom taken down in two 4px passes
       from 7px). margin-top was 0 -- the screen rule's 20px is exempted here because on paper Journey is a
       right-column grid item, not the header's sibling -- but zero ran it hard into Correct 1st Try above.
       The negative bottom is the same device .support-caveats used: the footnote's line box carries its own
       half-leading, so a zero margin still leaves visible space under it. Net, the block now costs the page
       ~5px LESS than before today. */
    .rp-print-doc .report > .journey       { grid-column: 2; grid-row: 6; margin-top: 6px; margin-bottom: -1px; }
    /* FOOTNOTES FULL WIDTH, BELOW BOTH COLUMNS (Ron, 2026-09-08). They were the last block in the right
       column, under Journey. Below the record, spanning the sheet, is where a reader expects footnotes on a
       formal document -- and at 748px each note sets on ONE line instead of wrapping to two in a 328px
       column, so the block itself is roughly half the height it was.
       IT IS NOT FREE, and this is the trade to watch. The strand table spans rows 5-7 and is the taller
       column, so anything in column 2 costs the page NOTHING -- the notes were riding in space the table
       was already leaving blank. Moving them to their own row below adds their full height to the sheet.
       Estimated at ~85px against a page measured at 988 of 988; Ron's read on the printed sheet is that it
       fits. MEASURE IT -- if it runs over, the cheapest recoveries in order are: set the notes as one band
       (separated by mid dots, or two CSS columns) rather than three stacked paragraphs; merge the two
       methodology notes; and only then touch the strand table's 4px/1.4 rows, which were deliberately
       RELAXED to that on 2026-09-07 and should not be quietly reclaimed.
       No 12px side inset here, unlike the two header boxes: those needed it because they draw VERTICAL
       borders at the content edge. This block's rule is horizontal and matches the tables' full width. */
    .rp-print-doc .report > .report-notes  { grid-column: 1 / -1; grid-row: 8; }
    /* THREE COLUMNS ON PAPER: Level, Score, Date. Completion and Challenge go -- five do not fit
       ~322px, and neither is lost outright: the Challenge is inside the Score, which is now Overall
       Mastery (strands AND Challenge), and Completion is the one of the five a parent reads past.
       Both stay on screen, where all five fit. */
    .rp-print-doc .journey-table th:nth-child(2), .rp-print-doc .journey-table td:nth-child(2),
    .rp-print-doc .journey-table th:nth-child(4), .rp-print-doc .journey-table td:nth-child(4) { display: none; }
    /* margin-bottom 8px, down from the .75rem/12px base (Ron, 2026-09-08): "Every level you have worked
       through." sat too far under the table it annotates. (An earlier draft of this note claimed the table
       is the last thing in column 2 -- it is not: .report-notes takes grid-row 7 beneath it.) */
    .rp-print-doc .journey-table { table-layout: fixed; width: 100%; margin-bottom: 5px; }
    .rp-print-doc .journey-table th:nth-child(1), .rp-print-doc .journey-table td:nth-child(1) { width: 34%; }
    .rp-print-doc .journey-table th:nth-child(3), .rp-print-doc .journey-table td:nth-child(3) { width: 24%; }
    .rp-print-doc .journey-table th:nth-child(5), .rp-print-doc .journey-table td:nth-child(5) { width: 42%; }
    /* The status pill is a label, not a paragraph: it never breaks across lines whatever cell it lands in. */
    .rp-print-doc .status-pill { white-space: nowrap; }

    /* THE SKILL LABELS SHORTEN ON PAPER. At 55% of ~730px the skill cell is ~250px; the longest full name
       ("Development of individuals, events & ideas (R.3)") is ~258px and wraps, and a wrapped row costs
       ~19px -- ten of them give back most of what the second column saved. Standards.AnchorTitleShort
       carries the short forms; both render and CSS swaps them, so nothing is recomputed per medium. The
       CCSS code is on both, so a shortened row is still unambiguous. */
    .rp-print-doc .anchor-long { display: none; }
    .rp-print-doc .anchor-short { display: inline; }

    /* Mastery is a PURE FUNCTION of Score (ScoringService.Band: >=80 / >=60 / >=45), so on paper it is a
       column of derived text costing ~85px of width the narrower table cannot spare. NOTE: the band is
       currently DROPPED from print rather than re-encoded -- carrying it as weight + a legend naming the
       cuts is still to do. Colour alone would fail docs/Accessibility_Rules.md. */
    .rp-print-doc .strand-table th:nth-child(4),
    .rp-print-doc .strand-table td:nth-child(4) { display: none; }
    .rp-print-doc .strand-table { table-layout: fixed; width: 100%; }
    .rp-print-doc .strand-table th:nth-child(1),
    .rp-print-doc .strand-table td:nth-child(1) { width: 64%; }
    .rp-print-doc .strand-table th:nth-child(2),
    .rp-print-doc .strand-table td:nth-child(2) { width: 15%; }
    .rp-print-doc .strand-table th:nth-child(3),
    .rp-print-doc .strand-table td:nth-child(3) { width: 21%; }
    .rp-print-doc .strand-table td[colspan] { width: auto; }

    /* NO TAGLINE ON PAPER (Ron, 2026-09-08). "The Challenge · Apply what you've learned" keeps its
       tagline on screen, where it is doing motivational work for the student; the printed report is a
       record of what was scored and the strap reads as marketing on it. The row still says The Challenge.
       SCOPED TO THE TITLE CELL, not the row. .sec-sub is a general muted-text class: the Overall row's
       "In progress" wears it, and so does the Challenge's own "Not taken" in the SCORE cell. Hiding it
       row-wide blanked that too -- the exact field this was meant to leave alone -- so the selector has to
       name where the tagline actually lives, which is beside the title. */
    .rp-print-doc .challenge-head .sec-title-cell .sec-sub { display: none; }


    /* The support table is the tightest fit on the page: five columns in ~328px. Let its headers wrap to
       two lines rather than force the table wider than its column. */
    .rp-print-doc .support-sec table { table-layout: fixed; width: 100%; }
    .rp-print-doc .support-sec th { white-space: normal; word-break: normal; hyphens: auto; }
    /* table-layout:fixed with no widths gives FIVE EQUAL columns — ~65px each in the ~328px right column.
       That is why "Passages" and "Questions" ran long, and it squeezed "Listen in English" in the label
       column at the same time. The label column takes 30% and the four data columns share the rest; the
       header drops to .68rem, which fits the longest of them ("Questions", ~47px) on one line.
       Kept as SIZING rather than cryptic abbreviations: "Psgs" and "Ques" would save the same width and
       cost the reader more than the two lines they replace. */
    .rp-print-doc .support-table th:first-child,
    .rp-print-doc .support-table td:first-child { width: 30%; }
    .rp-print-doc .support-table th:not(:first-child),
    .rp-print-doc .support-table td:not(:first-child) { width: 17.5%; }
    /* The labels break instead of shrinking. With PASS/AGES, 2ND/SOURCE and QUES/TIONS on two lines the
       widest line is six characters, which fits the ~57px data column at the header's normal .8rem — so the
       .68rem squeeze this replaced is no longer needed, and the header stays as legible as the rest. */
    .rp-print-doc .th-br { display: inline; }
    /* The support note sits 6px under the table's last rule — close enough to belong to it — while the
       gap before Correct 1st Try widens, so the two sections read as separate. */
    .rp-print-doc .support-table { margin-bottom: 0; }
    /* -1px, and the negative is not a hack: the note's line box carries ~3.4px of half-leading above its
       text (13.6px type in a 20.4px line), so a zero margin still leaves the TEXT sitting low. This puts
       the text 3px under the table's last rule, measured. The 5px it gives up is added to .support-sec's
       bottom margin below, so the block from table to "Your Journey" is the same 34px it was -- the note
       just rides at the top of that space instead of the middle. */
    /* 3px as of 2026-09-08 (Ron): -1px put the note 3px under the table's last rule, which read as
       crowded on paper. +4px. The half-leading reasoning above is unchanged and is why this is 3 and not
       4 -- the number is a margin, the gap the eye sees is ~4px more than it was. */
    .rp-print-doc .support-caveats { margin-top: 3px; }
}
