/* Properize — the teacher's app. properai.us only.
 *
 * SELF-CONTAINED ON PURPOSE. It shares the brand with prosperai.us and shares
 * no FILE with it: a stylesheet imported by both is a component imported by
 * both, and the two products make opposite claims. Same look, separate copy.
 *
 * No webfont. Not for the reason the marketing pages have — those self-host to
 * avoid a request to Google on a page promising no third party. Here it is
 * simply that Georgia and the system UI stack are already right for reading,
 * and a font request is latency in front of a teacher with a class waiting.
 */

:root {
  --ink: #1c1a17;
  --ink-2: #5c554b;
  /* MEASURED AT 3.96:1 AGAINST THE PAGE AND RAISED TO CLEAR AA. This is
     the colour of `.hint` and `.slip-link` — 'Tap to add or remove',
     'The text and the versions are kept and shared', every student's
     link — so it carries instructions rather than decoration, and 4.5
     is the floor for text under 18.66px. It was picked to look quiet
     and went one step past quiet into unreadable, which is the usual
     way this happens: nothing looks broken. */
  --ink-3: #6b6459;
  --bg: #fdfcfa;
  --panel: #ffffff;
  --line: #e4ded4;
  --line-2: #efeae2;
  --brand: #9c2130;
  --brand-ink: #ffffff;
  --accent: #1f5f5b;
  --ok-bg: #e8f3ec;
  --ok-ink: #1c5433;
  --warn-bg: #fdf1e0;
  --warn-ink: #7a4a12;
  --err-bg: #fdeceb;
  --err-ink: #8d2620;
  --radius: 10px;
  --font-display: Georgia, "Iowan Old Style", "Times New Roman", serif;
  --font-body: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --shadow: 0 1px 2px rgba(28, 26, 23, .05), 0 4px 14px rgba(28, 26, 23, .05);
}

:root:not([data-theme="light"]) { color-scheme: light; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ink: #f2ede5;
    --ink-2: #b9b1a5;
    --ink-3: #8d8579;
    --bg: #161513;
    --panel: #1f1d1a;
    --line: #35312b;
    --line-2: #2a2724;
    --brand: #c85562;
    --accent: #6fb3ad;
    --ok-bg: #17301f;
    --ok-ink: #9ad9b0;
    --warn-bg: #3a2b16;
    --warn-ink: #f0c98a;
    --err-bg: #3a1d1b;
    --err-ink: #f0a49d;
    --shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 4px 14px rgba(0, 0, 0, .3);
    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --ink: #f2ede5;
  --ink-2: #b9b1a5;
  --ink-3: #8d8579;
  --bg: #161513;
  --panel: #1f1d1a;
  --line: #35312b;
  --line-2: #2a2724;
  --brand: #c85562;
  --accent: #6fb3ad;
  --ok-bg: #17301f;
  --ok-ink: #9ad9b0;
  --warn-bg: #3a2b16;
  --warn-ink: #f0c98a;
  --err-bg: #3a1d1b;
  --err-ink: #f0a49d;
  --shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 4px 14px rgba(0, 0, 0, .3);
  color-scheme: dark;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.55;
  -webkit-text-size-adjust: 100%;
}

/* `[hidden]` is a UA rule and loses to any author `display`. Every element
   toggled by `.hidden` needs this or it stays on screen — the cascade trap
   that left an API-key field visible in Demo mode on the other product. */
[hidden] { display: none !important; }

.wrap { max-width: 940px; margin: 0 auto; padding: 0 20px; }

/* ------------------------------------------------------------------ header */
.bar {
  border-bottom: 1px solid var(--line);
  background: var(--panel);
  position: sticky; top: 0; z-index: 5;
}
.bar-in { display: flex; align-items: center; gap: 12px; height: 60px; }
.brand {
  display: inline-flex; align-items: center; gap: 8px;
  text-decoration: none; color: var(--ink);
}
.mark {
  display: inline-grid; place-items: center;
  width: 26px; height: 26px; border-radius: 7px;
  background: var(--brand); color: var(--brand-ink);
  font-size: 15px; font-weight: 700;
}
.brand-name { font-family: var(--font-display); font-size: 21px; letter-spacing: .01em; }
.tag {
  color: var(--ink-3); font-size: 13px;
  border-left: 1px solid var(--line); padding-left: 12px;
}

/* -------------------------------------------------------------------- type */
h1 {
  font-family: var(--font-display);
  font-size: clamp(26px, 4.2vw, 38px);
  line-height: 1.18; margin: 32px 0 10px; font-weight: 600;
}
h2 {
  font-family: var(--font-display);
  font-size: clamp(20px, 3vw, 25px);
  margin: 0 0 10px; font-weight: 600;
}
h3 { font-size: 15px; margin: 0; font-weight: 600; }
.lede { color: var(--ink-2); font-size: 17px; margin: 0 0 26px; max-width: 60ch; }
/* The cost answer, on the first screen. Smaller than the lede because it is a
   reassurance rather than the pitch, and directly under it because the
   question it settles arrives at the same moment. */
.lede-note { color: var(--ink-2); font-size: 14px; margin: -18px 0 26px; max-width: 60ch; }
.lbl { display: block; font-size: 13px; font-weight: 600; margin: 18px 0 6px; }
.opt { font-weight: 400; color: var(--ink-3); }
.hint { color: var(--ink-3); font-size: 13px; margin: 6px 0 0; }
.q { color: var(--accent); }

/* What came back from pulling a link in: where it came from, and whether it
   worked. A tinted strip rather than a plain line, because it is reporting on
   text the teacher did not write and is being asked to check.

   `[hidden]` is restated because this element is toggled by the `hidden`
   PROPERTY and given a `display` here. `[hidden] { display: none }` is a UA
   rule and `.source-note { display: block }` would be an author rule — author
   beats UA whatever the specificity — which is exactly how the API key field
   stayed on screen in Demo mode one product over. There is no `display` on
   this rule today; the guard is here so adding one later cannot break it. */
.source-note {
  margin: 8px 0 0;
  padding: 8px 11px;
  border-radius: var(--radius);
  background: var(--line-2);
  color: var(--ink-2);
  border: 1px solid var(--line);
}
.source-note.source-bad { background: var(--err-bg); color: var(--err-ink); border-color: transparent; }
.source-note[hidden] { display: none; }

/* ------------------------------------------------------------------ fields */
.field {
  width: 100%;
  font: inherit; color: var(--ink);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 11px 13px;
}
.field:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.area { resize: vertical; min-height: 190px; font-size: 15px; line-height: 1.6; }
.area.dropping { border-color: var(--accent); background: var(--line-2); }

/* The label and its "Open a file…" share a line; the label keeps its own top
   margin so the row sits where a bare label would have. */
.lbl-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.lbl-row .lbl { margin-bottom: 6px; }
.linky {
  font: inherit; font-size: 13px; font-weight: 600;
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--accent); text-decoration: underline; text-underline-offset: 2px;
  white-space: nowrap;
}
.linky:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.levels { border: 0; padding: 0; margin: 22px 0 0; }
.levels legend { padding: 0; }

.chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.chip {
  font: inherit; font-size: 14px;
  border: 1px solid var(--line);
  background: var(--panel); color: var(--ink-2);
  border-radius: 999px;
  /* A pill only reads as a pill on one line — let the label wrap and the
     radius resolves against a two-line box as a lozenge. */
  white-space: nowrap;
  padding: 7px 15px; cursor: pointer;
}
.chip:hover { border-color: var(--ink-3); }
.chip[aria-pressed="true"] {
  background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600;
}
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.more { margin: 22px 0 0; border-top: 1px solid var(--line-2); padding-top: 12px; }
.more summary { cursor: pointer; font-size: 14px; font-weight: 600; color: var(--ink-2); }
.more-in { padding: 4px 0 2px; }
.checks { display: flex; flex-wrap: wrap; gap: 8px 22px; margin-top: 14px; font-size: 14px; }
.checks label { display: inline-flex; align-items: center; gap: 7px; }

/* ----------------------------------------------------------------- buttons */
.go { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 26px 0 8px; }
.btn {
  font: inherit; font-size: 15px; font-weight: 600;
  border-radius: 999px; border: 1px solid transparent;
  padding: 11px 26px; cursor: pointer;
  white-space: nowrap;
}
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn-primary { background: var(--brand); color: var(--brand-ink); }
.btn-primary:hover { filter: brightness(1.07); }
/* A FADED BRAND COLOUR READS AS BROKEN, NOT AS DISABLED. `opacity: .55` over
   the red gave a washed-out pink that looks like a rendering fault on the one
   control the page is about — and it is the first thing anyone sees, because
   the button starts disabled. A neutral surface reads as "not yet", which is
   what it means, and the note beside it says what is missing. */
.btn-primary[disabled] {
  background: var(--line-2);
  color: var(--ink-3);
  border-color: var(--line);
  cursor: default;
  filter: none;
}
.btn-ghost { background: transparent; color: var(--ink-2); border-color: var(--line); }
.btn-ghost:hover { border-color: var(--ink-3); color: var(--ink); }

/* ---------- the middle weight ----------
   TWO SOLID BRAND BUTTONS ON ONE PAGE MEANS NEITHER IS PRIMARY. The roster
   carries "Save the class" and "Save the standards", which are two independent
   saves rather than one call to action, and in solid red they competed with
   each other and shouted over a page that is otherwise a quiet form. Plain
   `.btn-ghost` would go too far the other way: saving is more consequential
   than "Copy the list" and "Print a slip each", which are ghosts, and three
   identical-looking controls would flatten that difference.

   So: a brand-coloured BORDER and an ordinary ink label.

   **The label is `--ink` and not `--brand`, and that is a contrast decision
   rather than a stylistic one.** Inverting a solid button exposes the brand
   colour's own contrast for the first time — white on red was never in
   question, but `--brand` as TEXT measures 7.66:1 in the light theme and
   **4.29:1 in the dark one**, under the 4.5 floor for a 15px label. Nothing
   would have looked broken. The border carries the brand instead, where a
   contrast ratio does not apply, and the label cannot fail in either theme.

   The hover is a neutral wash rather than a brand fill for the same reason:
   filling with `--brand` would put `--brand-ink` on `#c85562` in dark mode,
   which is the same trap one step along. */
.btn-secondary {
  background: transparent;
  color: var(--ink);
  border-color: var(--brand);
}
.btn-secondary:hover { background: var(--line-2); }
.btn-secondary[disabled] {
  background: var(--line-2);
  color: var(--ink-3);
  border-color: var(--line);
  cursor: default;
}

/* ----------------------------------------------------------------- results */
.results-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; margin: 32px 0 4px;
}
.results-actions { display: flex; gap: 8px; }

.ledger {
  background: var(--ok-bg); color: var(--ok-ink);
  border-radius: var(--radius); padding: 10px 14px;
  font-size: 14px; margin: 12px 0 0;
}

.results { display: grid; gap: 16px; margin: 20px 0 40px; }

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
/* `margin-left: auto` on the copy button pushes it right, and when the row
   wraps it takes that auto margin onto the second line — so at 390px the head
   rendered as [Grade 3][pill] / [ ...gap... ][Copy link], a stranded control
   with an empty half-row above it that reads as a layout fault. A grid puts
   the label and pill in the first column and the button in a second that
   holds its own place, wrapped or not. */
.card-head {
  display: grid; grid-template-columns: 1fr auto; align-items: center;
  gap: 8px 10px;
  padding: 12px 16px; border-bottom: 1px solid var(--line-2);
}
.card-head .card-title {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; min-width: 0;
}
.pill {
  font-size: 12px; font-weight: 600;
  border-radius: 999px; padding: 3px 10px; white-space: nowrap;
}
.pill-free { background: var(--ok-bg); color: var(--ok-ink); }
.pill-new { background: var(--warn-bg); color: var(--warn-ink); }
.pill-err { background: var(--err-bg); color: var(--err-ink); }
.spinner {
  width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid var(--line); border-top-color: var(--accent);
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 3s; } }

/* Pushed to the right so it does not sit between the level and its status —
   the pill is what a teacher reads, and a control wedged in front of it makes
   the row scan as three unrelated things. */
.linkbtn {
  /* No `margin-left: auto` — the grid column above places it now, and the
     auto margin was what stranded it on a wrapped row. */
  font: inherit; font-size: 13px; font-weight: 600;
  color: var(--accent); background: transparent;
  border: 1px solid var(--line); border-radius: 999px;
  padding: 5px 13px; cursor: pointer; white-space: nowrap;
}
.linkbtn:hover { border-color: var(--accent); }
.linkbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.card-body { padding: 4px 18px 16px; }
.card-body p {
  font-family: var(--font-display);
  font-size: 16.5px; line-height: 1.65; margin: 14px 0;
}

/* ---------- a clamped version ----------
   `max-height` rather than `-webkit-line-clamp`, because the clamp is applied
   to a container of paragraphs and line-clamp only counts lines inside one
   block. The fade is a gradient over the last rows so the cut does not land
   mid-sentence looking like the text ended there — the commonest way a
   truncation gets mistaken for a bug.

   `.clamped` is added by JS only when the body is actually taller than this,
   measured after layout; a card that fits is left alone and gets no button. */
.card-body.clamped {
  max-height: 260px; overflow: hidden;
  /* FULLY TRANSPARENT BEFORE THE CUT, not at it. The first version faded to
     nothing exactly at the clamp and pulled the button up by 6px, so "Read
     the whole thing" was drawn across the last ghosted line and read as a
     rendering fault rather than as a control. The fade now finishes 30px
     early and the button sits below it with real space. */
  -webkit-mask-image: linear-gradient(to bottom, #000 165px, transparent 230px);
          mask-image: linear-gradient(to bottom, #000 165px, transparent 230px);
}
.card-more {
  display: block; margin: 2px 18px 14px;
  font-size: 13.5px;
}

/* THE PRINTED VERSION IS NEVER CLAMPED. A teacher printing four versions is
   printing them to hand out, and half a passage is worse than none. The
   control itself has nothing to do on paper. */
@media print {
  .card-body.clamped {
    max-height: none; overflow: visible;
    -webkit-mask-image: none; mask-image: none;
  }
  .card-more { display: none; }
}
.changed {
  margin: 0; padding: 12px 18px 16px;
  border-top: 1px solid var(--line-2);
  font-size: 13.5px; color: var(--ink-2);
}
.changed ul { margin: 6px 0 0; padding-left: 20px; }
.changed li { margin: 3px 0; }

.what { border-top: 1px solid var(--line-2); margin: 44px 0 0; padding: 26px 0 10px; }
.what p { color: var(--ink-2); max-width: 68ch; }

/* ---------- "What this stores", collapsed ----------
   **`display: flex` ON A `<summary>` DELETES THE DISCLOSURE TRIANGLE**, and
   the first version of this rule did exactly that — with a comment beside it
   saying the triangle must be left alone because it is the only thing telling
   a reader the section opens. The marker is drawn by `display: list-item`, so
   any other display value removes it; Chrome gives no warning and the section
   simply reads as a heading with a sentence under it, which is worse than the
   wall it replaced because now the rest is invisible AND unreachable-looking.

   A screenshot cannot settle this — a missing triangle and a small one look
   alike at a glance. What settles it is that the summary's left edge and its
   first child's left edge were the same number (183 and 183), so nothing was
   being drawn beside the words.

   So the summary stays `list-item` and the column lives in a wrapper. */
.what > summary {
  cursor: pointer; padding: 2px 0 8px;
  display: list-item;                 /* NEVER flex/block/grid — see above */
  list-style-position: outside;
}
.what > summary::marker { color: var(--ink-3); }
.what-sum {
  display: inline-flex; flex-direction: column; gap: 4px;
  /* A flex item defaults to `min-width: auto` and refuses to shrink below its
     content; the teaser is a long sentence and this is what keeps it inside a
     320px viewport. */
  min-width: 0; max-width: 68ch; vertical-align: top;
}
.what > summary h2 { margin: 0; }
.what-teaser { color: var(--ink-2); font-size: 14.5px; }
.what > summary:hover h2 { color: var(--accent); }
/* Once open, the first paragraph should not crowd the summary. */
.what[open] > summary { margin-bottom: 6px; }

.foot {
  border-top: 1px solid var(--line-2); margin-top: 30px;
  padding: 20px 0 40px; color: var(--ink-3); font-size: 13px;
}

/* ------------------------------------------------------- a class (Phase 2b) */

/*
 * `[hidden]` IS A UA RULE AND `.chips { display: flex }` IS AN AUTHOR RULE,
 * so the author one wins whatever the specificity and `el.hidden = true` does
 * NOTHING to a flex container. That is not a hypothetical: CLAUDE.md records
 * the same trap leaving the API key field on screen in the one mode built to
 * need no key. Both level renderings are toggled with `.hidden`, so both need
 * this line, and `.source-note[hidden]` above is the same fix already applied
 * once on this page.
 */
.chips[hidden] { display: none; }

.classbar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 0 0 22px; padding: 10px 14px;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--line-2); font-size: 14px; color: var(--ink-2);
}
.classbar-on { border-color: var(--accent); background: transparent; color: var(--ink); }
.classbar-on #classBarText { font-weight: 600; }

/* A level the teacher did not choose. Flat, not a button: it is a statement
   about the roster, and giving it a pressed state would invite a tap that does
   nothing. */
.chip-fixed {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--accent); border-radius: 999px;
  padding: 7px 14px; font-size: 14px; background: transparent; color: var(--ink);
}

.ledger-class { color: var(--ink-2); }
.ledger-class.source-bad { color: var(--err-ink); }

/* The correction row at the foot of a version card. Quiet by default — it is
   an offer, not a demand, and three loud rows under three versions would read
   as the app asking to be graded. */
.verdict {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 18px 16px; border-top: 1px solid var(--line-2); margin-top: 4px;
}
.verdict .field { flex: 1 1 220px; min-width: 0; margin: 0; }
.verdict label { flex: 0 0 auto; }
.verdict .btn { padding: 8px 18px; font-size: 14px; }

/* ------------------------------------------------------- the class page */

.linkcard {
  border: 1px solid var(--accent); border-radius: 12px;
  padding: 16px 18px; margin: 0 0 26px;
}
.linkcard h2 { font-family: var(--font-display); font-size: 19px; margin: 0 0 6px; }
.linkcard p { color: var(--ink-2); font-size: 14px; margin: 0 0 12px; max-width: 62ch; }
.linkrow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.linkrow code {
  flex: 1 1 260px; min-width: 0; overflow-x: auto; white-space: nowrap;
  font-size: 13px; padding: 8px 10px;
  background: var(--line-2); border-radius: 7px; color: var(--ink-2);
}

.roster { display: grid; gap: 8px; margin: 8px 0 4px; }
.row { display: flex; align-items: center; gap: 8px; }
.row .field { margin: 0; }
.row input[type="text"] { flex: 1 1 auto; min-width: 0; }
.field-inline { width: auto; flex: 0 0 auto; }
.inline { font-size: 14px; color: var(--ink-2); }
/* ---------- the destructive row action ----------
   `.row-x` is only ever "Remove" (a student) and "Take it back" (an
   assignment), so it can be styled as the destructive variant without
   touching the "Copy" and "Copy link" buttons that share `.linkbtn`.

   Six bordered teal pills stacked down the right edge of the roster carried
   the same visual weight as the six name fields beside them — and they are
   the one control on that page nobody is looking for. Teal is also the wrong
   signal: `--accent` reads as "go" everywhere else here.

   Quieter, not hidden. The word stays, at `--ink-3`, which clears AA in both
   themes (5.85 light, 4.61 dark) — a destructive control that has to be
   hunted for is its own kind of bad. The border is already there and merely
   transparent, so revealing it on hover shifts no layout, and the colour
   moves to the error ink at the moment the pointer is on it. */
.row-x {
  flex: 0 0 auto;
  background: transparent;
  color: var(--ink-3);
  border-color: transparent;
}
.row-x:hover, .row-x:focus-visible {
  color: var(--err-ink);
  border-color: currentColor;
}
.row-x:focus-visible { outline: 2px solid var(--err-ink); outline-offset: 2px; }

.links { display: grid; gap: 8px; margin: 14px 0 6px; }
/*
 * `min-width: 0` IS THE WHOLE FIX, AND WITHOUT IT THIS PAGE OVERFLOWED EVERY
 * PHONE. A grid item's default is `min-width: auto` — it refuses to shrink
 * below its own content — and this one's content is a `white-space: nowrap`
 * URL. So the slip was **390px wide inside a 320px viewport**, taking the page
 * with it, and `overflow-x: auto` on the code inside could not help because the
 * box around it had already grown. Measured in Chrome at 320/360/414 (105, 65
 * and 11 pixels over); no assertion in this repository can see it, because
 * jsdom has neither layout nor media queries.
 */
.slip {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  border: 1px solid var(--line-2); border-radius: 9px; padding: 9px 12px;
  min-width: 0;
}
/* ---------- section rhythm on the roster ----------
   MEASURED BEFORE IT WAS TOUCHED, because "it looks cramped" is not something
   to act on from a feeling: the gap above every one of the five headings was
   0, 8, 8, 8 and 0 pixels, with `margin-top: 0` on all of them. So a
   full-width red Save button sat directly against the next section's heading,
   and — the part that makes it a grouping error rather than a spacing one —
   the button belongs to the section ABOVE while reading as though it
   introduces the one below. Five sections ran together as one form.

   `.linkcard` is excluded: it is already a bordered panel, and a rule above a
   box draws a second line beside its own border. */
#classStep > section:not(.linkcard) {
  margin-top: 40px; padding-top: 30px; border-top: 1px solid var(--line);
}
/* A section explaining a control should sit closer to it than to the previous
   section — the lede under each heading is part of the same thought. */
#classStep > section > h2 { margin-bottom: 6px; }
#classStep > section > h2 + .lede,
#classStep > section > h2 + p { margin-top: 0; }

.slip-name { font-weight: 600; flex: 0 0 auto; }
/* ONE LINE, TRUNCATED — never a scroller and never three wrapped lines.
   A teacher does not read this string; the Copy button beside it is the whole
   affordance, and the NAME is what tells one row from another. On a phone it
   was taking two and three lines of monospace per student, so six students
   made a wall of URLs. `overflow-x: auto` was the previous answer and this
   project already records why that is wrong at phone width: a one-line sliver
   in a scroller nobody discovers. Truncation says the same thing in the space
   available and gets out of the way.

   The print rule below deliberately overrides all of this — on a printed slip
   the URL is the point, and it must wrap and be complete. */
.slip-link {
  flex: 1 1 200px; min-width: 0;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-size: 12.5px; color: var(--ink-3);
}

.assigned { display: grid; gap: 8px; margin: 10px 0 6px; }
/* Same `min-width: 0` as `.slip`, for the same reason: a grid item holding a
   long title will not shrink on its own. */
.assign-row {
  border: 1px solid var(--line-2); border-radius: 9px; padding: 9px 12px;
  min-width: 0;
}
.assign-title { flex: 1 1 auto; font-weight: 600; min-width: 0; }

/* ------------------------------------------------------------------- print */
@media print {
  .bar, .go, .results-actions, .what, .foot, .more, #composeStep,
  .linkbtn { display: none !important; }
  body { background: #fff; }
  .card { break-inside: avoid; border-color: #ccc; box-shadow: none; }
  .ledger { display: none; }

  /*
   * ONE SLIP PER STUDENT, and everything else off the page.
   *
   * `body.printing-slips` is added for the duration of `window.print()` — a
   * class on the body rather than a second stylesheet, so there is one place
   * the rules live and no second file to keep in step. What comes out is a
   * name and a link, big enough to read and hand over, which is the form a
   * teacher gives to a nine-year-old.
   */
  .printing-slips main > section > *:not(#linksStep) { display: none !important; }
  .printing-slips #linksStep > *:not(#studentLinks) { display: none !important; }
  .printing-slips .slip {
    break-inside: avoid; display: block; border: 1px dashed #999;
    padding: 18px 20px; margin: 0 0 10px;
  }
  .printing-slips .slip-name { font-size: 19px; display: block; margin-bottom: 6px; }
  .printing-slips .slip-link { font-size: 13px; white-space: normal; overflow: visible; color: #000; }
}

/* A narrow-screen override must come AFTER the rule it overrides — being
   inside a media query confers no specificity. */
@media (max-width: 560px) {
  .bar-in { height: 54px; }
  .tag { display: none; }
  .go { gap: 10px; }
  .btn { width: 100%; }
  .results-actions { width: 100%; }
  .results-actions .btn { flex: 1; }
  /* A NAME, A LEVEL AND A REMOVE BUTTON DO NOT FIT ACROSS 320px, and the
     failure mode is the one CLAUDE.md warns is invisible to an overflow check:
     the name input shrinks to a sliver rather than overflowing anything. So the
     row stops being a row. `align-items: stretch` goes with it, because the
     base rule centres and in a column that centres HORIZONTALLY — which would
     shrink each control straight back to its content width. */
  /*
   * ONCE A ROW WRAPS, IT HAS TO SAY WHICH NAME THE LEVEL BELONGS TO. Stacked
   * with no grouping, "Grade 4" sits between Ava's name and Milo's and is
   * genuinely ambiguous — a mis-set level on a roster is a child handed the
   * wrong version of everything, which is the one mistake on this page that
   * does not announce itself. Seen in a 360px screenshot; the measurements were
   * already clean, which is the standing lesson about overflow checks.
   */
  .row {
    flex-wrap: wrap; align-items: stretch; gap: 8px;
    border: 1px solid var(--line-2); border-radius: 10px; padding: 8px;
  }
  .row input[type="text"] { flex: 1 1 100%; }
  .field-inline { flex: 1 1 auto; width: auto; }
  /* ~~A URL WRAPS RATHER THAN SCROLLS ON A PHONE.~~ **REPLACED 2026-08-19,
     and the old reasoning is kept because it was right about the thing it
     rejected.** It said: `nowrap` + `overflow-x: auto` is fine on a desktop
     row where a horizontal nudge is cheap, and at 320px it is a one-line
     sliver inside a scroller nobody discovers — so let it wrap. True, and it
     traded a sliver for a wall: six students at two and three wrapped lines of
     monospace each, which is most of the screen given over to strings nobody
     reads. The note's own last sentence is the argument against it — *copying
     is a button, so the text only has to be legible* — and one truncated line
     is legible as "this is a link", which is all the row has to say. The NAME
     is what tells one row from another.

     Truncation is a third option the old note did not weigh: no scroller, no
     wall. The full URL is still printed in full on a slip, where it is the
     point, and `@media print` overrides all of this. */
  .slip-link, .linkrow code {
    flex-basis: 100%;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /*
   * TWO PILLS DO NOT FIT ACROSS A PHONE ONCE THEIR LABELS GROW, and `.btn` is
   * `white-space: nowrap` (deliberately — a wrapped pill is a lozenge), so a
   * flex item cannot shrink below its text. "Copy the list" and "Print a slip
   * each" wanted 162px each against 280px of row. Index's own Print / Start
   * over are short enough to have hidden this for months, which is the standing
   * lesson: **a button label is a layout input.** Stacked, one per line, which
   * is what `.btn { width: 100% }` above already does for a lone button.
   */
  .results-actions { flex-wrap: wrap; }
  .results-actions .btn { flex: 1 1 100%; }
}
