:root {
  color-scheme: light dark;
  --bg: #fbfaf8;
  --fg: #1d1c1a;
  --muted: #5f5c57;
  --line: #e0ddd6;
  --accent: #1f4f8b;
  --accent-fg: #ffffff;
  --warn-bg: #fdf3d8;
  --radius: 8px;
  --gap: 1rem;

  /* Chunk: docs/chunks/studentux_action_kit — the kit's tokens.

     --dim is the one that carries intent. A future or locked thing recedes by
     COLOR, never by container opacity: opacity fades the text, the border and
     the focus ring together, which takes a still-reachable control out of the
     eye at the same moment it takes it out of legibility. #6e6a64 on #fbfaf8
     clears AA for body text and still reads as "not yours yet".

     The rest are the approved design's palette additions
     (docs/narratives/studentux_alignment/OVERVIEW.md § The flairs). They are
     flair: a later chunk may change any hue here without ceremony. */
  --dim: #6e6a64;
  --good: #166534;
  --good-bg: #e8f3ea;
  --warn-ink: #7a5b12;
  --wash: #eef2f8;
  --wash-line: #d5deed;
  --radius-pill: 16px;
  --radius-card: 12px;

  /* Chunk: docs/chunks/scannable_kit — the red "needs a fix" tone. */
  --bad: #9b1c1c;
  --bad-bg: #fbe9e7;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #17181a;
    --fg: #eceae6;
    --muted: #a5a29c;
    --line: #33353a;
    --accent: #8ab4f8;
    --accent-fg: #10131a;
    --warn-bg: #3a3520;

    --dim: #a5a29c;
    --good: #7ee2a8;
    --good-bg: #14301f;
    --warn-ink: #f0d999;
    --wash: #1b2430;
    --wash-line: #2b3646;

    --bad: #f4a3a3;
    --bad-bg: #3b1d1d;
  }
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 1rem/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  overflow-wrap: anywhere;
}

.shell { display: flex; flex-direction: column; min-height: 100vh; }

.shell__header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap);
  padding: 0.75rem var(--gap);
  border-bottom: 1px solid var(--line);
}

.shell__brand { font-weight: 600; text-decoration: none; color: inherit; }

.shell__nav { display: flex; flex-wrap: wrap; gap: 0.75rem; flex: 1 1 auto; }

.shell__nav a {
  color: var(--fg);
  text-decoration: none;
  padding: 0.6rem 0.25rem;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

.shell__nav a[aria-current="page"] { text-decoration: underline; text-underline-offset: 4px; }

.shell__session { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }

.shell__email { color: var(--muted); font-size: 0.875rem; }

.shell__main { flex: 1 1 auto; width: 100%; padding: var(--gap); }

.shell__main > * { max-width: 42rem; }

.shell--instructor .shell__main > * { max-width: 70rem; }

.shell__footer {
  padding: var(--gap);
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.875rem;
}

h1 { font-size: 1.5rem; line-height: 1.25; margin: 0 0 0.5rem; }
h2 { font-size: 1.15rem; margin: 1.5rem 0 0.5rem; }
p { margin: 0 0 1rem; }

a { color: var(--accent); }

.card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap);
  margin: 0 0 var(--gap);
  background: transparent;
}

.notice { background: var(--warn-bg); border-radius: var(--radius); padding: var(--gap); }

.muted { color: var(--muted); }

.stack > * + * { margin-top: 0.75rem; }

form { margin: 0; }

label { display: block; font-weight: 600; margin-bottom: 0.25rem; }

input[type="email"], input[type="text"], textarea {
  display: block;
  width: 100%;
  min-height: 44px;
  padding: 0.6rem;
  font-size: 1rem;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

button {
  min-height: 44px;
  padding: 0.6rem 1rem;
  font: inherit;
  font-size: 1rem;
  color: var(--accent-fg);
  background: var(--accent);
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
}

button.link {
  color: var(--accent);
  background: transparent;
  border-color: var(--line);
  min-height: 44px;
}

code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9375rem; }

pre { overflow-x: auto; padding: 0.75rem; border: 1px solid var(--line); border-radius: var(--radius); }

table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 0.5rem; border-bottom: 1px solid var(--line); }

/* --- kit: the student's next action (docs/chunks/studentux_action_kit) ----

   THE ONE STATE LANGUAGE AND THE ONE ACTION, as CSS. Everything between this
   marker and "end kit" belongs to src/ui/student/kit/, and test/kit.test.ts
   reads the block by these two markers to enforce two rules over the whole of
   it rather than over a selector somebody remembered:

     1. NO "opacity" ANYWHERE IN HERE. A future or locked thing dims by color
        (var(--dim)). Container opacity fades the border and the focus ring
        along with the text, so a control that is still reachable stops looking
        reachable — and it compounds with whatever the parent already faded.
        The rule is over the block, not over .chip--future, because the next
        person to add a "quieter" variant is the one the rule is for.

     2. EVERY INTERACTIVE ELEMENT THE KIT RENDERS IS AT LEAST 44px TALL. The
        test renders the components, reads the classes off the anchors and
        buttons in the output, and looks each one up here — so a new kit
        control with no rule fails by having nothing to find.

   Radii, paddings and hues below are flair
   (docs/narratives/studentux_alignment/OVERVIEW.md § The flairs): change them
   freely. The two rules above are not. */

/* The chip. Five tones and no sixth — done, now, future, not-yet, pending —
   because the moment a surface invents its own sixth word for a state, the
   student is reading two vocabularies and has to learn which page uses which.
   Not interactive: it is a label, so no min-height applies. */
.chip {
  display: inline-flex;
  align-items: center;
  padding: 0.15rem 0.6rem;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  font-size: 0.8125rem;
  font-weight: 600;
  white-space: nowrap;
}

.chip--done { color: var(--good); background: var(--good-bg); border-color: var(--good-bg); }

/* "Now" is the only tone that draws a ring: the accent border IS the marker of
   the current thing, on the session map and on a criterion alike. */
.chip--now { color: var(--accent); border-color: var(--accent); background: transparent; }

/* Dimmed by color. See rule 1 above. */
.chip--future { color: var(--dim); border-color: var(--line); background: transparent; }

.chip--not-yet { color: var(--warn-ink); background: var(--warn-bg); border-color: var(--warn-bg); }

.chip--pending { color: var(--muted); background: var(--wash); border-color: var(--wash-line); }

/* The primary action. One per screen, and visually dominant by being the only
   full-width, 48px-tall, accent-filled thing on it. Anchor and button share the
   rule because whether the next action is a GET or a POST is a fact about the
   platform, not about how big the student's target should be. */
.primary-action { margin: 0 0 var(--gap); }

a.primary-action__button, button.primary-action__button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 48px;
  padding: 0.7rem 1.4rem;
  color: var(--accent-fg);
  background: var(--accent);
  border: 1px solid transparent;
  border-radius: var(--radius);
  font: inherit;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}

/* What it costs and why, under the button. Never the button's own words: a
   label a student has to read twice is not a primary action. */
.primary-action__note { margin: 0.4rem 0 0; font-size: 0.9375rem; color: var(--muted); }

/* The wait. J8: every wait says what is happening and when it resolves, so
   this box cannot render without a "next" line — src/ui/student/kit/pending.tsx
   refuses to. The bar is CSS-only because the strict CSP forbids inline script
   and a wait that needs JavaScript to look alive is the failure it is meant to
   prevent. studentux_liveness hangs a subscription off data-stream here. */
.pending {
  padding: 0.85rem 1rem;
  background: var(--wash);
  border: 1px solid var(--wash-line);
  border-radius: var(--radius-card);
}

.pending__track {
  height: 5px;
  margin: 0 0 0.6rem;
  background: var(--wash-line);
  border-radius: 3px;
  overflow: hidden;
}

.pending__bar {
  height: 5px;
  width: 40%;
  background: var(--accent);
  border-radius: 3px;
  animation: pending-slide 1.8s ease-in-out infinite;
}

@keyframes pending-slide {
  0% { margin-left: -40%; }
  100% { margin-left: 100%; }
}

/* Motion is the flair; the words are the substance. With reduced motion the
   bar simply sits there full-width and the sentences say the same thing. */
@media (prefers-reduced-motion: reduce) {
  .pending__bar { width: 100%; animation: none; }
}

.pending__what { margin: 0; font-weight: 600; }
.pending__next { margin: 0.25rem 0 0; }
.pending__since { margin: 0.25rem 0 0; font-size: 0.875rem; color: var(--muted); }

/* --- end kit --- */

/* --- scannable kit (docs/chunks/scannable_kit) ------------------------------

   The CSS for src/ui/kit/, shared by student and instructor pages.
   test/scannable_kit.test.ts reads this block by its two markers and checks
   that it uses tokens only (so dark mode follows the tokens), declares no
   opacity and no px widths, and gives every interactive element the kit
   renders a min-height of 44px or more. Hues, radii and spacing are free to
   change (docs/narratives/scannable_ui/OVERVIEW.md, Flairs). */

/* Chips. now and done are the student kit's rules above; these three use the
   same tokens as its not-yet and future tones, plus the red fix tone. */
.chip--waiting { color: var(--warn-ink); background: var(--warn-bg); border-color: var(--warn-bg); }
.chip--fix { color: var(--bad); background: var(--bad-bg); border-color: var(--bad-bg); }
.chip--ended { color: var(--dim); background: transparent; border-color: var(--line); }

/* Actions. */
.kit-action__form { display: inline-flex; margin: 0; }

a.kit-action, button.kit-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0.5rem 0.9rem;
  color: var(--accent);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font: inherit;
  font-size: 0.9375rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}

a.kit-action--primary, button.kit-action--primary {
  color: var(--accent-fg);
  background: var(--accent);
  border-color: transparent;
  min-height: 48px;
  padding: 0.6rem 1.2rem;
  font-size: 1rem;
}

/* Page head: title and summary on the left, the action on the right; stacked
   on a phone. */
.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 0.75rem var(--gap);
  margin: 0 0 1.25rem;
}
.page-head__text { flex: 1 1 16rem; }
.page-head__title { margin: 0; }
.page-head__summary { margin: 0.25rem 0 0; color: var(--muted); }
.page-head__action { flex: 0 0 auto; }

/* Section label. */
.section-label {
  margin: 1.75rem 0 0.5rem;
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.section-label__count { font-weight: 600; }

/* Rows. */
.rows {
  list-style: none;
  margin: 0 0 var(--gap);
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
}

.row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem 1rem;
  padding: 0.6rem 1rem;
  border-top: 1px solid var(--line);
}
.row:first-child { border-top: none; }

.row__main {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  flex: 1 1 14rem;
}
.row__name { font-weight: 600; overflow-wrap: anywhere; }

.row__facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  margin: 0;
  flex: 1 1 12rem;
  font-size: 0.9375rem;
  color: var(--muted);
}

.row__actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }

/* A whole-row link: the name's ::after covers the row, and the actions sit
   above it so they stay clickable. */
.row--linked { position: relative; }
.row--linked:hover { background: var(--wash); }
a.row__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: inherit;
  text-decoration: none;
}
a.row__link::after { content: ""; position: absolute; inset: 0; }
.row--linked .row__actions { position: relative; z-index: 1; }

/* Fold. */
.fold { margin: 0 0 var(--gap); }

.fold__summary {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0.5rem 0;
  color: var(--muted);
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}
.fold__summary::-webkit-details-marker { display: none; }
.fold__toggle { flex: none; color: var(--accent); white-space: pre; }
.fold__hide { display: none; }
.fold[open] > .fold__summary .fold__show { display: none; }
.fold[open] > .fold__summary .fold__hide { display: inline; }

/* Tiles. auto-fit with an rem minimum, so a phone gets one or two columns. */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: 0.75rem;
  margin: 0 0 var(--gap);
}

.tile {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.2rem;
  padding: 0.75rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  color: var(--fg);
  text-decoration: none;
}

a.tile { min-height: 44px; }
a.tile:hover { border-color: var(--accent); }

.tile__value { font-size: 1.5rem; font-weight: 700; line-height: 1.2; }
.tile__label { font-size: 0.9375rem; font-weight: 600; }
.tile__note { font-size: 0.875rem; color: var(--muted); }

.tile--queue .tile__value { color: var(--muted); }
.tile--busy { border-color: var(--accent); background: var(--wash); }
.tile--busy .tile__value { color: var(--accent); }

/* --- end scannable kit --- */

/* --- scannable audit modules (docs/chunks/scannable_audit_modules) ---------

   Audit: a ruled pass is a row that opens with <details>. Modules: one
   section per module, and a tags table that stacks on a phone and becomes a
   table from 48rem up. */

.audit-ruled__item { padding: 0; border-top: 1px solid var(--line); }
.audit-ruled__item:first-child { border-top: none; }
.audit-ruled__summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem 1rem;
  min-height: 44px;
  padding: 0.6rem 1rem;
  cursor: pointer;
}
.audit-ruled__summary:hover { background: var(--wash); }
.audit-ruled__title { font-weight: 600; overflow-wrap: anywhere; }
.audit-ruled__fact { font-size: 0.9375rem; color: var(--muted); overflow-wrap: anywhere; }
.audit-ruled__body { padding: 0.25rem 1rem 1rem; }
.audit__criterion-id .chip { margin-right: 0.25rem; }

.module-tags { margin: 0 0 1.5rem; }
.module-tags__head { margin: 0; }
.module-tags__title { margin: 0; font-size: 1.15rem; }
.module-tags__tag { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }

.tag-table { width: 100%; border-collapse: collapse; }
.tag-table thead { display: none; }
.tag-table__row { display: block; padding: 0.6rem 0; border-top: 1px solid var(--line); }
.tag-table__row td { display: block; padding: 0.15rem 0; overflow-wrap: anywhere; }
.tag-table__row td::before {
  content: attr(data-label) ": ";
  font-size: 0.8125rem;
  color: var(--muted);
}
.tag-table__row td:first-child::before,
.tag-table__row td.tag-table__actions::before { content: none; }
.tag-table__actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.tag-table__row td.tag-table__actions { display: flex; }

@media (min-width: 48rem) {
  .tag-table thead { display: table-header-group; }
  .tag-table__row { display: table-row; padding: 0; }
  .tag-table__row td { display: table-cell; padding: 0.5rem 0.75rem 0.5rem 0; vertical-align: middle; }
  .tag-table__row td.tag-table__actions { display: table-cell; }
  .tag-table__actions .kit-action { margin: 0.15rem 0.5rem 0.15rem 0; }
  .tag-table__row td::before { content: none; }
  .tag-table th { text-align: left; font-size: 0.8125rem; color: var(--muted); padding: 0.25rem 0.75rem 0.25rem 0; }
}

/* --- end scannable audit modules --- */

/* --- scannable today (docs/chunks/scannable_today) --------------------------

   The nav's count badge and the Today page (src/ui/instructor/dashboard.tsx).
   Tokens only, so dark mode follows. */

.nav__count {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 0 0.45rem;
  border-radius: var(--radius-pill);
  background: var(--warn-bg);
  color: var(--warn-ink);
  font-size: 0.8125rem;
  font-weight: 700;
  line-height: 1.5;
}

.today__none { margin: 0; }
.today__more { margin: 0; }
.today__more a { display: inline-flex; align-items: center; min-height: 44px; }
.today__metrics-line { margin: 0; }
.today .tile--metric[data-verdict="below"] { border-color: var(--bad); }

/* --- end scannable today --- */

/* --- student progress (docs/chunks/student_session_view) ------------------ */

/* --- home: resume, map, housekeeping (docs/chunks/studentux_home_resume) ---

   THE THREE BANDS, AND THEIR REGISTER IS THE ARGUMENT. The resume card is the
   loudest thing on the page because it is the answer to the only question a
   returning student has (J2); the map is quiet structure; the account facts
   are muted text. A student who lands here with twenty minutes should be able
   to find their next action without reading a word of the other two bands.

   NO OPACITY, HERE EITHER. The kit's rule (see the block above) is a rule
   about student surfaces, not about one directory: future rows recede with
   var(--dim) and keep their borders and focus rings at full strength.

   NO INLINE STYLE ANYWHERE, so the design's percentage-width progress bar is
   not here: the CSP is style-src 'self' with no 'unsafe-inline' (src/app.tsx).
   Progress is words on this page - "segment 2 of 3" - which is the same fact,
   survives a screen reader, and cannot drift from the number beside it. */

.home__title { margin: 0 0 0.75rem; }

/* The resume card. Filled with the accent so that it reads as one object at
   arm's length on a phone; the kit's primary action inverts inside it (below)
   so the button stays the brightest thing in the brightest block. */
.hero {
  padding: 1.15rem 1rem;
  border-radius: var(--radius-card);
  background: var(--accent);
  color: var(--accent-fg);
}

.hero__eyebrow {
  margin: 0 0 0.4rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.hero__title { margin: 0 0 0.25rem; font-size: 1.15rem; line-height: 1.25; }

.hero__meta { margin: 0 0 0.9rem; font-size: 0.9375rem; }

.hero a, .hero__link { color: var(--accent-fg); }

/* The kit's button, inverted. The kit owns its size and its role; what a page
   may say about it is which two colors it wears, and on an accent-filled card
   an accent-filled button is invisible. */
.hero .primary-action { margin: 0; }

.hero a.primary-action__button, .hero button.primary-action__button {
  color: var(--accent);
  background: var(--accent-fg);
}

/* var(--muted) is a gray chosen against the page background, and this card is
   not the page background. Full-strength ink, smaller - dim by size and
   weight, never by fading a sentence somebody has to read in sunlight. */
.hero .primary-action__note { color: var(--accent-fg); font-size: 0.875rem; }

.hero__aside { margin: 0.6rem 0 0; }

.hero__aside a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

.hero__note { margin: 0.4rem 0 0; font-size: 0.8125rem; }

/* Everything signed off. The one hero with no action in it, so it takes the
   done tone rather than the accent: nothing here is asking to be pressed. */
.hero--done { background: var(--good-bg); color: var(--good); }

/* The enrollment ended. Neither good news nor an error - a fact - so it takes
   the neutral wash and the page's own ink rather than a color that would
   editorialize about somebody's refund. */
.hero--ended { background: var(--wash); color: var(--fg); border: 1px solid var(--wash-line); }

/* The other context. A card with a link and no button - the moment this grows
   one, the page has two answers to "what now" and the kit's invariant is a
   style rule instead of a fact. */
.alt {
  padding: 0.85rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
}

.alt__title { margin: 0; font-weight: 600; }
.alt__detail { margin: 0.2rem 0 0; color: var(--muted); font-size: 0.9375rem; }
.alt__go { margin: 0.2rem 0 0; }

.alt__go a, .ticket-status__go a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* A question with a person on it (docs/chunks/studentux_stuck_path).

   THE GLANCE, and it is the same card on the home page and inside the stuck
   view - src/ui/student/question_status.tsx says why there is only one of it.
   The two clocks sit on the state line where the design puts them: when it was
   sent on the left, when a reply is due on the right, so the answer to "is
   anything happening" is one line and not a paragraph. */
.ticket-status {
  padding: 0.85rem 1rem;
  background: var(--wash);
  border: 1px solid var(--wash-line);
  border-radius: var(--radius-card);
}

.ticket-status__state {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.4rem;
  font-size: 0.875rem;
  color: var(--muted);
}

/* The due time is pushed to the end of the line on a screen with room and
   wraps under it on one without. No width is declared, so neither case can
   force a horizontal scroll. */
.ticket-status__due { margin-left: auto; }

.ticket-status__summary { margin: 0; font-weight: 600; overflow-wrap: anywhere; }
.ticket-status__sla { margin: 0.25rem 0 0; font-size: 0.9375rem; }
.ticket-status__mail { margin: 0.25rem 0 0; font-size: 0.875rem; color: var(--muted); }
.ticket-status__go { margin: 0.1rem 0 0; }

/* Inside the stuck view the card IS the head of the ticket it belongs to, so
   it drops its own box and keeps only its type. Two nested washes would read as
   two separate questions. */
.ticket__card .ticket-status {
  padding: 0;
  background: transparent;
  border: 0;
}

/* The map. Rows, not cards: nine bordered boxes is nine things competing with
   the hero, and the shape of the module is easier to see when the rows are
   quiet enough to scan down. */
.map__heading {
  margin: 0.5rem 0 0.25rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.map__rows { list-style: none; margin: 0; padding: 0; }

.map__row {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.5rem 0;
  border-top: 1px solid var(--line);
}

/* The circle. Decoration - it is aria-hidden, because the chip beside it
   carries the state in words - so its whole job is to make "done", "here" and
   "later" legible in one downward glance. */
.map__marker {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 1.65rem;
  height: 1.65rem;
  margin-top: 0.55rem;
  border-radius: 50%;
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 1;
}

.map__marker--done { color: var(--accent-fg); background: var(--accent); }

/* The 1.5px ring is what marks the current thing, here and on a criterion. */
.map__marker--now { color: var(--accent); border: 2px solid var(--accent); }

.map__marker--future { color: var(--dim); border: 1px solid var(--dim); }

.map__body { flex: 1 1 auto; min-width: 0; }

.map__title {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-weight: 600;
  color: var(--fg);
}

.map__status { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; margin: 0; }

.map__detail { color: var(--muted); font-size: 0.875rem; }

.map__ahead { margin: 0; }

.map__ahead a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: 0.9375rem;
}

/* Housekeeping. Present, readable, and last: these facts are what tell a
   student their setup worked (enrollment_invites_keys), and they are never
   again the first thing on the page. Muted, not hidden - a disclosure here
   would put the remaining budget behind a triangle. */
.keeping { border-top: 1px solid var(--line); padding-top: 0.75rem; }

.keeping__heading {
  margin: 0 0 0.4rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.keeping__fact { margin: 0 0 0.4rem; font-size: 0.9375rem; }

.keeping__gate { margin: 0.6rem 0 0; }

/* --- end home --- */

/* --- submitting (docs/chunks/certify_intake) ------------------------------ */

/*
 * The submit form. Every rule here obeys the phone rules at the top of this
 * file — fluid widths, 16px inputs so iOS does not zoom, 44px tap targets —
 * because the lab is done at a desk but the form is opened on whatever is
 * nearest.
 */

textarea {
  font-family: inherit;
  line-height: 1.4;
  resize: vertical;
}

input[type="file"] {
  display: block;
  width: 100%;
  min-height: 44px;
  padding: 0.6rem 0;
  font-size: 1rem;
  color: var(--fg);
}

.field + .field { margin-top: 0.75rem; }

.field-group {
  margin: 0 0 var(--gap);
  padding: var(--gap);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.field-group legend {
  padding: 0 0.4rem;
  font-weight: 600;
}

/*
 * One worksheet entry is a stack of labeled boxes, not a row of a table: four
 * text inputs side by side at 375px is the horizontal scrollbar
 * test/ui_phone.test.ts exists to prevent.
 */
.worksheet-row { background: transparent; }

.slot--submit summary { cursor: pointer; min-height: 44px; }

/*
 * docs/chunks/both_places_submit — the hand-in's source. Each log or file
 * artifact has a block per source; the one not checked is hidden. A browser
 * without :has() shows both, and the server reads only the checked source.
 */
.source-choice:has(input[name="source"][value="agent"]:checked) [data-source="upload"],
.source-choice:has(input[name="source"][value="upload"]:checked) [data-source="agent"] {
  display: none;
}

.source-choice__option {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-height: 44px;
  margin: 0;
  font-weight: 400;
}

.source-choice__option input { width: 1.25rem; height: 1.25rem; margin: 0; flex: none; }

.timeline { margin: 0; padding-left: 1.25rem; }

.slot { border-style: dashed; }

/* --- player: the listening surface (docs/chunks/studentux_player) ---------
/* --- the session page (docs/chunks/studentux_session_page) ----------------

   THE PAGE IS A PATH, AND THE CSS SAYS SO. Three shapes and no others:

     a STRIP    for the listening once it is behind the student — one line,
                one border, no card. The thing they are most certain about
                costs the least page.
     a CARD     for the contract, and for the one part they are on. Two cards
                on a session page, both about the present.
     a ROW      for every part that is not theirs right now. A row carries a
                number, a name and a cost, and no material at all.

   The current part is marked by an ACCENT BORDER, and the parts ahead recede
   by COLOR (var(--dim)) — never by opacity, for the reason the kit block
   above argues at length: a future part must stay legible, because "what is
   coming and how long does it take" is the question a student answers before
   deciding to start.

   Radii, hues and paddings here are flair
   (docs/narratives/studentux_alignment/OVERVIEW.md); the three shapes are not.
   Everything obeys the phone rules at the top of this file: no px width, 44px
   targets, min-width queries only. */

.listen-strip {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.75rem var(--gap);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--bg);
}

.listen-strip__line { margin: 0; }

/* Chunk: docs/chunks/studentux_twin_tracks — the other rendering, beside the
   button and not competing with it: a link, at a thumb's size. */
.listen-strip__twin {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 0.5rem;
}

.listen-strip__read > summary {
  cursor: pointer;
  min-height: 44px;
  display: flex;
  align-items: center;
  color: var(--accent);
}

/* The contract, before the work. The wash is what separates it from the
   material below without shouting: it is not an alert, it is the terms. */
.contract {
  padding: var(--gap);
  border: 1px solid var(--wash-line);
  border-radius: var(--radius-card);
  background: var(--wash);
}

.contract__title { margin: 0 0 0.5rem; font-size: 1.0625rem; }

.contract__artifacts { list-style: none; margin: 0; padding: 0; }

.contract__artifact + .contract__artifact { margin-top: 0.75rem; }

.contract__artifact-name { margin: 0; font-weight: 600; }
.contract__artifact-form { margin: 0; font-size: 0.9375rem; }

.contract__expects { margin: 0.25rem 0 0; padding-left: 1.25rem; font-size: 0.9375rem; }

.contract__bar { margin-top: var(--gap); }
.contract__bar-title { margin: 0 0 0.25rem; font-size: 1rem; }
.contract__criteria { margin: 0 0 0.5rem; padding-left: 1.25rem; }

.contract__examples > summary {
  cursor: pointer;
  min-height: 44px;
  display: flex;
  align-items: center;
  color: var(--accent);
}

.contract__example-of { margin: 0; font-weight: 600; font-size: 0.9375rem; }

.contract__promise { margin: var(--gap) 0 0; }

/* The lab, as its sittings. */
.parts__heading { margin: 0 0 0.5rem; }
.parts__cost { font-size: 0.9375rem; font-weight: 400; }

.parts__list { list-style: none; margin: 0; padding: 0; }

.parts__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.5rem;
  padding: 0.6rem var(--gap);
  min-height: 44px;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
}

/* Dimmed by color. See the kit block's rule 1. */
.parts__row--future .parts__title,
.parts__row--future .parts__number { color: var(--dim); }

.parts__row--done .parts__title { color: var(--muted); }

/* The one that is theirs. The accent border IS the marker of "current"
   everywhere in this design — the chip's "now" tone draws the same ring. */
.parts__row--current {
  display: block;
  border-color: var(--accent);
  border-width: 2px;
}

.parts__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

.parts__number {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  flex: 0 0 auto;
  border: 1px solid var(--line);
  border-radius: 50%;
  font-size: 0.8125rem;
  font-weight: 600;
}

.parts__number--current { border: 2px solid var(--accent); color: var(--accent); }

.parts__title { flex: 1 1 12rem; margin: 0; font-size: 1rem; font-weight: 600; }

.parts__minutes { font-size: 0.8125rem; white-space: nowrap; }

.parts__outcome { margin: 0; }

.parts__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap);
}

button.parts__finish {
  min-height: 44px;
  padding: 0.6rem 1.2rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.parts__last { margin: 0; }
.parts__stuck { margin: 0; font-size: 0.9375rem; }

/* --- audio concept track (docs/chunks/audio_concept_track) ---------------

   THE ONE SCREEN USED WITH THE PHONE IN A CRADLE, and the only one whose real
   criterion — it plays with the screen off — cannot be tested in CI at all
   (TESTING_PHILOSOPHY.md, README.md § Checking the concept track by ear). What
   CI can hold is the geometry, and everything between this marker and
   "end player" is what test/ui_phone.test.ts reads.

   THREE FLOORS, AND EACH IS A DECISION ABOUT A THUMB THAT CANNOT SEE:

     * .player__play is 84px. It is the control a person reaches for blind, at
       a red light or with the phone in a pocket, and it is the only one that
       has to be findable by size alone.
     * .player__step is 48px. Hitting the wrong skip costs a whole part and a
       re-listen, so it sits above the ordinary floor.
     * .player__back and .player__speed are 44px, the platform-wide minimum.
       These are chosen deliberately, at a desk or stopped.

   The rule the test enforces runs over the WHOLE BLOCK — no min-height under
   44px anywhere in it — because the next control added here is the one a list
   of named selectors would have missed. That is the seam the kit block cut
   above and it works for the same reason.

   Everything else here is flair
   (docs/narratives/studentux_alignment/OVERVIEW.md § The flairs): the 120px
   motif, the circle, the hues, the radii. Change them freely; the three
   numbers above are not free. */

/* Where you are, and the way back. */
.player__top {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}

.player__back {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  margin-left: -0.75rem;
  color: var(--fg);
  border-radius: var(--radius);
}

/* The page's <h1>, and deliberately not the biggest thing on it: the part
   title below is the visual anchor, and this is the line that says which
   session you are in. Sized as prose, weighted as prose. */
.player__where {
  margin: 0;
  font-size: 0.875rem;
  font-weight: 400;
  color: var(--muted);
}

.player { position: relative; text-align: center; }

.player__stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
}

/* The motif. Purely a place for the eye to land while the ears do the work —
   which is why it is aria-hidden in the markup and why its size is the one
   fixed dimension on the page that means nothing. */
.player__motif {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 120px;
  height: 120px;
  margin-bottom: 0.5rem;
  color: var(--accent);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 50%;
}

/* The part's own title: the largest thing on the screen, because it is the
   answer to the only question a student asks here. */
.player__title {
  margin: 0;
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.3;
}

.player__part { margin: 0; font-size: 0.875rem; color: var(--muted); }

.player__clock { margin: 1rem 0 0.5rem; }

/* A native <progress>, not a div with a width. The CSP carries no
   'unsafe-inline' (src/app.tsx), so a bar whose fill is an inline style
   attribute could not be rendered by the server or moved by the script
   without relaxing it. The numbers beside it say the same thing in words for
   any browser that renders this plainly. */
.player__track {
  display: block;
  width: 100%;
  height: 6px;
  -webkit-appearance: none;
  appearance: none;
  border: 0;
  border-radius: 3px;
  background: var(--line);
  color: var(--accent);
}

.player__track::-webkit-progress-bar { background: var(--line); border-radius: 3px; }
.player__track::-webkit-progress-value { background: var(--accent); border-radius: 3px; }
.player__track::-moz-progress-bar { background: var(--accent); border-radius: 3px; }

.player__times {
  display: flex;
  justify-content: space-between;
  margin: 0.4rem 0 0;
  font-size: 0.75rem;
  color: var(--muted);
}

.player__controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 1.5rem;
  margin: 0.5rem 0;
}

/* Skip a part. 48px: above the platform floor, because the cost of a mis-tap
   here is a part of the session. */
.player__step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  min-height: 48px;
  padding: 0;
  color: var(--fg);
  background: transparent;
  border: 0;
  border-radius: 50%;
}

/* The one control a thumb finds without eyes. */
.player__play {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 84px;
  height: 84px;
  min-height: 84px;
  padding: 0;
  color: var(--accent-fg);
  background: var(--accent);
  border: 1px solid transparent;
  border-radius: 50%;
  font-size: 1rem;
  font-weight: 600;
}

.player__speeds {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
}

.player__speed {
  min-height: 44px;
  min-width: 3.5rem;
  padding: 0.5rem 0.75rem;
  color: var(--accent);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
}

.player__speed[aria-pressed="true"] {
  color: var(--accent-fg);
  background: var(--accent);
  border-color: var(--accent);
}

/* The beat, pre-announced. Amber and quiet: it is not a warning, it is a
   rhythm the student is being let in on before it arrives. Hidden — by the
   attribute, not by a class — on a part that ends in no question, because a
   promised question that never comes is worse than no promise. */
.player__ahead {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
  text-align: left;
  background: var(--warn-bg);
  border-radius: var(--radius-card);
}

.player__ahead[hidden] { display: none; }

.player__ahead-icon { flex: none; color: var(--warn-ink); }
.player__ahead-title { margin: 0; font-size: 0.8125rem; font-weight: 600; color: var(--warn-ink); }
.player__ahead-note { margin: 0.1rem 0 0; font-size: 0.75rem; color: var(--warn-ink); }

/* The recall prompt and the model answer, as text, for a student who is at a
   desk rather than driving. Hidden while a part plays; the audio is what a
   driver gets and this is the same words. */
.player__beat {
  margin: 0;
  padding: 0.75rem;
  text-align: left;
  background: var(--warn-bg);
  border-radius: var(--radius);
  font-size: 1.0625rem;
}

/* The player card is hidden until its manifest has loaded. Until then the page
   IS the no-JavaScript page — the part list and the plain audio elements below
   are already there and already work — and a card of buttons that do nothing
   would be worse than no card. JavaScript removes the attribute; with
   JavaScript off it never appears.

   The part list below is deliberately NOT inside it. */
.player[data-idle] { display: none; }

/* --- this session, in parts ---------------------------------------------

   Always rendered, always visible, JavaScript or not: reading what a session
   contains is the thing a student does before deciding whether the drive is
   long enough. Rows are links into the by-hand players below, upgraded into
   in-place jumps when the script is running. */

.parts { margin-top: var(--gap); border-top: 1px solid var(--line); }

.parts__heading {
  margin: 1rem 0 0.25rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.parts__list { list-style: none; margin: 0; padding: 0; }

.parts__row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-height: 44px;
  padding: 0.5rem 0;
  color: var(--fg);
  text-decoration: none;
}

.parts__title { flex: 1 1 auto; }

.parts__minutes { flex: none; font-size: 0.75rem; color: var(--muted); }

/* The current part carries its weight in ink, not in a background: the chip
   beside it already says "Now", and a filled row would compete with the one
   thing on this page that is allowed to dominate. */
.parts__row[aria-current="true"] .parts__title { font-weight: 600; }

/* Parts already heard, and parts still ahead, recede by COLOR — the kit's
   rule (src/ui/styles.ts § kit), applied to the row as well as to its chip. */
.parts__row[aria-current="false"] { color: var(--muted); }

/* --- end player --- */

/* --- reading: the written twin (docs/chunks/studentux_twin_tracks) ---------

   THE TWIN IS NOT A LESSER PAGE, and the CSS is where that stops being a
   claim. The reading view reuses the player's header (.player__top,
   .player__back, .player__where) rather than drawing its own, because the
   two renderings are peers and a student crossing between them should feel one
   place. What is here is only what the player has no equivalent for: a part as
   a block of prose, its mark, and the question at the end of it.

   THE CURRENT PART IS MARKED BY AN ACCENT BORDER — the same marker of "now"
   the session page's current lab part and the kit's "now" chip use. Parts
   ahead recede by COLOR (var(--dim)) and never by opacity, for the reason the
   kit block argues: a part a student has not reached must stay legible,
   because reading ahead is allowed here and always was.

   Everything obeys the phone rules at the top of this file: no px width, 44px
   targets, min-width queries only. */

/* THE EQUIVALENCE STRIP, on both pages of the track — the reading view and the
   player (src/routes/student/listen.tsx renders .player__twin). One rule for
   one sentence: the two renderings say the same thing in the same box, because
   a promise that looks different on each page is two promises. */
.read__twin,
.player__twin {
  margin: 0 0 var(--gap);
  padding: 0.6rem var(--gap);
  border: 1px solid var(--wash-line);
  border-radius: var(--radius-card);
  background: var(--wash);
  font-size: 0.9375rem;
}

.read__twin a,
.player__twin a {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

.read__session { margin: 0 0 0.25rem; font-size: 1.125rem; }

.read__heading {
  margin: 1rem 0 0.5rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

/* One part. A block with room to read in, not a card fighting for attention. */
.read__part {
  margin: 0 0 var(--gap);
  padding: 0.75rem var(--gap);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--bg);
}

/* The one that is theirs. */
.read__part--current { border-color: var(--accent); border-width: 1.5px; }

.read__mark {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 0.25rem;
}

.read__minutes { font-size: 0.75rem; color: var(--muted); }

.read__title { margin: 0 0 0.5rem; font-size: 1.0625rem; }

/* A part not yet reached recedes in ink and keeps every border and focus ring
   at full strength. Dim by color; see the kit block's rule 1. */
.read__part:not(.read__part--current) .read__title { color: var(--muted); }

/* The recall beat: the question is the control, the answer is behind it. */
.read__recall {
  margin: var(--gap) 0 0;
  padding: 0.6rem var(--gap);
  border: 1px solid var(--warn-bg);
  border-left: 3px solid var(--warn-ink);
  border-radius: var(--radius);
  background: var(--warn-bg);
}

.read__recall-note { margin: 0 0 0.35rem; font-size: 0.8125rem; color: var(--warn-ink); }

.read__answer > summary {
  cursor: pointer;
  min-height: 44px;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 600;
}

.read__answer-body { margin-top: 0.5rem; }

/* The library copy: reference, and shaped like reference. */
.read__sources { margin-top: var(--gap); border-top: 1px solid var(--line); }

.read__sources-title { margin: var(--gap) 0 0.25rem; font-size: 1rem; }

.read__source-list { margin: 0.5rem 0 0; padding-left: 1.25rem; }

.read__source-list a { min-height: 44px; display: inline-flex; align-items: center; }

/* --- end reading --- */

audio { width: 100%; min-height: 44px; }

/* A link that has to be as easy to hit as a button (the "Listen" affordance on
   the session page). Same box as a button, without pretending to be a form. */
.button-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0.6rem 1rem;
  color: var(--accent-fg);
  background: var(--accent);
  border-radius: var(--radius);
  text-decoration: none;
  font-weight: 600;
}

/* Rendered module markdown. The module wrote it; these rules keep it inside a
   phone screen whatever it contains. */
.prose { max-width: 100%; }
.prose > *:first-child { margin-top: 0; }
.prose img { max-width: 100%; height: auto; }
.prose pre { max-width: 100%; }
.prose table { display: block; max-width: 100%; overflow-x: auto; }
.prose blockquote {
  margin: 0 0 1rem;
  padding: 0 0 0 0.75rem;
  border-left: 3px solid var(--line);
  color: var(--muted);
}

/* Chunk: docs/chunks/ta_grounded_chat — the TA panel.

   It is a form, not an app: no JavaScript, so no scroll containers to manage,
   no sticky positioning to fight the iOS keyboard, and no fixed heights. The
   thread grows down the page and the compose box is under it, which is where a
   phone keyboard expects it. */
.shell__aside { width: 100%; padding: 0 var(--gap) var(--gap); }
.shell__aside > * { max-width: 42rem; }
.shell--instructor .shell__aside > * { max-width: 70rem; }

.ta__thread { list-style: none; margin: 0; padding: 0; }
.ta__turn { border-top: 1px solid var(--line); padding: 0.75rem 0; }
.ta__turn:first-child { border-top: 0; padding-top: 0; }
.ta__who { margin: 0 0 0.25rem; font-size: 0.8125rem; font-weight: 600; color: var(--muted); }
.ta__body > p { margin: 0 0 0.5rem; }
.ta__body > p:last-child { margin-bottom: 0; }
.ta__sources, .ta__rated { margin: 0.375rem 0 0; font-size: 0.875rem; }
.ta__source { overflow-wrap: anywhere; }
.ta__rate { margin: 0.375rem 0 0; }
.ta__provenance { margin: 0; font-size: 0.875rem; }

/* The TA, writing (docs/chunks/studentux_liveness). One line under a reply
   that is still arriving: "Reading your question", then nothing once the words
   themselves are the motion. It is the live region, not the answer — a screen
   reader read every token of a paragraph as it lands is worse than silence.
   The element exists only while the enhancement script is rendering into it,
   so this rule costs nothing on a page without it. */
.ta__writing {
  margin: 0.25rem 0 0;
  font-size: 0.875rem;
  color: var(--muted);
  font-style: italic;
}
.ta__writing:empty { display: none; }

/* A question the student is typing on a phone. 16px keeps iOS from zooming, and
   the width is fluid because nothing here may force a horizontal scroll. */
.ta__ask textarea {
  width: 100%;
  font: inherit;
  font-size: 1rem;
  padding: 0.5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg);
  resize: vertical;
  min-height: 88px;
}
.ta__ask label { font-weight: 600; }

/* --- stuck: three ways forward (docs/chunks/studentux_stuck_path) ---------

   THE PANEL IS THE STUCK VIEW, so these rules dress the same element the rules
   above dress. What they add is the design's Stuck artboard: an eyebrow that
   names where the student is, three numbered cards in the order the ways are
   meant to be tried, and the open question under all of them.

   Read over the WHOLE block by test/stuck_path.test.ts, the way the kit's and
   the player's blocks are read: no px width (this is an aside on a phone), no
   opacity (way three recedes in INK - a control faded to 60% stops existing in
   sunlight, and way three is the one a stuck student needs to still be able to
   read), and nothing tappable under 44px. */

.stuck__head { margin: 0; }

.stuck__where {
  margin: 0;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.stuck__title { margin: 0.15rem 0 0; }
.stuck__normal { margin: 0.35rem 0 0; font-size: 0.875rem; color: var(--muted); }

/* One way. A box each, because the three are alternatives rather than steps in
   a sequence: a student picks one, and a card is the shape that says so. */
.stuck__way {
  padding: 0.85rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
}

/* Way three is the quietest of the three and dims BY COLOR. It is also the
   only one with nothing to press, which is the honest thing: a 1:1 is offered,
   never demanded (DEC-005). */
.stuck__way--quiet { color: var(--dim); }

.stuck__step {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.5rem;
  font-size: 1rem;
  font-weight: 600;
}

/* The number. aria-hidden in the markup - the heading text carries the
   ordering for a screen reader and "1 A hint, right now" read aloud is noise. */
.stuck__number {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  background: var(--wash);
  color: var(--accent);
  font-size: 0.8125rem;
  font-weight: 700;
}

.stuck__what { font-weight: 600; }

/* "a person answers within a day", said before the student asks for it. The
   positive tone rather than the warn one: this is the platform keeping a
   promise, not warning anybody about anything. */
.stuck__pill {
  border-radius: var(--radius-pill);
  padding: 0.15rem 0.6rem;
  background: var(--good-bg);
  color: var(--good);
  font-size: 0.75rem;
  font-weight: 600;
}

.stuck__promise { margin: 0 0 0.6rem; font-size: 0.9375rem; }
.stuck__live { margin: 0; font-size: 0.9375rem; }
.stuck__booking { margin: 0.5rem 0 0; font-size: 0.9375rem; color: var(--fg); }
.stuck__booking a { display: inline-flex; align-items: center; min-height: 44px; }

/* The entry point on the player, which is the one student page with no aside
   and therefore no anchor to jump to (src/routes/student/listen.tsx). */
.player__stuck a { display: inline-flex; align-items: center; min-height: 44px; }

/* --- end stuck --- */

/* --- certification feedback (docs/chunks/certify_judge) ------------------- */
/* --- the verdict (docs/chunks/studentux_verdict_moment) -------------------- */

/*
 * A verdict a student reads on a phone, five minutes after handing work in.
 * One column, no table, quotes that wrap: this is the most important text on
 * the platform and it is opened on whatever is to hand.
 *
 * WHAT THE REDESIGN MOVED, in CSS terms. The old block styled a list; this one
 * styles a document with a head, rows, a question card and a foot
 * (docs/design/canvas/Verdict.dc.html). The rules below are ordered the way
 * the page is, so a reader of either can find the other.
 *
 * A ROW IS A CARD, NOT A LIST ITEM. Every criterion sits on its own surface
 * with its own border, because the eye finds "how many of these are there and
 * which one is amber" in a stack of cards and reads a bulleted list line by
 * line. The border on a row that is not met is the only place a color does
 * work the words are not already doing.
 */

.verdict__head { display: flex; flex-direction: column; gap: 0.35rem; }

/* Small caps over the headline: what this card is about, said once, quietly,
   so the headline underneath does not have to spend a word on it. */
.verdict__eyebrow {
  margin: 0;
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The chip sits BESIDE the headline and wraps under it on a narrow phone —
   flex-wrap rather than a media query, because the breakpoint here is the
   length of the headline, not the width of the device. */
.verdict__headline-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
}

/* The four to six words a student reads before deciding to read anything
   else. Bigger than an h3 anywhere else on the platform, on purpose. */
.verdict__headline { margin: 0; font-size: 1.375rem; line-height: 1.25; }

.verdict__summary { margin: 0; }

/* The provenance, above the criteria and never in small print at the bottom:
   SPEC.md § Guarantees requires the label, and a label a student meets after
   reading the whole verdict has already let them believe a person wrote it. */
.verdict__provenance { margin: 0; font-size: 0.875rem; }

/* An instructor's own words about their own ruling. Full ink, and marked as a
   quotation from a person by the accent rule rather than by a label. */
.verdict__ruling {
  margin: 0;
  padding-left: 0.75rem;
  border-left: 3px solid var(--accent);
}

/* --- the rows --- */

.verdict__criteria {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.verdict__row {
  padding: 0.75rem 1rem;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
}

/* The one row that needs the student's attention says so in its edge as well
   as in its chip. Color, never opacity (src/ui/styles.ts § kit). */
.verdict__row--unmet { border-color: var(--warn-bg); }
.verdict__row--pending { border-color: var(--wash-line); }

.verdict__criterion { margin: 0; }
.verdict__statement { font-weight: 500; }

/*
 * THE ONE NEXT STEP. Full ink and a little heavier than the row around it,
 * because it is the only sentence on the card the student can act on and it
 * used to render in the same gray as the locator (src/certify/feedback.ts
 * guarantees there is exactly one of them per criterion).
 */
.verdict__next { margin: 0.5rem 0 0; font-weight: 600; }

/*
 * The quoted evidence. Indented and bordered so it reads as the student's own
 * words rather than the platform's, and overflow-wrap because a pasted line
 * from a log has no spaces in it and must not force a horizontal scroll
 * (test/ui_phone.test.ts).
 */
.verdict__quote {
  margin: 0.5rem 0 0;
  padding: 0.35rem 0 0.35rem 0.75rem;
  border-left: 3px solid var(--line);
  overflow-wrap: anywhere;
}
.verdict__quote p { margin: 0; }
.verdict__quote-label { font-size: 0.75rem; color: var(--muted); }
.verdict__quote-text { font-style: italic; line-height: 1.55; }
.verdict__quote-where { font-size: 0.875rem; color: var(--muted); }

/* What it read on a criterion that IS met: available, and not in the way.
   The summary is a real tap target on a phone. */
.verdict__proof > summary {
  min-height: 44px;
  display: flex;
  align-items: center;
  font-size: 0.875rem;
  color: var(--muted);
  cursor: pointer;
}

/* --- the question --- */

/*
 * A PERSON LEANING IN. Its own surface, its own border color, and the only
 * 1.5px rule in the student UI: the card has to read as a different KIND of
 * thing from the rows above it before a word of it is read. #d9a92c is the
 * design's question-card border and is flair
 * (docs/narratives/studentux_alignment/OVERVIEW.md § The flairs).
 */
.verdict__question {
  padding: 1.15rem 1.15rem 1rem;
  background: var(--bg);
  border: 1.5px solid #d9a92c;
  border-radius: var(--radius-card);
}

.verdict__question-head { margin: 0; }
.verdict__question-title { font-weight: 600; }
.verdict__question-out { margin: 0; font-size: 0.875rem; }

.verdict__ask-about { margin: 0; font-weight: 600; }

/* The mark, the question and the box: a row on a laptop, a stack on a phone
   when the mark and the words no longer fit side by side. */
.verdict__ask-body { display: flex; gap: 0.75rem; align-items: flex-start; }

.verdict__ask-mark {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-fg);
  font-weight: 700;
}

.verdict__ask-field { flex-grow: 1; min-width: 0; }

/* The question is the label, so it is not small — it is the largest run of
   text in the card, which is the design's own emphasis. */
.verdict__ask-field label { font-size: 1.0625rem; font-weight: 600; line-height: 1.45; }
.verdict__ask-hint { margin: 0.25rem 0 0.5rem; font-size: 0.875rem; }
.verdict__ask-field textarea { font-size: 1rem; }

/* --- the foot --- */

.verdict__foot {
  padding-top: 0.85rem;
  border-top: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/* The dispute is an offer, not a demand: its label is a sentence at body
   weight and its button keeps the quiet link face. */
.verdict__dispute label { font-weight: 400; }
.verdict__clock { margin: 0; font-size: 0.8125rem; }
.verdict__disputed { margin: 0; }

/* --- end verdict --- */

/* --- escalation (docs/chunks/escalation_inbox) ---------------------------- */

/*
 * THE INSTRUCTOR'S QUEUE. One column of cards on any width, because the page is
 * read on a phone between other things as often as at a desk — the 24-hour
 * promise in DEC-005 is kept in the gaps of a day. No table: a ticket is a
 * paragraph of context, not a row of fields, and a table is what makes a queue
 * skimmable in a way that skips the package.
 */
.inbox__ticket--breached { border-left: 4px solid var(--accent); }

.inbox__badge {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

/*
 * The three sources, told apart at a glance. 0007_ta.sql asks for it: "the
 * machine gave up" and "the student disagrees" are read in a different frame,
 * and per DEC-002 the second is the rarer and more informative one. Color is
 * the hint and the badge TEXT is the fact — nothing here depends on seeing it.
 */
.inbox__badge--student { color: var(--accent); }
.inbox__badge--certifier { color: var(--fg); }
.inbox__badge--ta { color: var(--muted); }

.inbox__summary { margin: 0; font-size: 1.05rem; }
.inbox__sla { margin: 0; font-size: 0.9375rem; color: var(--muted); }
.inbox__sla--breached { color: var(--fg); font-weight: 600; }
.inbox__who, .inbox__frame, .inbox__booking { margin: 0; font-size: 0.875rem; }
.inbox__head { gap: 0.25rem; }

/* The package. A definition list, so the seven parts keep their labels. */
.inbox__package { margin: 0; }
.inbox__part {
  margin: 0.75rem 0 0.25rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--muted);
}
.inbox__body {
  margin: 0 0 0 0;
  padding: 0;
  overflow-wrap: anywhere;
}
.inbox__body p { margin: 0 0 0.5rem; }

/*
 * The student's own words, set apart. src/escalation/open.ts says to lead with
 * these, and a quote rule is what makes "lead with" visible rather than merely
 * first.
 */
.inbox__body--studentWords {
  padding: 0.5rem 0.75rem;
  border-left: 3px solid var(--accent);
}
/* A machine transcript: votes, verdicts, prompt versions. Monospace so the
   columns of a five-vote tally line up, and pre-wrap so nothing scrolls. */
.inbox__body--triedAndDecided p {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8125rem;
  white-space: pre-wrap;
}

.inbox__messages, .inbox__stuck-list { list-style: none; margin: 0; padding: 0; }
.inbox__message { padding: 0.5rem 0; border-top: 1px solid var(--line); }
.inbox__role { margin: 0 0 0.25rem; font-size: 0.8125rem; font-weight: 600; color: var(--muted); }
.inbox__message p { margin: 0 0 0.5rem; overflow-wrap: anywhere; }
.inbox__delivery { font-size: 0.8125rem; font-weight: 600; }

.inbox__more summary { cursor: pointer; min-height: 44px; padding: 0.6rem 0; font-weight: 600; }
.inbox__check { font-weight: 400; }
.inbox__check input { min-height: 44px; }
.inbox__rule-buttons { display: flex; flex-wrap: wrap; gap: 0.5rem; }

/* Somebody the platform told a person would look, and never told a person. */
.inbox__stuck { border-left: 4px solid var(--accent); }
.inbox__stuck-list li { padding: 0.5rem 0; border-top: 1px solid var(--line); }
.inbox__stuck-list p { margin: 0 0 0.25rem; overflow-wrap: anywhere; }

/*
 * THE STUDENT'S HALF. Inside the TA panel, deliberately quieter than the ask
 * box above it: DEC-004 makes the TA the first line, and a human-shaped button
 * that shouts is how a 100:1 budget stops working. It is always present, which
 * is the other half of the same balance — the prior art's finding is that
 * students under-escalate.
 */
/* Chunk: docs/chunks/studentux_stuck_path — the card wears the status card's
   own wash instead of the warn-colored notice it used to be. A question with
   a person on it is not a warning, and the three rules that used to render its
   head by hand (state, summary, sla) are now OpenQuestionCard. */
.ticket__card {
  padding: 0.85rem 1rem;
  background: var(--wash);
  border: 1px solid var(--wash-line);
  border-radius: var(--radius-card);
}

.ticket__messages { list-style: none; margin: 0.5rem 0 0; padding: 0; }
.ticket__message { padding: 0.5rem 0; border-top: 1px solid var(--line); }
.ticket__message p { margin: 0 0 0.5rem; overflow-wrap: anywhere; }
.ticket__who { font-size: 0.8125rem; font-weight: 600; color: var(--muted); }
.ticket__reply textarea { font-size: 1rem; }
.ticket__reply label { font-weight: 400; font-size: 0.9375rem; }

/* Chunk: docs/chunks/studentux_stuck_path — the composer is a form, not a
   <details> the student has to think to open. It lives in way two of the panel
   and the promise above it is what earns the click. */
.ticket__stuck textarea { font-size: 1rem; }
.ticket__stuck-note { margin: 0; font-size: 0.875rem; }
/*
 * Chunk: docs/chunks/checkins_email — a pending check-in draft.
 *
 * The body is shown in a <pre> because it IS plain text: what the student's
 * mail client will render, newlines and all. It sets white-space to pre-wrap
 * rather than taking the pre default, so an eighty-word paragraph wraps on a
 * phone instead of scrolling sideways (test/ui_phone.test.ts), and it sets
 * overflow-wrap because the one link in it is a URL with no spaces in it.
 */
.checkin__body {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin: 0;
  background: none;
  font-family: inherit;
  font-size: 0.9375rem;
}

/* Send and Discard, side by side, wrapping when there is no room. */
.checkin__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; }

/*
 * Chunk: docs/chunks/instructor_dashboard — the page the course is run from.
 *
 * THE ROSTER IS A TABLE ON A DESK AND A STACK OF CARDS ON A PHONE, and it is
 * the same markup either way. The base (phone) rules turn every cell into a
 * block with its column name printed in front of it from its data-label; the
 * min-width query below hands the browser back its table layout. That is the
 * only honest way to keep a seven-column comparison readable at 375px without
 * a second component to maintain — and the roster IS a comparison, which is
 * why it is a table at all and the inbox is not.
 *
 * No px width appears anywhere below; the table is width: 100% and the metric
 * grid uses auto-fit with an rem minimum, so test/ui_phone.test.ts stays quiet
 * and nothing scrolls sideways.
 */
.dash__queue-list { list-style: none; margin: 0; padding: 0; }
.dash__queue { padding: 0.5rem 0; border-top: 1px solid var(--line); }
.dash__queue:first-child { border-top: none; }
.dash__queue a { min-height: 44px; display: inline-block; padding: 0.4rem 0; }
.dash__queue-count { font-weight: 700; font-size: 1.125rem; }
.dash__queue-note { font-size: 0.875rem; }
.dash__queue--urgent .dash__queue-count { color: var(--accent); }

.dash__table { width: 100%; border-collapse: collapse; }
.dash__table thead { display: none; }
.dash__row { display: block; padding: 0.75rem 0; border-top: 1px solid var(--line); }
.dash__row td { display: block; padding: 0.15rem 0; overflow-wrap: anywhere; }
/* The column name, on the phone layout only. */
.dash__row td::before {
  content: attr(data-label) ": ";
  font-size: 0.8125rem;
  color: var(--muted);
}
.dash__row td:first-child::before { content: none; }
.dash__row--risk { border-left: 4px solid var(--accent); padding-left: 0.5rem; }
.dash__stall { margin: 0.25rem 0 0; font-size: 0.875rem; }

/*
 * One metric per cell, each with GOAL.md's own words underneath. The criterion
 * line is deliberately not muted into invisibility: it is what turns a number
 * into a pass or a fail, and a reader who skips it is reading trivia.
 */
.dash__metric-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 1rem;
}
.dash__metric { padding: 0.5rem 0; border-top: 1px solid var(--line); }
.dash__metric-label {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.dash__metric-value { margin: 0.15rem 0; font-size: 1.25rem; font-weight: 700; }
.dash__metric-detail { margin: 0; font-size: 0.875rem; }
.dash__metric-criterion {
  margin: 0.35rem 0 0;
  font-size: 0.8125rem;
  font-style: italic;
  color: var(--muted);
}
.dash__unmeasured summary { cursor: pointer; min-height: 44px; padding: 0.6rem 0; font-weight: 600; }
.dash__unmeasured li { margin-bottom: 0.35rem; }
.dash__build { font-size: 0.875rem; }
.dash__audit-why { font-size: 0.9375rem; }

/* The audit card: a rubric, its votes, and three buttons. */
.audit__run--ruled { opacity: 0.75; }
.audit__head { gap: 0.25rem; }
.audit__confidence {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.audit__ruled-mark { text-transform: none; letter-spacing: 0; }
.audit__title { margin: 0; font-size: 1.05rem; }
.audit__who { margin: 0; font-size: 0.875rem; overflow-wrap: anywhere; }
.audit__reason { margin: 0; }
.audit__criteria { list-style: none; margin: 0; padding: 0; }
.audit__criterion { padding: 0.5rem 0; border-top: 1px solid var(--line); }
.audit__criterion--close { border-left: 3px solid var(--accent); padding-left: 0.5rem; }
.audit__criterion-id { margin: 0; font-weight: 600; overflow-wrap: anywhere; }
/* Monospace so a five-vote tally lines up between criteria. */
.audit__votes {
  margin: 0.15rem 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8125rem;
}
.audit__quote {
  margin: 0.25rem 0;
  padding-left: 0.75rem;
  border-left: 3px solid var(--line);
  overflow-wrap: anywhere;
}
.audit__next { margin: 0; font-size: 0.875rem; }
.audit__note-label { font-size: 0.9375rem; }
.audit__form textarea { font-size: 1rem; }
.audit__buttons { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.audit__caveat { font-size: 0.875rem; }

/*
 * The friction entry, as bytes. A <pre> because its destination is a clipboard
 * and then a markdown file — anything rendered would have to be un-rendered by
 * whoever pastes it. pre-wrap so it does not scroll on a phone.
 */
.friction__entry {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin: 0;
  padding: 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.875rem;
}
.friction__source {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.friction__summary { margin: 0; font-size: 1.05rem; }
.friction__detail { margin: 0; font-size: 0.9375rem; }
.friction__preview summary { cursor: pointer; min-height: 44px; padding: 0.6rem 0; }
.friction__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; }
.friction__ready { padding-bottom: 0.5rem; border-top: 1px solid var(--line); }
.friction__why { font-size: 0.875rem; }

/* The student detail page: sections of lists, nothing fancy. */
.student__head { gap: 0.35rem; }
.student__spend, .student__actions, .student__stall { margin: 0; }
.student__sessions { margin: 0; padding-left: 1.25rem; }
.student__session { padding: 0.2rem 0; }
.student__session-title { font-weight: 600; }
.student__submission summary { cursor: pointer; min-height: 44px; padding: 0.6rem 0; }
.student__changed { margin: 0.25rem 0; font-style: italic; }
.student__runs, .student__criteria, .student__conversations,
.student__checkins, .student__tickets {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
}
.student__runs li, .student__criteria li, .student__conversation,
.student__checkins li, .student__ticket {
  padding: 0.5rem 0;
  border-top: 1px solid var(--line);
  overflow-wrap: anywhere;
}
.student__runs p, .student__conversation p, .student__checkins p { margin: 0.15rem 0; }
.student__criterion-id { font-weight: 600; }
.student__quote { margin: 0.25rem 0; padding-left: 0.75rem; border-left: 3px solid var(--line); }
.student__checkin-subject { font-weight: 600; }
.student__ticket-summary { margin: 0; font-weight: 600; }
.student__ticket--breached { border-left: 3px solid var(--accent); padding-left: 0.5rem; }
.student__spend-log summary {
  cursor: pointer;
  min-height: 44px;
  padding: 0.6rem 0;
  font-weight: 600;
}
.student__spend-log ul { list-style: none; margin: 0; padding: 0; }
.student__spend-log li { padding: 0.35rem 0; border-top: 1px solid var(--line); font-size: 0.875rem; }

@media (min-width: 48rem) {
  /* Give the roster back its real table layout where there is room for it. */
  .dash__table thead { display: table-header-group; }
  .dash__table th {
    text-align: left;
    padding: 0.4rem 0.5rem 0.4rem 0;
    font-size: 0.8125rem;
    color: var(--muted);
  }
  .dash__row { display: table-row; }
  .dash__row td {
    display: table-cell;
    padding: 0.5rem 0.5rem 0.5rem 0;
    border-top: 1px solid var(--line);
    vertical-align: top;
  }
  .dash__row td::before { content: none; }
  .dash__row--risk { border-left: none; padding-left: 0; }
  .dash__row--risk td:first-child { border-left: 4px solid var(--accent); padding-left: 0.5rem; }

  .shell__header { padding: 0.75rem 1.5rem; }
  .shell__main { padding: 1.5rem; }
  .shell__aside { padding: 0 1.5rem 1.5rem; }
}

/* Chunk: docs/chunks/instructor_dashboard — the submission, readable. The body
   is the student's own text: pre-wrap so it reads as written, wrap anywhere so
   a pasted command cannot force a horizontal scroll. */
.submission__body {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: 0.9375rem;
  background: none;
  margin: 0;
}
.submission__calls { margin: 0; padding-left: 1.5rem; }
.submission__changed { font-style: italic; }

/* --- scannable students (docs/chunks/scannable_students) -----------------

   The Students page (src/ui/instructor/students.tsx) and the key controls on
   the student detail page. Tokens only, no px widths, no opacity. */

/* Withdraw sits after a waiting invitation's two actions, quieter than them. */
button.kit-action.students__withdraw {
  color: var(--muted);
  border-color: transparent;
  font-weight: 500;
}

/* The progress bar: setup, then one segment per session. Segments share the
   width with flex, so no inline width is needed. */
.students__progress {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.5rem;
}
.students__bar {
  display: inline-flex;
  gap: 0.15rem;
  width: 6rem;
  height: 0.5rem;
}
.students__seg {
  flex: 1 1 0;
  background: var(--line);
  border-radius: 0.15rem;
}
.students__seg--setup { flex-grow: 0.6; }
.students__seg--done { background: var(--good); }
.students__progress-text { white-space: nowrap; }

.students__invite { padding: 0.25rem 0 0.5rem; }

/* --- end scannable students --- */

/* --- setting up (docs/chunks/studentux_setup_stepper) --------------------- */

/*
 * A RAIL AND A PANEL. On a phone the rail stacks above the step, which is the
 * right order: "where am I in this" before "what do I do now", and the whole
 * thing is one column of readable width. From 52rem up they sit side by side,
 * because setting up happens at a desk with a terminal next to the browser and
 * the rail is what keeps five steps from feeling like an unknown number.
 *
 * The widths are fluid on purpose (src/ui/styles.ts § PHONE RULES). The rail's
 * column is a minmax, not a fixed 330px, so a narrow laptop shrinks it instead
 * of pushing the panel off the screen.
 */
.setup { display: grid; gap: var(--gap); align-items: start; }

@media (min-width: 52rem) {
  .setup { grid-template-columns: minmax(14rem, 20rem) minmax(0, 1fr); }
}

.setup__rail {
  padding: 0.5rem;
  background: var(--wash);
  border: 1px solid var(--wash-line);
  border-radius: var(--radius-card);
}

.setup__steps { margin: 0; padding: 0; list-style: none; }
.setup__step-item { margin: 0; }

/* One row. A link when the student can go there, a span when they cannot, and
   the same box either way so the rail does not jump as they advance. */
.setup__step {
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
  min-height: 44px;
  padding: 0.6rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  color: inherit;
  text-decoration: none;
}

a.setup__step:hover { background: var(--bg); }

/* The 1.5px accent border marks "current" here as it does on the session map.
   Color and border, never opacity: a step further down the list is dimmer,
   not fainter (src/ui/styles.ts § kit, rule 1). */
.setup__step--open { background: var(--bg); border-color: var(--accent); }
.setup__step--later .setup__step-title { color: var(--dim); }
.setup__step--later .setup__step-note { color: var(--dim); }

.setup__step-body { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.setup__step-title { font-weight: 600; font-size: 0.9375rem; }
.setup__step-note { font-size: 0.8125rem; color: var(--muted); }

/* Done is a filled circle with a check; current is a numbered ring; ahead is a
   numbered outline. Three shapes for three tenses, so the rail is readable
   without reading it. */
.setup__mark {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border: 2px solid var(--line);
  border-radius: 50%;
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--dim);
}

.setup__mark--done { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
.setup__mark--now { border-color: var(--accent); color: var(--accent); }
.setup__mark--pending { border-color: var(--wash-line); color: var(--muted); }

/* The stuck entry, in the rail rather than at the bottom of the page: the
   moment a person needs it is the moment they have stopped reading downwards
   (DEC-005, said before they ask). */
.setup__stuck {
  margin: 0.5rem 0 0;
  padding: 0.75rem 0.6rem 0.25rem;
  border-top: 1px solid var(--wash-line);
  font-size: 0.9375rem;
  color: var(--muted);
}

/* The panel: one step, top to bottom, in reading order. */
.setup__panel { display: flex; flex-direction: column; gap: 0.75rem; min-width: 0; }

.setup__eyebrow {
  margin: 0;
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.setup__title { margin: 0; }
.setup__lead { margin: 0; }

/* What they are about to see, quoted from the module's own installer. Green
   because it is the good outcome and mono because it is a screen, not prose —
   a student comparing this box to their terminal is doing a character-by-
   character match and the shapes have to agree. */
.setup__expect { display: flex; flex-direction: column; gap: 0.4rem; }
.setup__expect-lead { margin: 0; font-weight: 600; font-size: 0.9375rem; }

.setup__evidence {
  margin: 0;
  padding: 0.75rem 1rem;
  background: var(--good-bg);
  border: 1px solid var(--good-bg);
  border-radius: var(--radius);
  color: var(--good);
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 0.8125rem;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* The gate's expected outcome is on this site, not in a terminal, so it drops
   the monospace face and keeps the box. */
.setup__evidence--page { font-family: inherit; font-size: 0.9375rem; }

.setup__command { margin: 0; }
.setup__command code { font-size: 0.9375rem; }

/* The one-line warning that carries the shown-once rule. Amber, not red: this
   is a thing to be careful about, not a thing that has gone wrong. */
.setup__warn {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.75rem 1rem;
  background: var(--warn-bg);
  border-radius: var(--radius-card);
  color: var(--warn-ink);
}

.setup__warn p { margin: 0; font-size: 0.9375rem; }

/* A link the student is meant to press, sized like a control rather than like
   a word in a sentence. Not the primary action — the kit owns that — and
   deliberately quieter than one. */
.setup__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0.6rem 1.2rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  font-weight: 600;
  text-decoration: none;
}

.setup__foot { margin: 0; font-size: 0.9375rem; }

/* --- scannable student (docs/chunks/scannable_student) ----------------------

   The student header as one row, the home page's folds and stuck row, and
   the session page's step strip, folded contract, exercises and stuck fold.
   Every fold is a <details>. test/scannable_student.test.ts reads this block
   by its markers: tokens for color, no opacity, no px widths, and a 44px
   floor on everything a thumb taps. */

/* The header: the course name and the account menu, one row. */
.shell__header--student {
  flex-wrap: nowrap;
  justify-content: space-between;
  min-height: 56px;
  padding-top: 0.25rem;
  padding-bottom: 0.25rem;
}
.shell__brand-nav { flex: 1 1 auto; min-width: 0; }
.shell__header--student .shell__brand {
  display: block;
  min-height: 44px;
  line-height: 44px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.account { position: relative; flex: 0 0 auto; }
.account__summary,
.account__signin {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  color: var(--fg);
  font-size: 0.9375rem;
  text-decoration: none;
  cursor: pointer;
  list-style: none;
}
.account__summary::-webkit-details-marker { display: none; }
.account__summary::after { content: "▾"; margin-left: 0.35rem; color: var(--muted); }
.account__menu {
  position: absolute;
  right: 0;
  top: calc(100% + 0.25rem);
  z-index: 10;
  min-width: 14rem;
  max-width: 80vw;
  padding: 0.75rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--bg);
}
.account__email { margin: 0 0 0.25rem; color: var(--muted); font-size: 0.875rem; }
.account__menu form { margin: 0; }
.account__logout { min-height: 44px; }

/* Home: the count under the map heading. */
.map__count { margin: 0 0 0.5rem; color: var(--muted); font-size: 0.9375rem; }

/* Home: the stuck row. */
.stuck-row { margin: 0.5rem 0 0; }

/* Home: the account fold. Its summary is the heading and the budget left. */
.keeping__summary {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  min-height: 44px;
  padding: 0.5rem 0;
  cursor: pointer;
}
.keeping__summary .keeping__heading { display: inline; margin: 0; }
.keeping__budget { color: var(--muted); font-size: 0.9375rem; }

/* Session: the three steps. */
.steps {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.375rem;
  margin: 0 0 1rem;
  padding: 0;
  list-style: none;
}
.steps__step { min-width: 0; }
.steps__link {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  min-height: 44px;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  color: var(--dim);
  text-decoration: none;
  font-size: 0.9375rem;
}
.steps__marker {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  border: 1px solid currentColor;
  font-size: 0.8125rem;
  font-weight: 700;
}
.steps__step--done .steps__link { color: var(--good); background: var(--good-bg); border-color: var(--good-bg); }
.steps__step--current .steps__link { color: var(--accent); border-color: var(--accent); font-weight: 600; }
.steps__state {
  position: absolute;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
  height: 0.0625rem;
  width: 0.0625rem;
}

/* Session: the contract, folded once the lab has been opened. */
.contract__summary {
  display: flex;
  align-items: center;
  min-height: 44px;
  cursor: pointer;
  list-style: none;
}
.contract__summary::-webkit-details-marker { display: none; }
.contract__summary::after { content: "Show"; flex: none; white-space: nowrap; margin-left: auto; padding-left: 0.75rem; color: var(--accent); font-weight: 600; font-size: 0.9375rem; }
.contract[open] > .contract__summary::after { content: "Hide"; }
.contract:not([open]) .contract__title { margin: 0; }

/* Session: a part's exercises. */
.exercises { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.exercise { border: 1px solid var(--line); border-radius: var(--radius-card); }
.exercise__summary {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0.5rem 1rem;
  font-weight: 600;
  cursor: pointer;
}
.exercise[open] > .exercise__summary { border-bottom: 1px solid var(--line); }
.exercise__body { padding: 0.75rem 1rem; }
.exercise__body > :last-child { margin-bottom: 0; }
.exercise__see {
  margin: 0 0 1rem;
  padding: 0.75rem 1rem;
  border-left: 4px solid var(--good);
  border-radius: var(--radius);
  background: var(--good-bg);
}
.exercise__see p:last-child { margin-bottom: 0; }

/* Session: the stuck fold in the aside. */
.stuck-fold { border: 1px solid var(--line); border-radius: var(--radius-card); }
.stuck-fold__summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
  padding: 0.5rem 1rem;
  color: var(--accent);
  font-weight: 600;
  cursor: pointer;
}
.stuck-fold[open] > .stuck-fold__summary { border-bottom: 1px solid var(--line); }
.stuck-fold > .ta { border: none; margin: 0; }

/* --- end scannable student --- */

/* --- agent page (docs/chunks/hosted_agent_page) ------------------------------

   The conversation with the hosted agent: the student's requests, the agent's
   words, one card per tool call, the request form, the earlier versions and
   the files. test/ui_phone.test.ts reads this block by its markers: tokens
   for color, no opacity, no px widths, a 44px floor on what a thumb taps.
   Commands, diffs and output wrap; nothing scrolls the page sideways. */

/* The chat window (docs/chunks/hosted_agent_chat): the conversation scrolls
   in its own pane, the request box is pinned under it, and on a wide screen
   the folder, versions and files sit in a column beside it. */
.shell__main > .agent-page--chat { max-width: 76rem; }
.agent-page--chat .page-head { margin-bottom: 0.75rem; }
.agent-layout { display: flex; flex-direction: column; gap: var(--gap); }
.agent-chat {
  display: flex;
  flex-direction: column;
  height: 70vh;
  height: clamp(22rem, calc(100dvh - 10rem), 60rem);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  overflow: hidden;
}
.agent-chat__scroll {
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0.75rem;
}
.agent-chat__scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.agent-chat__working { margin: 0.75rem 0 0; color: var(--accent); }
.agent-chat__latest {
  position: sticky;
  bottom: 0.25rem;
  display: block;
  margin: 0.5rem auto 0;
  min-height: 44px;
  padding: 0 1rem;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--accent-fg);
  border: 0;
  font-weight: 600;
}
.agent-chat__latest[hidden] { display: none; }
.agent-chat__composer { border-top: 1px solid var(--line); padding: 0.5rem 0.75rem; background: var(--bg); }
.agent__buttons.agent-chat__row { align-items: flex-end; margin-top: 0; flex-wrap: nowrap; }
.agent-chat__row button { flex: none; white-space: nowrap; }
.agent-chat__row textarea { flex: 1 1 auto; min-width: 0; max-height: 12rem; resize: none; margin: 0; }
.agent-chat__meta { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0 0.5rem; }
.agent-chat__meta .agent__buttons { margin-top: 0; margin-left: auto; }
.agent-chat__meta .agent__status { margin: 0; font-size: 0.875rem; }
.agent-sr {
  position: absolute;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  width: 1rem;
  overflow: hidden;
  white-space: nowrap;
}
.agent-side .agent__folder { margin-top: 0; padding-top: 0; border-top: 0; }
@media (min-width: 60rem) {
  .agent-layout { display: grid; grid-template-columns: minmax(0, 1fr) 20rem; align-items: start; }
  .agent-side {
    height: clamp(22rem, calc(100dvh - 10rem), 60rem);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

.agent-page__intro { margin: 0 0 var(--gap); font-size: 0.9375rem; }
.agent__log { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.75rem; }

.agent-turn { margin: 0; }
.agent-turn__who {
  position: absolute;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  width: 1rem;
  overflow: hidden;
}
.agent-turn__text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.agent-turn--you { align-self: flex-end; max-width: 85%; }
.agent-turn--you .agent-turn__text {
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-card);
  background: var(--wash);
  border: 1px solid var(--wash-line);
}
.agent-md { white-space: normal; }
.agent-md > * + * { margin-top: 0.5rem; }
.agent-md__p, .agent-md__heading { margin: 0; }
.agent-md__heading { font-weight: 700; }
.agent-md__list { margin: 0; padding-left: 1.25rem; }
.agent-md code { font-size: 0.875em; padding: 0 0.25rem; border-radius: var(--radius); background: var(--wash); overflow-wrap: anywhere; }
.agent-md pre code { padding: 0; background: none; }
.agent-turn--live .agent-md > :last-child::after { content: "\258D"; margin-left: 0.125rem; color: var(--accent); }
@media (prefers-reduced-motion: no-preference) {
  .agent-turn--live .agent-md > :last-child::after { animation: agent-caret 1s steps(2, start) infinite; }
}
@keyframes agent-caret { to { visibility: hidden; } }

.agent-note { margin: 0; padding: 0.5rem 0.75rem; border-left: 4px solid var(--line); color: var(--muted); }
.agent-note:only-child { border-left: 0; text-align: center; padding: 2rem 0.75rem; }

.agent-more .link { min-height: 44px; }

.agent-card {
  margin: 0;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
}
.agent-card--problem { border-color: var(--bad); }
.agent-card__head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0; min-height: 44px; }
.agent-card__title { font-weight: 600; min-width: 0; }
.agent-card__subject { font-weight: 600; }
.agent-card__problem { margin: 0.25rem 0; color: var(--bad); }
.agent-card__pre {
  margin: 0.5rem 0 0;
  padding: 0.5rem;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  overflow-x: visible;
  font-size: 0.875rem;
  max-height: 24rem;
  overflow-y: auto;
}
.agent-card__fold > summary {
  display: flex;
  align-items: center;
  min-height: 44px;
  color: var(--accent);
  font-weight: 600;
  cursor: pointer;
}
.agent-diff { display: block; }
.agent-diff--added { color: var(--good); background: var(--good-bg); }
.agent-diff--removed { color: var(--bad); background: var(--bad-bg); }

.agent__status { margin: 0 0 0.5rem; color: var(--muted); }
.agent[data-busy="true"] .agent__status { color: var(--accent); }
.agent__ask textarea { font-size: 1rem; min-height: 44px; }
.agent__buttons { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; }
.agent__buttons button { min-height: 44px; }
.agent__buttons button:disabled { color: var(--dim); background: transparent; border-color: var(--line); cursor: default; }

.agent__versions, .agent__files { margin-top: var(--gap); }
.agent__versions-help { font-size: 0.9375rem; }
.agent__link { display: inline-flex; align-items: center; min-height: 44px; }

.agent-list { list-style: none; margin: 0 0 var(--gap); padding: 0; }
.agent-list__note { color: var(--muted); padding: 0.5rem 0; }
.agent-version, .agent-file {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.75rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--line);
}
.agent-version__title { margin: 0; flex: 1 1 12rem; min-width: 0; }
.agent-version__when { margin: 0; color: var(--muted); font-size: 0.875rem; }
.agent-version__back { min-height: 44px; }
.agent-file__path { flex: 1 1 12rem; min-width: 0; }
.agent-file__links { display: inline-flex; flex-wrap: wrap; gap: 0.75rem; }
.agent-file__links a { display: inline-flex; align-items: center; min-height: 44px; }
.agent-file__heading { font-size: 1.15rem; }
.agent-link { margin: 0.5rem 0; }

/* Your folder (docs/chunks/hosted_folder_link): the linked folder's state,
   what moved, what was skipped, and the upload form and zip for every
   browser. */
.agent__folder { margin-top: var(--gap); padding-top: var(--gap); border-top: 1px solid var(--line); }
.agent__folder-heading { font-size: 1.15rem; margin: 0 0 0.5rem; }
.agent__folder-subheading { font-size: 1rem; margin: var(--gap) 0 0.25rem; }
.agent__folder-state { margin: 0.5rem 0; font-weight: 600; overflow-wrap: anywhere; }
.agent__folder-other { margin-top: 0.5rem; }
.agent-folder-move { padding: 0.375rem 0; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; }
.agent-folder-move__label { color: var(--muted); }
.agent__upload { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0.5rem 0; }
.agent__upload label { flex: 1 1 100%; }
.agent__upload input[type="file"] { font-size: 1rem; min-height: 44px; max-width: 100%; }
.agent__upload button { min-height: 44px; }

/* --- end agent page --- */