/* The school system's stylesheet — a sibling of the file app's, not a copy of
   it and not a link to it.

   Two applications on two hostnames cannot share a stylesheet without one
   fetching an asset from the other, which is the sort of tie this project has
   gone out of its way not to have. So the tokens below are the same palette
   deliberately — a teacher moving between the two should feel one school
   rather than two products — and everything after them is only what these
   pages actually use.

   No theme toggle. The file app has one because a smart-board is switched
   between a lit classroom and a dark hall; this is read on a counter PC and a
   phone, both of which already know which theme their owner wants. A media
   query needs no JavaScript, which keeps this application at zero scripts
   until the scanner arrives. */

:root {
  --radius: 8px;

  --bg: #f6f7f9;
  --surface: #ffffff;
  --border: #d8dce2;
  --text: #1c2024;
  --muted: #5f6b76;
  --accent: #1b5e9e;
  --good: #1e7a44;
  --warn: #8a5a00;
  --error: #a32020;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #16191d;
    --surface: #1f242a;
    --border: #333a42;
    --text: #e8ebee;
    --muted: #9aa5b1;
    --accent: #6aa9e0;
    --good: #5cc188;
    --warn: #d9a441;
    --error: #e46a6a;
  }
}

* { box-sizing: border-box; }

/* `hidden` has to actually hide, and by default it does not.
   The UA rule is `[hidden] { display: none }`, and **any** author rule that
   sets `display` beats it — author styles win over the user agent's whatever
   the specificity. `.scanner`, `.scan-result` and `.scan-log` all set one, so
   the camera panel rendered in full on exactly the browsers it is hidden from:
   a phone with no decoder showed «Увімкнути камеру» and a button that did
   nothing. Found by looking at the page rather than by a test, which is why
   TestAnElementMarkedHiddenIsActuallyHidden now exists.
   The !important is deliberate and is the only one in this file: it is a UA
   default being restored, not a preference being asserted. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 16px/1.5 system-ui, "Segoe UI", Roboto, sans-serif;
}

/* ---- Frame ---- */

.topbar {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.75rem 1.25rem;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 700;
  font-size: 1.1rem;
  text-decoration: none;
  color: var(--text);
}
.brand-mark { display: block; flex: none; }

.wrap {
  max-width: 60rem;
  margin: 0 auto;
  padding: 1.5rem 1.25rem 3rem;
}

h1 { font-size: 1.5rem; margin: 0 0 0.75rem; }
h2 { font-size: 1.05rem; margin: 0 0 0.6rem; }
a { color: var(--accent); }

/* ---- Panels ---- */

.panels {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: 1rem;
}

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem 1.15rem;
}

.stats {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.35rem 1rem;
  margin: 0;
}
.stats dt { color: var(--muted); }
.stats dd { margin: 0; overflow-wrap: anywhere; }

/* A file path is the one value here that is long, exact, and read character by
   character when something is wrong. */
.path { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-size: 0.9em; }

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

.notice {
  color: var(--muted);
  font-size: 0.9rem;
  margin: 0.75rem 0 0;
}

.verdict {
  display: inline-block;
  border-radius: var(--radius);
  font-weight: 600;
  margin: 0 0 0.25rem;
  padding: 0.35rem 0.75rem;
}
.verdict-good {
  background: color-mix(in srgb, var(--good) 14%, transparent);
  color: var(--good);
}

.problem {
  border-left: 3px solid var(--error);
  color: var(--error);
  margin: 0.75rem 0 0;
  padding-left: 0.75rem;
}

/* ---- The gate ---- */

/* Centred, and it has to be said: max-width alone leaves the card hard
   against the left edge of a wide screen, which reads as a broken page rather
   than a narrow one. Sign-in and the gate are the only two screens that are a
   single small card on an otherwise empty page. */
.gate { max-width: 26rem; margin-inline: auto; }

/* ...and centred in the page rather than parked under the header. Sign-in and
   the gate are the only two screens that are a single small card on an
   otherwise empty page, and :has keeps that from touching any other .wrap.

   min-height, never height: a short screen (a phone held sideways, the
   on-screen keyboard up) then scrolls past the button instead of clipping it. */
.wrap:has(> .gate) {
  align-content: center;
  display: grid;
  min-height: calc(100vh - 9rem);
}

.gate-form {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  margin-top: 1rem;
}

.field { display: flex; flex-direction: column; gap: 0.3rem; }
.field span { color: var(--muted); font-size: 0.9rem; }
.field input {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font: inherit;
  padding: 0.6rem 0.7rem;
}
.field input:focus-visible { border-color: var(--accent); outline: 2px solid var(--accent); }

/* Large enough to be pressed on a phone held in one hand at a counter. */
.button {
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  color: #ffffff;
  cursor: pointer;
  font: inherit;
  font-weight: 600;
  padding: 0.65rem 1.1rem;
}
.button:hover { filter: brightness(1.08); }

/* ---- The roster screens ----

   Added with stage 1. The vocabulary is deliberately small — a table, a panel,
   a form, a couple of tags — because these are screens a dozen members of staff
   use a few times a term, and every class here has to earn a rule that
   TestEverySchoolTemplateClassIsStyled will insist on. */

/* The bar. `on` marks the tab being looked at. */
.navlink {
  border-radius: var(--radius);
  color: var(--accent);
  padding: 0.3rem 0.55rem;
  text-decoration: none;
  white-space: nowrap;
}
.navlink:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); }
.navlink.on {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  font-weight: 600;
}

.tabs { display: flex; gap: 0.4rem; margin: 0 0 1rem; }

/* The way back up. One line, because the pages past a list have no breadcrumb
   of their own to climb. */
.crumb { margin: 0 0 0.4rem; }
.crumb a { color: var(--muted); font-size: 0.9rem; }

/* The warning the roster screens carry until sign-in lands. Louder than a
   notice on purpose: it is a statement about who can edit a roster of named
   minors, not a hint. */
.warn {
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  border-left: 3px solid var(--warn);
  border-radius: var(--radius);
  color: var(--text);
  margin: 0 0 1.25rem;
  padding: 0.7rem 0.9rem;
}

/* ---- Tables ---- */

.table {
  border-collapse: collapse;
  width: 100%;
}
.table th,
.table td {
  border-bottom: 1px solid var(--border);
  padding: 0.5rem 0.6rem;
  text-align: left;
  vertical-align: middle;
}
.table th {
  color: var(--muted);
  font-size: 0.85rem;
  font-weight: 600;
}
.table tbody tr:last-child td { border-bottom: 0; }
.table .num { text-align: right; }

/* A table on a phone scrolls inside its panel rather than pushing the page
   sideways. */
.panel:has(.table) { overflow-x: auto; }

.empty {
  color: var(--muted);
  padding: 0.6rem 0;
}

/* ---- Lists ---- */

.list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.list li {
  border-bottom: 1px solid var(--border);
  padding: 0.4rem 0;
}
.list li:last-child { border-bottom: 0; }

/* A class list is read aloud against a printed register, so it is numbered. */
.list.numbered {
  counter-reset: roster;
  list-style: none;
}
.list.numbered li { counter-increment: roster; }
.list.numbered li::before {
  color: var(--muted);
  content: counter(roster) ".";
  display: inline-block;
  min-width: 1.8rem;
}

.tag {
  background: color-mix(in srgb, var(--muted) 18%, transparent);
  border-radius: 999px;
  color: var(--muted);
  font-size: 0.78rem;
  padding: 0.1rem 0.5rem;
  white-space: nowrap;
}

/* ---- Forms ---- */

.form {
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
  margin-top: 0.9rem;
}

/* Fields side by side, wrapping on a phone rather than shrinking to nothing. */
.form-row {
  align-items: flex-end;
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  margin-top: 0.9rem;
}
.form .form-row { margin-top: 0; }

/* The "as of" control every list carries, because every question in this system
   is a question about a day. */
.asof {
  align-items: flex-end;
  display: flex;
  gap: 0.7rem;
  margin: 0 0 1rem;
}

.grow { flex: 1 1 12rem; }

.field select {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font: inherit;
  padding: 0.6rem 0.7rem;
}
.field select:focus-visible { border-color: var(--accent); outline: 2px solid var(--accent); }

.field.checkbox {
  align-items: center;
  flex-direction: row;
  gap: 0.45rem;
}
.field.checkbox input { width: 1.1rem; height: 1.1rem; }

/* A form that sits inside a table cell or a list row and must not break it. */
.inline { display: inline; margin: 0; }

/* The second-rank button: a real action, but not the one the form is for. */
.button-quiet {
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  cursor: pointer;
  font: inherit;
  padding: 0.4rem 0.7rem;
  white-space: nowrap;
}
.button-quiet:hover { border-color: var(--accent); color: var(--accent); }

/* Destructive, and the colour is the only warning a row-level button has room
   for. The sentence that explains the difference between removing a record and
   marking somebody as having left is beside the button, not on it. */
.button-quiet.danger { color: var(--error); }
.button-quiet.danger:hover { border-color: var(--error); color: var(--error); }

/* ---- The import report ----

   Errors and warnings look different because they mean different things: an
   error stopped the whole file, a warning did not stop anything. A list where
   both look the same is a list somebody scrolls past. */

.verdict-warn {
  background: color-mix(in srgb, var(--warn) 14%, transparent);
  color: var(--warn);
}
.verdict-bad {
  background: color-mix(in srgb, var(--error) 14%, transparent);
  color: var(--error);
}

/* Long sentences naming a sheet and a row, so they wrap rather than truncate —
   a message that says which row is useless with the row cut off. */
.issues li {
  font-size: 0.92rem;
  overflow-wrap: anywhere;
  padding-left: 0.7rem;
}
.issue-bad  { border-left: 3px solid var(--error); }
.issue-warn { border-left: 3px solid var(--warn); }

/* ---- Cards, and the two things that get printed --------------------------

   Two rules matter more than the rest of this block.

   One: everything a person presses is inside .sheet-tools, and .sheet-tools is
   not printed. A control on paper is a smudge, and a print run that carries a
   header and a navigation bar wastes the top third of every page.

   Two: the print block restates every colour outright rather than inheriting
   the custom properties. The variables may be carrying the dark palette at the
   moment somebody presses Print, and a card sheet printed in the dark theme is
   a black rectangle with a QR nothing can read — plus a laser cartridge. The
   file app learned this on its quiz sheets; this is the same lesson, second
   application. */

.alarm { color: var(--error); font-weight: 600; }

.code { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; letter-spacing: 0.06em; }
.subhead { margin-top: 1.2rem; }

.row-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; justify-content: flex-end; }

.sheet-tools { margin: 0 0 1.5rem; }

/* Eight to A4. Two columns by four rows, at 90×62mm — big enough that a phone
   camera finds the QR from twenty centimetres without anybody aiming. */
.cards-page {
  display: grid;
  grid-template-columns: repeat(2, 90mm);
  gap: 0;
  justify-content: center;
}

.card {
  width: 90mm;
  height: 62mm;
  padding: 5mm 6mm;
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 4mm;
  /* Dashed, because it is a cut line rather than a border: it says where the
     scissors go and it disappears with the cut. */
  border: 1px dashed var(--muted);
  background: var(--surface);
  overflow: hidden;
}

.card-who { display: flex; flex-direction: column; gap: 1mm; min-width: 0; }
.card-school { font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.card-name { font-size: 1.05rem; line-height: 1.25; overflow-wrap: anywhere; }
.card-class { font-size: 1.4rem; font-weight: 700; }
.card-qr {
  display: block;
  width: 30mm;
  height: 30mm;
  /* A QR is a bitmap of squares. Smoothing it on the way up is how a symbol
     that scans on screen stops scanning on paper. */
  image-rendering: pixelated;
}
.card-code {
  grid-column: 1 / -1;
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 1.15rem;
  letter-spacing: 0.12em;
  text-align: center;
}

/* The binding QR on the roster screen. Bigger than a card's, because it is
   read off a screen across a desk rather than off paper held in a hand — and
   because there is exactly one of it on the page, so it costs no layout. */
.bind-qr {
  display: block;
  width: 12rem;
  height: 12rem;
  margin: 0.6rem 0;
  background: #ffffff;
  padding: 0.5rem;
  image-rendering: pixelated;
}

/* The address on a slip, which is a card-code that is fifty characters long
   rather than eight. Smaller, unspaced and allowed to break anywhere: it is
   there to be typed when the camera will not focus, not to be read across a
   room. */
.card-url {
  grid-column: 1 / -1;
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 0.72rem;
  line-height: 1.3;
  text-align: center;
  overflow-wrap: anywhere;
  color: var(--muted);
}

/* The paper meal list. */
.sheet { background: var(--surface); border: 1px solid var(--border); padding: 1rem; }
.sheet-head { display: flex; flex-wrap: wrap; gap: 0.2rem 1rem; align-items: baseline; margin: 0 0 0.6rem; }
.sheet-head strong { font-size: 1.2rem; }

.table.grid th.day {
  /* Vertical, because ten dates across A4 is otherwise ten columns of wrapped
     text and no room left for the boxes. */
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  white-space: nowrap;
  font-weight: 500;
  padding: 0.4rem 0.15rem;
}
.table.grid td.code { font-size: 0.85em; }
.table.grid td.box {
  width: 1.6rem;
  border-left: 1px solid var(--border);
  /* An empty cell with nothing in it collapses; this is the tick box. */
  height: 1.6rem;
}

@media print {
  /* See the note at the top of this block: stated outright, never inherited. */
  :root, :root[data-theme="dark"], :root[data-theme="light"] {
    --bg: #ffffff;
    --surface: #ffffff;
    --border: #999999;
    --text: #000000;
    --muted: #555555;
    --accent: #000000;
    --error: #000000;
  }

  body { background: #fff; color: #000; font-size: 10pt; }
  .wrap { max-width: none; padding: 0; margin: 0; }

  /* Everything that is a control rather than content. */
  .topbar, .sheet-tools, .asof { display: none !important; }

  @page { size: A4; margin: 8mm; }

  .cards-page { gap: 0; justify-content: start; }
  .card {
    /* A card split across two pages is eight wasted sheets and a reprint.
       break-inside is the current property and page-break-inside the one older
       engines still read; both are given deliberately. */
    break-inside: avoid;
    page-break-inside: avoid;
    background: #fff;
    border-color: #999;
  }

  .sheet { border: 0; padding: 0; background: #fff; }
  .table.grid td.box { border-color: #000; }
  .table.grid th, .table.grid td { border-color: #666; }
  .table.grid tr { break-inside: avoid; page-break-inside: avoid; }
}

/* ---- The scanner ---------------------------------------------------------

   The panel is hidden until scan.js decides the browser can help, so on Safari
   and on every browser on an iPhone none of this is ever drawn. The typed field
   below it is not styled here because it is an ordinary form: it is the primary
   control, and it works with the camera off and with scripting off. */

.scanner { display: grid; gap: 0.9rem; }

.scan-controls { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center; }

.scan-state { font-weight: 600; }
.scan-state.good { color: var(--good); }
.scan-state.warn { color: var(--warn); }
.scan-state.bad { color: var(--error); }

/* The queue depth, in the alarm colour, because a queue growing silently is the
   failure this exists to prevent. */
.scan-queue {
  color: var(--error);
  font-weight: 600;
  border: 1px solid var(--error);
  border-radius: var(--radius);
  padding: 0.1rem 0.5rem;
}

.scan-video {
  width: 100%;
  max-width: 32rem;
  border-radius: var(--radius);
  background: #000;
  /* A camera pointed at a card is taller than it is wide on a phone and wider
     than it is tall on a laptop; cover keeps the middle of the frame — which is
     where somebody holds the card — visible in both. */
  object-fit: cover;
  aspect-ratio: 4 / 3;
}

.scan-result {
  display: grid;
  gap: 0.2rem;
  padding: 0.8rem 1rem;
  border-radius: var(--radius);
  border-left: 4px solid var(--border);
}
/* Colour is never the only signal — the verdict is a word, and the log line
   carries the name or the number. A dim screen and a colour-blind worker both
   read the same thing. */
.scan-result.good { border-left-color: var(--good); background: color-mix(in srgb, var(--good) 8%, transparent); }
.scan-result.warn { border-left-color: var(--warn); background: color-mix(in srgb, var(--warn) 10%, transparent); }
.scan-result.bad { border-left-color: var(--error); background: color-mix(in srgb, var(--error) 8%, transparent); }

.scan-verdict { font-size: 1.15rem; }
.scan-who { font-size: 1.05rem; }
.scan-code { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; color: var(--muted); }

.scan-log { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.2rem; }
.scan-log li {
  padding: 0.25rem 0.5rem;
  border-left: 3px solid var(--border);
  font-size: 0.95rem;
}
.scan-log li.good { border-left-color: var(--good); }
.scan-log li.warn { border-left-color: var(--warn); color: var(--warn); }
.scan-log li.bad { border-left-color: var(--error); color: var(--error); }

/* The undo the script puts on each line of the counter's log. Pushed to the end
   so a line reads name-first and the control is where the thumb is. */
.scan-log li button { float: right; margin-left: 0.5rem; }

/* The wake-lock indicator, and it is deliberately not a colour on its own: the
   dot appears only while the lock is actually held, so a lock that was refused
   looks different from one that is working. */
.scanner.awake .scan-state::before {
  content: "";
  display: inline-block;
  width: 0.55rem;
  height: 0.55rem;
  margin-right: 0.4rem;
  border-radius: 50%;
  background: var(--good);
  vertical-align: baseline;
}

/* The answer the no-JavaScript path renders. Same vocabulary as the panel
   above, because it is the same answer. */
.scan-answer {
  display: flex;
  flex-wrap: wrap;
  gap: 0.2rem 0.8rem;
  align-items: baseline;
  margin-top: 1rem;
  padding: 0.8rem 1rem;
  border-radius: var(--radius);
  border-left: 4px solid var(--border);
}
.scan-answer.good { border-left-color: var(--good); background: color-mix(in srgb, var(--good) 8%, transparent); }
.scan-answer.warn { border-left-color: var(--warn); background: color-mix(in srgb, var(--warn) 10%, transparent); }
.scan-answer.bad { border-left-color: var(--error); background: color-mix(in srgb, var(--error) 8%, transparent); }

/* ---- The counter ----

   Three rules, and each of them is a sentence from docs/school.md rather than a
   preference. */

/* The headline is the size of the name beside it. «НЕ ВІДМІЧЕНИЙ НА ВХОДІ» in
   small grey text under a green tick is a warning nobody reads across a serving
   hatch, and the same is true of every other state here — so the word carrying
   the decision is the largest thing on the panel. Colour is never the only
   signal: the word is already capitals and already unambiguous with the colour
   removed entirely. */
.loud {
  font-size: 1.5rem;
  line-height: 1.25;
  letter-spacing: 0.01em;
}

/* «24 з 28» — the ordered-against-served reconciliation, happening while
   somebody can still act on it. Tabular figures so the number does not jump
   about as it climbs during a slot. */
.tally {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  margin: 0;
  color: var(--muted);
}

/* An undone serving on the strip. Struck through rather than removed, because
   the row is still there and the way back is on the same line — a deletion that
   vanished would leave nothing to press. */
.gone td { text-decoration: line-through; color: var(--muted); }
