/* ══════════════════════════════════════════════════════════════════════
   The tutor stylesheet, in six parts.

   This was one 3,000-line file that every change got appended to, which
   is how it grew a second name for an action bar and a third tint for
   "needs attention". The split is mechanical: the same bytes, cut at
   section boundaries, in the same order. Nothing was rewritten.

   ORDER IS THE CASCADE. index.html links them 01 to 06 and that order is
   load-bearing — a rule in 06 beats the same-specificity rule in 02. A
   new rule goes in the file whose subject it shares, not on the end of
   the last one.

   The names describe what each file actually holds, which is not always
   tidy: the cut fell at section boundaries nearest an even sixth, so a
   file covers a stretch of the old order rather than a clean theme.

   01-shell.css     9 sections — Login, The sign-in screen, Test settings page …
   02-editor.css    13 sections — Roster / access links, Editor, Rich text editor (instructions / end message) …
   03-review.css    10 sections — Results database table, Attempt review, Download-report modal (review page) …
   04-lists.css     12 sections — Grouped action menus, The student's own link (student profile), Bulk skill tagging (question list) …
   05-queues.css    9 sections — Moving a figure inside a question, The first reading, in the review queue, The roster, grouped by what may be done to a row …
   06-grammar.css   13 sections — The set is live, Item analysis, in the design's grammar, Horizon: a cohort figure with its field attached …

   Search a section title, not a line number.
   ══════════════════════════════════════════════════════════════════════ */

* , *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: var(--text-xl); }
body { font-family: var(--font-body); background: var(--parchment); color: var(--text-primary); line-height: 1.5;
  /* FIGURES LINE UP COLUMN-WISE, everywhere, at any row count. Proportional
     digits make a column of marks ragged — 1 is narrow, 0 is wide — so every
     table on this platform was asking for tabular figures a cell at a time,
     37 times, and a cell that forgot stayed ragged. Said once. */
  font-variant-numeric: tabular-nums;
}
button { cursor: pointer; font-family: inherit; }
input, textarea, select { font-family: inherit; }
.hidden { display: none !important; }

h1, h2, h3 { font-family: var(--font-display); font-weight: 600; }

/* ── Login ─────────────────────────────────────────────────────────────── */
/* Every in-app modal (New/Edit test, Add students, Marking, formula picker,
   confirm/prompt dialogs, etc.) reuses this class — inset from the left by
   the sidebar's own width so none of them black it out while doing routine
   authoring work. The one modal that's genuinely a gate BEFORE the app is
   usable (#login-overlay itself, plus #auth-loading) looks identical either
   way, since #app (sidebar included) is hidden then and the background
   color is the same on both sides of that gutter — no exception needed.
   Collapses to full-bleed at the sidebar's own 700px mobile breakpoint,
   where there's no persistent column left to preserve anyway. */
/* Anchored near the top, NOT vertically centred.
   Centring re-centres on every content change, so a dialog whose body grows
   or shrinks — switching a tab, revealing a field, an error message
   appearing — slides its own title and buttons up or down under the cursor.
   You aim at Download, the list shortens, and Download has moved. Anchoring
   means growth only ever pushes the bottom edge down; everything above the
   change stays exactly where you were looking. Short dialogs still read as
   centred because the padding is generous and most are short. */
.login-overlay {
  position: fixed; top: 0; right: 0; bottom: 0; left: var(--sidebar-width);
  z-index: 100; background: var(--parchment);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 6vh 16px 4vh; overflow-y: auto;
  /* THE SCROLL STOPS AT THE EDGE OF THE DIALOG. Every one of these scrolls
     itself, and without this, reaching the end of a long one on a trackpad or
     an iPad hands the rest of the gesture to the page behind it — which then
     scrolls under a dialog that is still open. One rule, twenty-six dialogs. */
  overscroll-behavior: contain;
}
@media (max-width: 700px) { .login-overlay { left: 0; } }
.login-card {
  background: var(--warm-white); border: 1px solid var(--border-card);
  border-radius: var(--radius-lg); padding: 48px; max-width: 400px; width: 100%;
  display: flex; flex-direction: column; gap: 16px;
  /* The overlay scrolls now, so the card doesn't need its own scrollbar —
     a dialog that scrolls inside a page that scrolls is the same nested-
     scroller problem the Settings page was made a page to escape. */
  flex: 0 0 auto;
}
.login-card-wide { max-width: 720px; }

/* The button row at the foot of a modal. Every other modal writes this as the
   same inline style copied from the last one; naming it means the next modal
   has something to reach for, and means a modal that asks for `.modal-actions`
   gets it instead of silently getting nothing — which is how the Staff modal
   ended up with its buttons stacked full-width at the bottom of the page. */
.modal-actions {
  display: flex; gap: 8px; justify-content: flex-end; margin-top: 14px;
}

/* ── The sign-in screen ──────────────────────────────────────────────────
   .login-overlay is shared with every modal, and those are deliberately top
   aligned: a tall dialog centred vertically has nowhere to grow and clips its
   own heading. Sign-in is the one that fills the whole window with a short
   card, so it gets centred on its own — by ID, not by changing the shared rule.

   Centred with `margin: auto` on the card rather than `align-items: center`,
   because align-items on a scrolling flex container clips the top of anything
   taller than the viewport, which is exactly what happens on a phone in
   landscape with the keyboard up. */
#login-overlay, #auth-loading {
  left: 0;                       /* no sidebar behind it — nobody is signed in */
  padding: 24px;
}
#login-overlay .login-card, #auth-loading > * { margin: auto; }

/* A little more presence than an ordinary modal: this is the first screen
   anyone sees, and on an empty parchment field a hairline border alone reads
   as unfinished. */
#login-overlay .login-card {
  box-shadow: 0 1px 2px rgba(46, 35, 24, 0.04), 0 12px 32px rgba(46, 35, 24, 0.07);
}
.login-brand {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  margin-bottom: 4px; font-family: var(--font-display);
  font-weight: 600; font-size: 17px; color: var(--text-primary);
}
.login-brand .brand-mark { width: 26px; height: 26px; flex: 0 0 auto; }
/* The full logo on the sign-in card, at the size and spacing the portal and
   the exam door already use for it — the same value, written the same way, so
   the three screens a person meets cold cannot drift apart. */
#login-overlay .brand-logo {
  display: block; margin: 0 auto var(--space-3); width: 64px; height: auto;
}
#login-overlay .login-card h1 { text-align: center; }
#login-overlay .login-card > p { text-align: center; }
#login-overlay .login-form { margin-top: 6px; gap: 12px; }
#login-overlay .login-form input { padding: 12px 14px; font-size: var(--text-lg); }
/* The focus ring matters more here than anywhere else in the app: this is the
   one screen people arrive at by keyboard, from a password manager. */
#login-overlay .login-form input:focus {
  border-color: var(--text-secondary);
  box-shadow: 0 0 0 3px rgba(46, 35, 24, 0.08);
}
#login-overlay .login-form button { padding: 12px 14px; font-size: var(--text-lg); margin-top: 2px; }
#login-overlay .hint-text { text-align: center; }

/* New/Edit test specifically also gets a wider card + taller instructions/
   end-message editors (TestPortal comparison) — the sidebar-visibility fix
   itself is the general .login-overlay rule above, shared by every modal. */
/* Creation only now — four fields, so the old 960px (sized for thirteen
   fields and two rich-text editors) would just be a lot of empty card. */
#new-test-overlay .login-card { max-width: 560px; }

/* The per-test modal grew a rich-text editor, so it needs room. flex-shrink:0
   matters as much as the width: the card is a flex column with a max-height,
   so on a short viewport its children were being compressed below their own
   content instead of scrolling — the hint text ended up underneath the
   editor's toolbar. The card already scrolls; the children just have to stop
   volunteering to shrink. */
#new-section-overlay .login-card { max-width: 640px; }
#new-section-overlay .login-card > * { flex-shrink: 0; }
#new-section-instructions-editor { min-height: 200px; max-height: 40vh; }

/* On a phone the modal card is the whole screen minus a sliver — trade the
   desktop's roomy 48px padding for usable content width, and keep a little
   overlay inset so the card still reads as a card. */
@media (max-width: 640px) {
  .login-overlay { padding: 12px; }
  .login-card { padding: 24px 20px; max-height: 92vh; }
}
/* ── Test settings page ────────────────────────────────────────────────────
   Grouped cards on a real page: the form is wide enough for two columns, so
   the two rich-text editors get usable height instead of being squeezed into
   a scrolling dialog. Nothing here scrolls on its own — the page does. */
.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
.settings-card {
  background: var(--warm-white); border: 1px solid var(--border-card);
  border-radius: var(--radius-lg); padding: 20px 22px;
}
.settings-card-wide { grid-column: 1 / -1; }
/* The right-hand column of the settings grid: three short cards stacked
   beside the tall Basics card, so neither side is left with a hole. Its
   cards are half-width, so the exam-mode card's inner two-column grid
   becomes one column here: the long "end the paper at the limit" option
   does not fit half of a half. */
.settings-stack { display: grid; gap: 16px; align-content: start; min-width: 0; }
.settings-stack .field-grid-2 { grid-template-columns: 1fr; }
/* The status card beside the checklist: three lines and a button next to a
   list that may run to eight rows. Stretched to the row and with the button
   held at the foot, it reads as a card of the same size rather than a short
   box over a hole. Only the grid's first row does this; the stacks below
   balance by content. */
#settings-lifecycle-panel { align-self: stretch; }
#settings-lifecycle-panel > .readiness:last-child {
  height: 100%; margin-bottom: 0; display: flex; flex-direction: column; box-sizing: border-box;
}
#settings-lifecycle-panel > .readiness:last-child > .readiness-actions { margin-top: auto; padding-top: 12px; }
.settings-card h3 {
  font-size: var(--text-lg); font-weight: 800; margin-bottom: 12px;
  padding-bottom: 8px; border-bottom: 1px solid var(--border);
}
.settings-card .field-label { margin-top: 12px; }
.settings-card .field-label:first-of-type { margin-top: 0; }
.settings-card .rte-editor { min-height: 240px; max-height: 46vh; }
@media (max-width: 900px) { .settings-grid { grid-template-columns: 1fr; } }

/* Reading-text editor page: extract cards get room, and the name row hides
   entirely for a single text (no tabs for the student = no name to give). */
#view-passage .passage-tab-card { margin-top: 12px; }
#view-passage .rte-editor { min-height: 260px; max-height: 50vh; }
.extract-header .hint-text { font-weight: 400; margin: 0 0 0 4px; }

/* Placement choice on the new-test modal — two stacked radio cards. */
/* Matching-block review: one row per statement, dropdown on the right —
   deliberately the same shape the student sees, so checking the key here is
   checking the thing itself rather than a different rendering of it. */
.import-match { margin-top: 8px; }
.import-match-row {
  display: grid; grid-template-columns: 1fr 150px; gap: 10px; align-items: center;
  padding: 6px 8px; border-radius: var(--radius-sm); font-size: var(--text-md);
}
.import-match-row:nth-child(even) { background: var(--parchment); }
.import-match-row select { padding: 5px 8px; font-size: 13px; border: 1.5px solid var(--border); border-radius: var(--radius-sm); background: var(--warm-white); }
@media (max-width: 640px) { .import-match-row { grid-template-columns: 1fr; gap: 4px; } }

.placement-choice { display: grid; gap: 8px; margin-bottom: 4px; }
.placement-choice label {
  display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px;
  border: 1.5px solid var(--border); border-radius: var(--radius-sm); cursor: pointer;
  font-size: 14px; background: var(--warm-white);
}
.placement-choice label:hover { border-color: var(--border-card); }
.placement-choice input { margin-top: 3px; flex-shrink: 0; }
.placement-choice .hint-text { margin: 0; }

.field-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 640px) { .field-grid-2 { grid-template-columns: 1fr; } }
.login-card h1 { font-size: 24px; }
.login-card p { color: var(--text-secondary); font-size: 14px; }
.login-field { display: flex; gap: 8px; }
.login-field input {
  flex: 1; padding: 10px 14px; border: 1.5px solid var(--border);
  border-radius: var(--radius-md); font-size: 14px; background: var(--warm-white);
  color: var(--text-primary); outline: none;
}
.login-field input:focus { border-color: var(--brown-ink); }
.login-error { color: #C0392B; font-size: 13px; }

/* ── App shell ─────────────────────────────────────────────────────────── */
/* Persistent left sidebar (TestPortal-style) instead of a single-row top
   bar — "Tests" and "Question Bank" are the app's only two top-level
   destinations, so they get permanent, always-visible nav entries rather
   than competing for space with the breadcrumb and page actions in one
   thin strip. The top bar (below) is now page CONTEXT only: which test/
   section/question you've drilled into, not app-level navigation. */
.app { display: flex; min-height: 100vh; align-items: stretch; }

.sidebar {
  width: var(--sidebar-width); flex-shrink: 0; background: var(--warm-white); border-right: 1px solid var(--border);
  display: flex; flex-direction: column; padding: 20px 14px;
  position: sticky; top: 0; align-self: flex-start; height: 100vh; overflow-y: auto;
}
.sidebar-brand {
  display: flex; align-items: center; gap: 10px; padding: 2px 10px 20px;
  font-family: var(--font-display); font-weight: 600; font-size: var(--text-lg); color: var(--text-primary);
  background: none; border: none; width: 100%; text-align: left; cursor: pointer;
}
.sidebar-brand:hover .brand-mark { opacity: 0.7; }
/* The real logomark asset used as a MASK rather than an <img>: the source PNG
   is a flat black silhouette, so masking lets the brand ink come from CSS
   instead of being baked into the file. Keeps the exact brand shape while
   letting it match --text-primary, which is what every heading already uses —
   the raw asset's pure #000 reads cold next to warm parchment. */
.brand-mark {
  width: 26px; height: 26px; flex-shrink: 0;
  background-color: var(--text-primary);
  -webkit-mask: url('/assets/BingsAcademy_Logomark.png') center / contain no-repeat;
  mask: url('/assets/BingsAcademy_Logomark.png') center / contain no-repeat;
}
.sidebar-nav { display: flex; flex-direction: column; gap: 2px; flex: 1; }
/* Sits apart from the daily screens above it, because it is not one: the
   import log is for whoever is improving the importer, not for a tutor
   building a paper. A rule rather than a heading — one item does not need a
   section, and the gap alone reads as "and separately". */
.sidebar-link.is-maintenance {
  margin-top: 10px; padding-top: 12px;
  border-top: 1px solid var(--border);
}
.dark-sidebar .sidebar-link.is-maintenance { border-top-color: rgba(250, 246, 238, 0.14); }

.sidebar-footer { border-top: 1px solid var(--border); padding-top: 10px; margin-top: 10px; }
.sidebar-link {
  display: flex; align-items: center; gap: 11px; width: 100%; text-align: left;
  background: none; border: none; border-radius: var(--radius-sm); padding: 10px 12px;
  font-size: 14px; font-weight: 600; color: var(--text-secondary);
}
.sidebar-link svg { width: 18px; height: 18px; flex-shrink: 0; color: var(--text-muted); }
.sidebar-link:hover { background: var(--parchment); color: var(--text-primary); }
.sidebar-link:hover svg { color: var(--brown-ink); }
.sidebar-link.active { background: var(--sand); color: var(--deep-ink); }
.sidebar-link.active svg { color: var(--brown-ink); }

/* ── Dark sidebar variant ──────────────────────────────────────────────────
   The YouTube banner's exact treatment — cream on #2E2318 — applied ONLY to
   the sidebar. It's chrome: no student-facing content is rendered in it and
   nobody reads it for more than a second, so the readability argument against
   a dark content area doesn't apply here. The content column stays parchment
   on purpose: it renders stems, passages and the rich-text editors as the
   student will see them, and authoring white-background content inside a dark
   frame means judging it in the wrong context. */
.dark-sidebar .sidebar {
  background: var(--text-primary);
  border-right-color: rgba(250, 246, 238, 0.14);
}
.dark-sidebar .sidebar-brand { color: var(--parchment); }
.dark-sidebar .brand-mark { background-color: var(--parchment); }
.dark-sidebar .sidebar-link { color: rgba(250, 246, 238, 0.72); }
.dark-sidebar .sidebar-link svg { color: rgba(250, 246, 238, 0.5); }
.dark-sidebar .sidebar-link:hover {
  background: rgba(250, 246, 238, 0.08); color: var(--parchment);
}
.dark-sidebar .sidebar-link:hover svg { color: var(--parchment); }
/* Active uses the warm sand at low opacity rather than a flat grey, so the
   selected item still reads as the brand's tan and not as "greyed out". */
.dark-sidebar .sidebar-link.active {
  background: rgba(237, 224, 200, 0.16); color: var(--parchment);
}
.dark-sidebar .sidebar-link.active svg { color: var(--sand); }
.dark-sidebar .sidebar-footer { border-top-color: rgba(250, 246, 238, 0.14); }
/* "Signed in as John" had NO dark override at all — it kept --text-muted and
   --text-secondary, which are brown inks meant for parchment. On the rail they
   measured 3.70:1 and, for the name itself, 2.31:1: the worst contrast in the
   sidebar, and it was the one line naming the person using it. These live here
   rather than beside .sidebar-whoami in 02-editor.css because that file loads
   later; the extra class wins on specificity from either file, and every other
   dark rule is here. */
.dark-sidebar .sidebar-whoami { color: rgba(250, 246, 238, 0.62); }
.dark-sidebar .sidebar-whoami strong { color: rgba(250, 246, 238, 0.86); }

.sidebar-theme-toggle {
  background: none; border: none; width: 100%; text-align: left; cursor: pointer;
  padding: 6px 12px; font-size: 11.5px; font-weight: 600; color: var(--text-muted);
}
.sidebar-theme-toggle:hover { color: var(--text-primary); }
/* 0.62, not 0.45. At 0.45 this measured 4.05:1 against the rail — under the
   4.5:1 that 11.5px text needs, and visibly faint in the footer next to
   "Signed in as". Darkening the rail does not fix it: at #241B12 the same
   text is 4.21:1 and at #1F1710 it is 4.24:1, both still short. The alpha is
   what was wrong, and 0.62 puts it at 6.35:1 while staying quieter than the
   nav links above it at 0.72. */
.dark-sidebar .sidebar-theme-toggle { color: rgba(250, 246, 238, 0.62); }
.dark-sidebar .sidebar-theme-toggle:hover { color: var(--parchment); }

.app-content { flex: 1; min-width: 0; }

.top-nav { background: var(--warm-white); border-bottom: 1px solid var(--border); }
.nav-inner {
  max-width: 1280px; margin: 0 auto; padding: 16px 24px;
  display: flex; align-items: center; gap: 16px;
}
.breadcrumb { flex: 1; color: var(--text-secondary); font-size: 14px; min-height: 20px; }
.breadcrumb button { background: none; border: none; padding: 0; font: inherit; color: var(--brown-ink); font-weight: 600; text-decoration: none; }
.breadcrumb button:hover { text-decoration: underline; }

/* 1280 rather than 1100: the question editor (form + live preview side by
   side) is the page that actually earns the width — writing space is the
   whole job there. List views just breathe a little more. */
.main { max-width: 1280px; margin: 0 auto; padding: 32px 24px; }
/* The editor earns more than the list views do — it is two panes side by side,
   and a paper full of diagrams needs room in both. Scoped to the editor rather
   than widening every screen, because a list of tests at 1600px is just a long
   walk for the eye. */
/* `.on-editor` is set by revealView, which is the single place a view is
   revealed and therefore already knows. Was body:has(#view-editor:not(.hidden)),
   which needs a 2023 browser and silently left the editor at list width on an
   older one — the screen that earns the width most, getting it least. */
body.on-editor .main { max-width: 1600px; }
.view-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; flex-wrap: wrap; gap: 10px 16px; }

/* A fixed 216px rail has no room on a phone, so the nav becomes a horizontal
   top bar rather than a drawer — nine destinations are worth showing, not
   worth hiding behind a hamburger.

   It WRAPS to a second row. It has to be said explicitly: the nav carries
   flex-wrap already, but wrapping only happens when something makes the
   container narrower than its contents, and `flex: 0 0 auto` did the
   opposite — it pinned the nav at its max-content width (955px measured,
   inside a 581px bar). The wrap never engaged, the sidebar's overflow-x took
   over instead, and the last four entries — Results, Marking rubrics,
   Students, Staff — sat off the edge behind a thin scrollbar. Half the
   application, reachable only by a sideways drag nobody thinks to try.

   `flex: 1 1 auto` with `min-width: 0` lets the nav be squeezed, which is
   what turns its own flex-wrap back on. min-width matters on its own: a flex
   item's floor is min-content, not zero, so without it the nav still refuses
   to go narrow enough to wrap. */
/* ── A screen that needs a window ──────────────────────────────────────────
   Shown INSTEAD of a desktop-only screen on a phone. Deliberately calm: the
   attention tint, not warn or bad — nothing is broken and nobody did anything
   wrong. It carries a way out, because a notice that only explains itself is
   still a dead end. */
.too-narrow {
  margin: 24px auto; max-width: 460px; padding: 24px 22px;
  background: var(--state-attention-bg); border: 1px solid var(--border-card);
  border-radius: var(--radius-lg);
  display: flex; flex-direction: column; gap: 12px; align-items: flex-start;
}
.too-narrow h2 { font-size: var(--text-xl); margin: 0; }
.too-narrow p { margin: 0; font-size: var(--text-md); line-height: 1.6; }
.too-narrow-note { color: var(--text-secondary); font-size: var(--text-sm); }
.too-narrow #too-narrow-back {
  margin-top: 4px; min-height: 44px; padding: 10px 18px;
  background: var(--warm-white); border: 1px solid var(--border-card);
  border-radius: var(--radius-sm); font-weight: 600; color: var(--deep-ink);
}

/* DON'T ADVERTISE WHAT YOU WILL REFUSE. Four of the nine nav items lead only
   to "needs a bigger window" on a phone, and a menu whose entries are dead
   ends teaches somebody to distrust the menu. Hiding them also gives the phone
   header back a row: nine items wrapped to three lines and took half the
   screen before the content started.

   Hidden by SIZE and pointer, never removed — the same window widened brings
   them straight back, because they are only ever unavailable, never gone. */
@media (max-width: 700px) and (pointer: coarse) {
  .sidebar-link.needs-a-window { display: none; }
  /* An empty breadcrumb bar is 45px of nothing between the header and the
     content, which on a phone is most of what is above the fold.
     `.is-empty` is set by renderBreadcrumb, which already knows whether there
     is a trail — a plain class rather than :has(), so this works on a browser
     older than 2023 and cannot disagree with what was actually rendered. */
  .top-nav.is-empty { display: none; }
}

@media (max-width: 700px) {
  .app { flex-direction: column; }
  .sidebar {
    position: static; height: auto; width: 100%;
    flex-direction: row; flex-wrap: wrap; align-items: center;
    padding: 10px 14px; gap: 6px 10px; border-right: none; border-bottom: 1px solid var(--border);
  }
  /* Two bands, not one ragged pile. The DOM order is brand, nav, footer, which
     read straight out would put the nav between the wordmark and the account —
     so `order` pairs the brand with the account on a header line, and the nav
     claims a full row of its own beneath it (flex-basis 100% is what forces
     that line break). Without this the nav wrapped into whatever gap was left
     after the wordmark and the rows came out ragged and un-alignable. */
  .sidebar-brand { order: 1; padding: 0; margin: 0; border-right: none; width: auto; }
  .sidebar-footer {
    order: 2; margin: 0 0 0 auto; border-top: none; padding: 0;
    display: flex; flex-direction: row; align-items: center; flex-wrap: wrap; gap: 2px 12px;
  }
  .sidebar-nav {
    order: 3; flex: 1 0 100%; min-width: 0;
    flex-direction: row; flex-wrap: wrap; gap: 6px;
  }
  /* The divider earns its place between the wordmark and the account, where
     there are two things to separate. In the rail it hangs off the end of the
     brand as a stray vertical line with nothing on the other side of it. */
  /* `.sidebar` in front of it, and NOT decoration. Written as a bare
     `.sidebar-whoami { margin: 0 }` this lost to `.sidebar-whoami { margin: 0
     0 8px }` in 02-editor.css: identical specificity, and that file loads
     later — a media query adds none of its own. So the reset never applied.
     The 8px stayed, and in a row that centres its items it is the MARGIN box
     that gets centred, which sat "Signed in as John" four pixels above the two
     controls either side of it for as long as this layout has existed.
     Same trap, same fix as the .dark-sidebar rules above. */
  .sidebar .sidebar-whoami { margin: 0; }
  /* Full-width in the rail, where it is a stacked block. Left at 100% here it
     is a 304px button holding a 90px label, which is single-handedly what
     pushed the account line in two. */
  .sidebar-theme-toggle { width: auto; }
  .sidebar-link { width: auto; padding: 7px 10px; font-size: 13px; }
  /* No divider here. In the rail it separates a section from the one above it;
     in a wrapped row it is a line over one chip in the middle of a line of
     them, which reads as damage rather than as a grouping. Later in this same
     file than the rule it overrides, and the same specificity — which is the
     way round that works. */
  .sidebar-link.is-maintenance { margin-top: 0; padding-top: 7px; border-top: none; }
  .nav-inner { padding: 12px 16px; }
  .main { padding: 20px 16px; }
}

/* ── A failure, on the screen it happened on ───────────────────────────────
   Uses the platform's own bad-state tokens rather than a red of its own: this
   is the same "something is wrong here" the rest of the app already speaks,
   and a second red would read as a different severity.

   Deliberately NOT the full-bleed alarm bar. The band is inside the view, it
   keeps the page's margins, and it carries a Dismiss rather than an X — the
   reader is being told something, not asked to clear an obstruction. Nothing
   behind it is disabled, because the whole point is that they can carry on
   looking at the thing that failed while they read why. */
.failure-bar {
  display: flex; align-items: flex-start; gap: 14px;
  margin: 0 0 18px; padding: 13px 16px;
  background: var(--state-bad-bg); border: 1px solid var(--state-bad-border);
  border-radius: var(--radius-md);
  color: var(--state-bad-text); font-size: var(--text-md); line-height: 1.55;
}
.failure-text { flex: 1; min-width: 0; }
.failure-close {
  flex: none; background: none; border: 1px solid var(--state-bad-border);
  border-radius: var(--radius-sm); padding: 4px 10px;
  font: inherit; font-size: var(--text-sm); color: var(--state-bad-text); cursor: pointer;
}
.failure-close:hover { background: var(--warm-white); }

/* The same band, carrying news instead of a fault. It shares every bone with
   the failure — position, spacing, the Dismiss rather than an X — because it
   is the same object doing the same job, and only the colour should have to
   say which of the two you are reading. */
.done-bar {
  background: var(--correct-bg); border-color: var(--correct-border);
  color: var(--correct-text);
}
.done-bar .failure-close { border-color: var(--correct-border); color: var(--correct-text); }

/* A hint that has noticed something, sitting under the hint that explains the
   control. Warn rather than bad: nothing is broken, a person has a decision to
   make. It stays a hint — same size, same place in the flow — because a band
   or a dialog for "these two disagree" would outrank the setting it is about. */
.hint-text.warn-note {
  margin-top: 8px; padding: 9px 12px;
  background: var(--state-warn-bg); border: 1px solid var(--state-warn-border);
  border-radius: var(--radius-sm); color: var(--state-warn-text);
}

/* ── Buttons ───────────────────────────────────────────────────────────── */
.btn-primary {
  background: var(--deep-ink); color: var(--parchment); border: none;
  border-radius: var(--radius-pill); padding: 10px 20px; font-weight: 600; font-size: 14px;
}
/* The same treatment .btn-ghost and .btn-icon already had, and the primary
   button did not. Activate is disabled until the readiness list passes, and it
   was rendering byte-identical to a live primary — same ink, same parchment,
   same pointer cursor — so it read as a working button, invited the click and
   did nothing. Whoever is uploading a paper deserves to be told by the control
   itself, not only by the list above it. */
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }

.btn-ghost {
  background: transparent; color: var(--brown-ink); border: 1.5px solid var(--border-card);
  border-radius: var(--radius-pill); padding: 9px 18px; font-weight: 600; font-size: 14px;
}
.btn-ghost:disabled { opacity: 0.4; cursor: not-allowed; }

/* ── Test hub tabs ─────────────────────────────────────────────────────── */
.hub-tabs {
  display: flex; gap: 2px; border-bottom: 1.5px solid var(--border-card);
  margin: 4px 0 18px; overflow-x: auto;
  /* The active tab's underline hangs 1.5px below the strip (negative margin
     below), which otherwise makes overflow spawn a stray scrollbar track —
     hide the track; the strip stays swipe-scrollable on narrow screens. */
  scrollbar-width: none;
}
.hub-tabs::-webkit-scrollbar { display: none; }
.hub-tab {
  background: none; border: none; padding: 10px 16px; font-weight: 600; font-size: 14px;
  color: var(--text-secondary); border-bottom: 2.5px solid transparent;
  margin-bottom: -1.5px; white-space: nowrap; cursor: pointer;
}
.hub-tab:hover { color: var(--brown-ink); }
.hub-tab.active { color: var(--deep-ink); border-bottom-color: var(--deep-ink); }
/* A JOB COUNT on a tab, which is not the same object as the neutral
   .chip-count on the marking-stage strip below it. That one measures a queue
   you already chose to look at; this one is asking to be looked at, so it
   carries the warn tint the rest of the app uses for "a decision waiting on a
   person". Figures are tabular so the pill does not resize as it counts down. */
.hub-tab-job {
  display: inline-block; margin-left: 7px; padding: 1px 7px; border-radius: 999px;
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 700;
  font-variant-numeric: tabular-nums; vertical-align: 1px;
  background: var(--state-warn-bg); border: 1px solid var(--state-warn-border);
  color: var(--state-warn-text);
}
.hub-panel-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 16px; margin-bottom: 14px; flex-wrap: wrap;
}
.hub-panel-head .hint-text { margin: 0; flex: 1 1 260px; }
.hub-panel-head .btn-primary { flex-shrink: 0; }

/* ── Card lists ────────────────────────────────────────────────────────── */
.card-list { display: flex; flex-direction: column; gap: 10px; }
.empty-state {
  border: 1.5px dashed var(--border-card); border-radius: var(--radius-md);
  padding: 28px 20px; text-align: center; color: var(--text-secondary); font-size: 14px;
  background: var(--warm-white);
}
.empty-state strong { display: block; color: var(--text-primary); font-size: var(--text-lg); margin-bottom: 4px; }

/* Loading skeletons — sized to match .card-row so a modal doesn't grow and
   re-centre itself the moment its list arrives. */
.skeleton-list { display: flex; flex-direction: column; gap: 10px; }
.skeleton-row {
  height: 58px; border-radius: var(--radius-md);
  border: 1px solid var(--border-card); background: var(--warm-white);
  position: relative; overflow: hidden;
}
.skeleton-row::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, var(--sand), transparent);
  transform: translateX(-100%);
  animation: skeleton-sweep 1.1s ease-in-out infinite;
}
@keyframes skeleton-sweep { to { transform: translateX(100%); } }
/* A sweeping gradient is decoration, not information — the reserved height is
   what actually does the work here, so dropping the motion loses nothing. */
@media (prefers-reduced-motion: reduce) {
  .skeleton-row::after { animation: none; }
}

/* ── Live now ────────────────────────────────────────────────────────────── */
.live-updated { font-size: 13px; color: var(--text-secondary); }
.live-row { align-items: flex-start; }
.live-progress { display: flex; align-items: center; gap: 10px; margin-top: 6px; }
.live-bar {
  display: block; width: 140px; height: 8px; flex-shrink: 0;
  border-radius: 4px; background: var(--sand); overflow: hidden;
}
.live-bar > span { display: block; height: 100%; background: var(--correct-border); transition: width 0.4s ease; }
.live-progress-text { font-size: 12px; color: var(--text-secondary); }
.live-right {
  display: flex; flex-direction: column; align-items: flex-end; gap: 3px;
  text-align: right; flex-shrink: 0;
}
/* Tabular figures, or a counting-down clock jitters as digit widths change. */
.live-clock { font-size: var(--text-lg); font-weight: 700; }
.live-clock-soon { color: var(--state-warn-text); }
.live-clock-out { color: #A5321F; }
.live-save { font-size: 12px; color: var(--text-muted); }
.live-breach { margin-top: 6px; font-size: 12px; color: var(--state-warn-text); }
/* Only when they've hit the limit a blocking test will act on — otherwise
   this is information, not an alarm. */
.live-breach-hot { color: #A5321F; font-weight: 700; }
.live-chip {
  display: inline-block; padding: 1px 8px; border-radius: 10px;
  font-size: var(--text-xs); font-weight: 700; vertical-align: middle;
}
.live-chip-preview { background: #E4EAF5; color: #2F4A7A; }
.sidebar-count {
  margin-left: auto; min-width: 20px; padding: 0 6px; border-radius: 10px;
  background: #C0392B; color: #fff; font-size: var(--text-xs); font-weight: 700;
  text-align: center; line-height: 18px;
}
@media (max-width: 520px) {
  .live-right { align-items: flex-start; text-align: left; margin-top: 8px; }
}
.card-row {
  background: var(--warm-white); border: 1px solid var(--border-card); border-radius: var(--radius-md);
  padding: 16px 20px; display: flex; align-items: center; justify-content: space-between; cursor: pointer;
  gap: 12px; flex-wrap: wrap;
  box-shadow: 0 1px 2px rgba(46, 35, 24, 0.05);
  transition: box-shadow 0.15s ease, border-color 0.15s ease;
}
.card-row:hover { border-color: var(--warm-tan); box-shadow: 0 3px 10px rgba(46, 35, 24, 0.09); }
.card-row-title { font-weight: 700; }
/* Floating "make this a fraction?" prompt — body-level and absolutely
   positioned so it can't be clipped by the editor's own overflow. */
.fraction-suggest {
  position: absolute; z-index: 200; display: flex; align-items: center; gap: 6px;
  background: var(--warm-white); border: 1px solid var(--border-card);
  border-radius: var(--radius-sm); padding: 6px 10px; font-size: 13px;
  box-shadow: 0 4px 14px rgba(46, 35, 24, 0.15);
}
.fraction-suggest .katex { font-size: 1em; }

/* Plain block with INLINE children, not flex: as a flex row the stem was a
   single unbreakable item, so any stem long enough to wrap pushed itself
   entirely onto a second line and left "Q1." stranded alone above it. As
   inline content the text simply flows after the number the way it reads. */
.stem-title-row { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 6px; }
.stem-title-row > strong { flex: 0 0 auto; }
/* Clamped, because a gap-fill's stem is a whole article: unclamped, one row
   was taller than the six rows around it put together and the list stopped
   being a list. Two lines is enough to tell questions apart; the row is a
   way IN to the question, not the question. */
.stem-title-content {
  flex: 1 1 240px; min-width: 0; font-weight: 400;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* Paragraphs are collapsed to one line so a stem fits a list row — but
   collapsing them with no separator ran the last word of one paragraph into
   the first of the next ("…open before.You have 20 minutes."). A trailing
   space after each, invisible when there's only one. */
.stem-title-content :is(p, div) { margin: 0; display: inline; }
.stem-title-content :is(p, div) + :is(p, div)::before { content: " "; }
.stem-title-content .katex { font-size: 1em; }
/* A diagram inserted into a stem is full-width artwork; left alone it renders
   at its natural size and turns one list row into a 300px slab while every
   other row is ~60px, wrecking the scannable list. Cap it to a thumbnail —
   big enough to tell two diagrams apart, small enough to keep rows even.
   (Maths is deliberately NOT downgraded here: fractions must stay rendered.) */
.stem-title-content img {
  max-height: 34px; width: auto; vertical-align: middle;
  margin: 0 4px; border: 1px solid var(--border); border-radius: 3px;
}
.stem-title-row .category-badge { margin-left: 6px; vertical-align: middle; }
.card-row-meta { color: var(--text-secondary); font-size: 13px; margin-top: 4px; }
.card-row-actions { display: flex; gap: 6px; flex-shrink: 0; flex-wrap: wrap; }
/* The design's name for the same thing, on the screens that have been ported
   to its grammar. Kept beside card-row-actions rather than aliased, because
   the gap differs and one of them will change without the other. */
.band-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-left: auto; }
.btn-icon {
  background: none; border: 1px solid var(--border-card); border-radius: var(--radius-sm);
  padding: 7px 14px; font-size: 13px; font-weight: 600; color: var(--brown-ink);
  transition: background 0.12s ease, border-color 0.12s ease;
}
.btn-icon:hover { background: var(--parchment); border-color: var(--warm-tan); }
.btn-icon-danger { color: #C0392B; border-color: #E8C4BC; }
.btn-icon-danger:hover { background: #FBF0EE; border-color: #C0392B; }
.btn-icon-primary { background: var(--brown-ink); border-color: var(--brown-ink); color: var(--warm-white); }
.btn-icon-primary:hover { background: var(--deep-ink); border-color: var(--deep-ink); }
.btn-icon:disabled { opacity: 0.4; cursor: not-allowed; }
.card-row.no-nav { cursor: default; }
/* Reorder arrows: borderless ghosts — functional, but they shouldn't compete
   with the row's real actions for visual weight. */
.reorder-btns { display: flex; flex-direction: column; gap: 2px; flex-shrink: 0; }
.reorder-btns .btn-icon {
  padding: 3px 8px; font-size: 10px; line-height: 1;
  border-color: transparent; color: var(--text-muted);
}
.reorder-btns .btn-icon:hover { color: var(--deep-ink); background: var(--sand); border-color: transparent; }

/* Below this width the row's left content (name/meta) and right content
   (actions/score) each get the full row width instead of squeezing side by
   side — the squeeze is what let action buttons and score text overflow
   off-screen on a phone (found during the Results-overview mobile check). */
@media (max-width: 520px) {
  .card-row { flex-direction: column; align-items: stretch; }
  .card-row-actions { justify-content: flex-start; }
}

/* ── The Generate tab ──────────────────────────────────────────────────────
   Deliberately plain: existing tokens, no new visual language. The job here
   is that every number the terminal prints has somewhere to sit — a chip, a
   table row, a sentence — because a refusal or a measurement summarised away
   is the one thing this screen must not do. */
.gen-controls, .gen-write, .gen-strip {
  display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end;
  padding: 14px 0; border-bottom: 1px solid var(--border);
}
.gen-field { display: flex; flex-direction: column; gap: 4px; }
.gen-field label { font-size: var(--text-sm); color: var(--text-tertiary); }
.gen-field input { width: 130px; }
.gen-field input[type="number"] { width: 92px; }

/* The 130px above is for text and selects; a checkbox inherits it and
   becomes a 130px-wide box with the label stranded beside it. */
.gen-check {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--text-sm); max-width: 22ch;
}
.gen-check input[type="checkbox"] { width: auto; margin: 0; }

.gen-out, .gen-wrote { padding: 16px 0; }
.gen-stem { font-size: var(--text-lg); max-width: 60ch; }
.gen-fig { display: block; max-width: 420px; margin: 12px 0; }

.gen-opts { display: flex; flex-wrap: wrap; gap: 14px; margin: 12px 0; }

/* THE BATCH SOMEBODY IS DECIDING ABOUT. Plain on purpose — it is here
   to be usable, and the look is John's in the morning. */
.gen-batch { padding: 8px 0 16px; }
.gen-batchbar { padding: 8px 0; display: flex; flex-wrap: wrap;
                align-items: flex-end; gap: 14px; }
.gen-batchbar .hint-text { flex: 1 1 100%; margin: 0; }
.gen-kept { font-weight: 600; padding: 6px 0 12px; }
.gen-grid { display: grid; gap: 14px;
            grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
.gen-card { border: 1px solid #d8d2c6; border-radius: 6px; padding: 10px; }
.gen-card-head { display: flex; align-items: center; gap: 8px;
                 cursor: pointer; }
.gen-card-n { font-weight: 600; }
.gen-card-meta { color: #6d6659; font-size: 0.85em; }
.gen-card-fig { display: block; max-width: 100%; margin: 8px 0; }
.gen-card-stem { font-size: 0.85em; margin: 6px 0 0; }
.gen-card-bad { font-size: 0.85em; color: #9a3b28; margin: 6px 0 0; }
.gen-card-opts { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 0; }
.gen-card-opt { flex: 0 0 auto; text-align: center; padding: 3px;
                border: 1px solid transparent; border-radius: 4px; }
.gen-card-opt.is-key { border-color: #3f7d54; background: #eef5f0; }
.gen-card-opt img { display: block; max-width: 62px; height: auto; }
.gen-card-lab { font-size: 0.7em; color: #6d6659; }
.gen-card-otext { font-size: 0.8em; display: block; }
/* A question nobody is keeping should look like one. The class is put
   on by the code that knows, not asked for with :has() — see the rule
   in test_device_tiers about selectors that need a 2023 browser. */
.gen-card.is-culled { opacity: 0.45; }
.gen-opt {
  border: 1px solid var(--border); border-radius: 8px; padding: 10px;
  width: 210px;
}
.gen-opt.is-key { border-color: var(--correct-text); }
.gen-opt img { display: block; max-width: 100%; }
.gen-opt-head { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
.gen-why { font-size: var(--text-sm); color: var(--text-tertiary); }
.gen-key { font-size: var(--text-sm); color: var(--correct-text); }

/* The distance chips carry the audit's own words. Colour says "near" and
   "far" at a glance; the number beside it is what an author acts on. */
.gen-chip {
  font-size: 11px; border-radius: 10px; padding: 2px 8px;
  border: 1px solid currentColor; opacity: 0.8;
}
.gen-chip-near { color: #B26A00; }
.gen-chip-middling { color: #8A7A68; }
.gen-chip-far { color: #6B7A8A; }
.gen-chip-answer { display: none; }

.gen-evidence { margin: 10px 0; max-width: 70ch; }
.gen-decoy { font-style: italic; color: var(--text-secondary); }
.gen-gates td, .gen-coverage td { padding: 2px 12px 2px 0; font-size: var(--text-sm); }
.gen-gates td:first-child, .gen-coverage td:first-child { color: var(--text-tertiary); }

/* A refusal is a result. It gets a box, not a toast that disappears. */
.gen-refused {
  border-left: 3px solid #B26A00; padding: 8px 12px; margin: 12px 0;
  background: rgba(178, 106, 0, 0.05); max-width: 70ch;
}
.gen-refused h4 { margin: 0 0 6px; font-size: var(--text-md); }
.gen-warn { color: #B26A00; max-width: 70ch; }
.gen-done { color: var(--correct-text); }

.gen-shapes { padding: 16px 0; border-top: 1px solid var(--border); }
.gen-trace { display: block; max-width: 100%; margin: 10px 0; }
.gen-cert table td { padding: 2px 12px 2px 0; font-size: var(--text-sm); }
