/* Optic's design system - the one stylesheet every page shares
   (docs/design-plan.md): a plum and lilac-grey palette, Atkinson
   Hyperlegible, the door on the kiosk. The tokens below are the only
   place a colour, a type step, a space or a radius is defined, and light
   is the only theme. Plum is identity only; green, amber and red mean
   signed out or back, late, and refused or overdue, and nothing else, so
   no button, border or decoration wears one.
   Every size is a real layout: phone 390, iPad portrait 820, iPad
   landscape 1180 (the kiosk), desktop 1920. Phones mean iOS Safari, so
   every input stays at 16px (smaller and the page zooms on focus) and a
   full-screen height is dvh. */

/* Atkinson Hyperlegible, the variable fonts' latin subsets, from Optic's
   own files: no request leaves for Google. A file is renamed when it
   changes, so browsers keep it a year (server.go). */
@font-face {
  font-family: "Atkinson Hyperlegible Next";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url(/static/fonts/atkinson-hyperlegible-next-v7.woff2) format("woff2");
}
@font-face {
  font-family: "Atkinson Hyperlegible Mono";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url(/static/fonts/atkinson-hyperlegible-mono-v8.woff2) format("woff2");
}

:root {
  /* docs/design-plan.md "Palette". --panel is the white: panels, the app
     bar, and the words on plum or a signal colour. */
  --ink: #241B30;        /* text */
  --ground: #F3F1F6;     /* the page */
  --panel: #FFFFFF;      /* panels, the app bar */
  --muted: #5C5868;      /* secondary text; the kiosk's offline screen */
  --line: #E2DEEA;       /* dividers and panel edges - never a control's only edge */
  --edge: #88809A;       /* the outline of an input or a secondary button */
  --brand: #4A2D73;      /* plum: the logo, the current nav link, primary buttons */
  --pass: #0D8045;       /* signed out, back; the logo's check */
  --pass-light: #5BE29A; /* the check on plum */
  --late: #A35E00;       /* back late, overdue soon */
  --refused: #C42B2B;    /* refused, overdue */
  /* An outcome's chip on a panel: its soft tint, ink text on it. */
  --pass-tint: #E3F5EB;
  --late-tint: #FBEFDA;
  --refused-tint: #FBE6E6;

  /* Type: Next for the interface, Mono with tabular figures for every
     Person ID, time and count. One scale, the kiosk's two largest steps
     included; each step is a whole font shorthand - weight, size, line
     height and family - so a rule picks one with `font: var(--fs-h2)`
     and never writes a number of its own. */
  --font-ui: "Atkinson Hyperlegible Next", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: "Atkinson Hyperlegible Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --fs-kiosk-name: 800 128px/128px var(--font-ui); /* the student's first name */
  --fs-kiosk-word: 800 80px/88px var(--font-ui);   /* the outcome's word */
  --fs-display: 800 56px/60px var(--font-ui);
  --fs-h1: 700 36px/42px var(--font-ui);
  --fs-h2: 700 24px/30px var(--font-ui);
  --fs-body: 400 16px/24px var(--font-ui);
  --fs-body-strong: 700 16px/24px var(--font-ui);
  --fs-small: 400 14px/20px var(--font-ui);
  --fs-eyebrow: 700 13px/16px var(--font-ui);
  --eyebrow-spacing: 2px;

  --space-1: 4px;
  --space-2: 8px;
  --space-4: 16px;
  --space-6: 24px;
  --space-10: 40px;
  --radius-sm: 8px;     /* inputs */
  --radius-md: 12px;    /* buttons, cards */
  --radius-lg: 18px;    /* panels */
  --radius-pill: 999px; /* outcome chips, the board's markers */

  --focus-ring: 3px solid var(--ink);
  --appbar-height: 72px; /* the sticky app bar; a phone's is 64px */
}

/* --- base --- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font: var(--fs-body);
}
h1, h2, h3, p, ol, ul, figure { margin: 0; }
ol, ul { padding: 0; list-style: none; }
h1 { font: var(--fs-h1); }
h2 { font: var(--fs-h2); }
h3 { font: var(--fs-body-strong); }
img { max-width: 100%; height: auto; }
a { color: var(--ink); text-underline-offset: 3px; }
:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }
[hidden] { display: none !important; } /* the attribute wins over any display rule */

/* A label above a block, uppercase and spaced. */
.eyebrow {
  font: var(--fs-eyebrow);
  letter-spacing: var(--eyebrow-spacing);
  text-transform: uppercase;
  color: var(--muted);
}

/* --- the app bar: white, a line under it, the plum wordmark at its
   start linking home. It never wraps and never scrolls with the page. --- */
.appbar {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: var(--space-10); /* more than the "o" is tall: the wordmark's clear space */
  min-height: var(--appbar-height);
  /* The page's own edges, so the wordmark sits over the page's first
     column however wide the window. */
  padding: 0 max(var(--space-10), calc((100% - 1680px) / 2 + var(--space-10)), env(safe-area-inset-right)) 0 max(var(--space-10), calc((100% - 1680px) / 2 + var(--space-10)), env(safe-area-inset-left));
  border-bottom: 1px solid var(--line);
  background: var(--panel);
}
.brand { flex: none; display: block; border-radius: var(--radius-sm); }
.brand img { display: block; width: 104px; } /* never under 96px: below that, the icon */
.appbar > nav { flex: none; align-self: stretch; display: flex; } /* never squeezed under its links: the email gives way */
.appbar nav a {
  display: flex;
  align-items: center;
  padding: 0 var(--space-4);
  color: var(--ink);
  font: var(--fs-body-strong);
  text-decoration: none;
  white-space: nowrap;
}
.appbar > nav a.current { color: var(--brand); box-shadow: inset 0 -3px 0 var(--brand); }
.appbar > nav a:focus-visible { outline-offset: -3px; } /* the link is the bar's full height */
.signout {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-width: 0;
  margin-left: auto;
}
.signout .who {
  min-width: 0;
  max-width: 320px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--muted);
  font: var(--fs-small);
}
.signout .role {
  flex: none;
  color: var(--muted);
  font: var(--fs-eyebrow);
  letter-spacing: var(--eyebrow-spacing);
  text-transform: uppercase;
}
.signout button, .menu summary {
  flex: none;
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--space-4);
  border: 1px solid var(--edge);
  border-radius: var(--radius-md);
  background: var(--panel);
  color: var(--ink);
  font: var(--fs-body-strong);
  white-space: nowrap;
  cursor: pointer;
}
@media (hover: hover) {
  .appbar nav a:hover { color: var(--brand); }
  .signout button:hover, .menu summary:hover { border-color: var(--ink); }
}

/* Phones: the links and Sign out fold behind a Menu button that drops a
   panel under the bar. A <details> opens and closes with no script. */
.menu { display: none; }
.menu summary { list-style: none; }
.menu summary::-webkit-details-marker { display: none; }
.menu[open] summary { border-color: var(--ink); background: var(--ground); }
.menu-panel {
  position: absolute; /* the sticky bar is its containing block */
  top: 100%;
  left: 0;
  right: 0;
  max-height: calc(100dvh - var(--appbar-height));
  overflow-y: auto;
  padding: var(--space-2) 0 var(--space-6);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  background: var(--panel);
  box-shadow: 0 var(--space-4) var(--space-6) color-mix(in srgb, var(--ink) 18%, transparent);
}
.menu-panel nav { display: flex; flex-direction: column; }
.menu-panel nav a {
  min-height: 48px;
  padding: 0 max(var(--space-4), env(safe-area-inset-left));
  font: var(--fs-h2);
}
.menu-panel nav a.current { color: var(--brand); box-shadow: inset 4px 0 0 var(--brand); }
.menu-panel .signout {
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin: var(--space-4) max(var(--space-4), env(safe-area-inset-left)) 0;
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
}
.menu-panel .signout .who { flex: 1 1 100%; max-width: none; font: var(--fs-body); }
@media (max-width: 1023px) {
  /* An iPad held upright, and smaller tablets down to the phone's
     menu: six links, the email and Sign out on one line, the role label
     left to the menu's panel. */
  .appbar { gap: var(--space-6); padding: 0 max(var(--space-6), env(safe-area-inset-right)) 0 max(var(--space-6), env(safe-area-inset-left)); }
  .appbar nav a { padding: 0 var(--space-2); }
  .appbar > .signout .role { display: none; }
}
@media (max-width: 699px) {
  :root { --appbar-height: 64px; }
  .appbar {
    gap: var(--space-6);
    padding: 0 max(var(--space-4), env(safe-area-inset-right)) 0 max(var(--space-4), env(safe-area-inset-left));
  }
  .appbar > nav, .appbar > .signout { display: none; }
  .menu { display: block; margin-left: auto; }
}

/* --- the page --- */
.page {
  flex: 1 0 auto;
  width: 100%;
  max-width: 1680px;
  margin: 0 auto;
  padding: var(--space-10) max(var(--space-10), env(safe-area-inset-right)) var(--space-10) max(var(--space-10), env(safe-area-inset-left));
}
.page > h1 { margin-bottom: var(--space-6); }
.pagehead { margin-bottom: var(--space-10); }
.pagehead h1 { margin-bottom: var(--space-2); }
.lead, .note { max-width: 70ch; color: var(--muted); text-wrap: pretty; }
@media (max-width: 1023px) {
  .page { padding: var(--space-6) max(var(--space-6), env(safe-area-inset-right)) var(--space-10) max(var(--space-6), env(safe-area-inset-left)); }
}
@media (max-width: 699px) {
  .page { padding: var(--space-6) max(var(--space-4), env(safe-area-inset-right)) var(--space-6) max(var(--space-4), env(safe-area-inset-left)); }
}

/* The release that is running. */
footer.site {
  padding: var(--space-6) var(--space-4) max(var(--space-6), env(safe-area-inset-bottom));
  color: var(--muted);
  font: var(--fs-small);
  text-align: center;
}

/* --- panels: white on the ground, a line for an edge --- */
.panel {
  min-width: 0;
  padding: var(--space-6);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--panel);
}
.panel-title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 24px; /* a count beside the title doesn't push it down */
  margin-bottom: var(--space-4);
  color: var(--muted);
  font: var(--fs-eyebrow);
  letter-spacing: var(--eyebrow-spacing);
  text-transform: uppercase;
}
.count {
  padding: 0 var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--ground);
  color: var(--ink);
  font: var(--fs-body-strong);
  letter-spacing: 0;
}
@media (max-width: 699px) {
  .panel { padding: var(--space-4); }
}

/* An empty state says what is true, plainly. */
.empty { padding: var(--space-10) var(--space-6); color: var(--muted); text-align: center; }

/* --- buttons: primary is plum with white words; secondary is outlined
   in --edge. There is no green, amber or red button. Touch targets are
   44px. A file field's own button is a secondary one. --- */
.button, input[type="file"]::file-selector-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: 0 var(--space-6);
  border: 1px solid var(--brand);
  border-radius: var(--radius-md);
  background: var(--brand);
  color: var(--panel);
  font: var(--fs-body-strong);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}
.button.secondary, input[type="file"]::file-selector-button { border-color: var(--edge); background: var(--panel); color: var(--ink); }
.button svg { flex: none; width: 20px; height: 20px; }
.button:disabled { opacity: .5; cursor: default; }
@media (hover: hover) {
  .button:hover { background: color-mix(in srgb, var(--brand) 85%, var(--ink)); }
  .button.secondary:hover, input[type="file"]::file-selector-button:hover { border-color: var(--ink); background: var(--panel); }
}

/* --- outcome chips: a pill in the outcome's tint, ink text, the word
   always saying what the colour does --- */
.chip {
  display: inline-flex;
  align-items: center;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--ground);
  color: var(--ink);
  font: var(--fs-small);
  font-weight: 700;
  white-space: nowrap;
}
.chip.pass { background: var(--pass-tint); }
.chip.late { background: var(--late-tint); }
.chip.refused { background: var(--refused-tint); }
/* A fact about a student - missing from the roster, inactive - is no
   outcome, so its chip is a neutral tint: it shows on a table's banded
   rows, which are --ground. So is already out, which changed nothing
   and refused nothing. */
.chip.neutral { background: var(--line); }

/* --- tables fit their panel and scroll inside it, never the page --- */
.tablewrap { max-width: 100%; overflow-x: auto; border-radius: var(--radius-sm); }
table.data { width: 100%; border-collapse: collapse; }
.data th {
  padding: var(--space-2) var(--space-4);
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font: var(--fs-eyebrow);
  letter-spacing: var(--eyebrow-spacing);
  text-align: left;
  text-transform: uppercase;
  white-space: nowrap;
}
.data td { padding: var(--space-2) var(--space-4); white-space: nowrap; }
.data tbody tr:nth-child(even) td { background: var(--ground); }

/* --- fields: outlined in --edge, 16px, so iOS never zooms on focus --- */
label { display: block; margin-bottom: var(--space-1); font: var(--fs-body-strong); }
input[type="text"], input[type="password"], input[type="email"], input[type="search"], input[type="date"], input[type="time"], select, textarea {
  width: 100%;
  min-height: 44px;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--panel);
  color: var(--ink);
  font: var(--fs-body);
}
input::placeholder, textarea::placeholder { color: var(--muted); }
/* iOS Safari centres a date's or a time's words and gives an empty one
   no height of its own: start-aligned, as every other field reads. */
input[type="date"]::-webkit-date-and-time-value, input[type="time"]::-webkit-date-and-time-value { text-align: left; }
input[type="file"] { width: 100%; font: var(--fs-body); }
input[type="file"]::file-selector-button { margin-right: var(--space-4); }

/* Choices: radios and checkboxes, each with its words in a 44px target,
   checked in plum. */
fieldset { min-width: 0; margin: 0; padding: 0; border: 0; }
legend { margin-bottom: var(--space-1); padding: 0; font: var(--fs-body-strong); }
.choices label { display: flex; align-items: center; gap: var(--space-2); min-height: 44px; margin: 0; font: var(--fs-body); }
.choices input { flex: none; width: 20px; height: 20px; margin: 0; accent-color: var(--brand); }
/* A rule's weekdays: one box each, Monday first, as many to a line as
   fit. */
.weekdays { display: flex; flex-wrap: wrap; gap: 0 var(--space-4); }

/* A field and its button on one line, the field taking the room left;
   on a phone the button wraps under it rather than squeeze it. */
.inline { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); min-width: 0; }
.inline input, .inline select { flex: 1 1 240px; min-width: 0; }

/* An error - the sign-in page's, a refused write's - is ink words in
   bold, never an outcome's colour. */
.err { color: var(--ink); font: var(--fs-body-strong); }

/* A form's answer: what a write saved, or why it was refused, which is
   an error outlined in --edge. */
.notice {
  max-width: 70ch;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--ground);
}
.notice.err { border-color: var(--edge); }
.page > .notice { margin-bottom: var(--space-6); }

/* --- the sign-in page: the plum wordmark on a white panel, the way in
   under it --- */
.signin .page {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: calc(100% - 2 * var(--space-4));
  max-width: 440px;
  margin: auto;
  padding: var(--space-10);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--panel);
}
.lockup { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); }
.signin .lockup { margin-bottom: var(--space-10); }
.lockup img { display: block; }
.signin .lockup img { width: 240px; }
.tagline {
  white-space: nowrap; /* one line under the wordmark, never two */
  color: var(--muted);
  font: var(--fs-eyebrow);
  letter-spacing: var(--eyebrow-spacing);
  text-transform: uppercase;
}
.signin .button { width: 100%; }
.signin details { margin-top: var(--space-4); }
.signin summary {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  color: var(--muted);
  font: var(--fs-body-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.signin details form { display: grid; gap: var(--space-2); margin-top: var(--space-2); }
.signin details label { margin: 0; }
.signin .err { text-align: center; }
@media (max-width: 699px) {
  .signin .page { padding: var(--space-10) var(--space-6); }
  .signin .lockup img { width: 200px; }
}

/* --- the dashboard (docs/design-plan.md "Dashboard"): the status strip
   over the door monitor, the board and the scan feed side by side, and
   recent changes and kiosk health under them. An iPad held upright pairs
   them two by two, recent changes across the foot; a phone stacks them
   in that order. --- */
.dash {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-areas:
    "status status status"
    "monitor board feed"
    "changes changes health";
  align-items: start;
  gap: var(--space-6);
}
.status-strip {
  grid-area: status;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-6);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--panel);
}
.status-strip p { display: flex; flex-direction: column; gap: var(--space-1); }
.status-line { font: var(--fs-h2); }
.door-monitor { grid-area: monitor; }
.board { grid-area: board; }
.feed { grid-area: feed; }
.changes { grid-area: changes; }
.health { grid-area: health; }
@media (max-width: 1023px) {
  .dash {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-areas:
      "status status"
      "monitor board"
      "feed health"
      "changes changes";
  }
}
@media (max-width: 699px) {
  .dash {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "status" "monitor" "board" "feed" "changes" "health";
    gap: var(--space-4);
  }
  .status-strip { padding: var(--space-4); }
}
/* Now's strip (now.html): a write's answer and Not updating over one
   entry per activity, Close now or Reopen opening its form under its
   row at full width. */
.now-status { grid-area: status; display: grid; gap: var(--space-4); min-width: 0; }
/* A column, not a grid: an entry's own grid-area (the design
   reference's lone strip) would make a grid here place it in a column
   of its own. */
.status-rows { display: flex; flex-direction: column; gap: var(--space-2); }
.board > .notice { margin-bottom: var(--space-4); }

/* The door monitor: the last scan's photo, large, for the office to
   check against the face walking out; the scans before it under it. A
   student's page has the same photo box, what an admin can do with it
   beside it. */
.door-now, .student-photo { display: flex; align-items: flex-start; gap: var(--space-4); }
.photo {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 160px;
  aspect-ratio: 4 / 5; /* the photos are 240x300 */
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--ground);
  color: var(--muted);
  font: var(--fs-small);
}
/* A student without a photo: their initials fill the box. */
.photo.initials { font: var(--fs-h1); }
/* A small one, beside a name in a list of scans. */
.photo.small { width: 40px; border-radius: var(--radius-sm); }
.door-who { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-2); min-width: 0; }
.name-lg { font: var(--fs-h2); overflow-wrap: anywhere; }
.sub { color: var(--muted); font: var(--fs-small); }
.door-outcome { display: flex; align-items: center; gap: var(--space-2); font: var(--fs-body-strong); }
.door-monitor .scans { margin-top: var(--space-6); }
@media (max-width: 1399px) {
  .photo { width: 120px; }
}

/* A list of scans: the time, the student, the outcome, and a refusal's
   staff reason under the name. */
.scans li, .change-rows li {
  display: grid;
  grid-template-columns: 48px minmax(0, 1fr) auto;
  align-items: center;
  gap: 0 var(--space-4);
  padding: var(--space-2) 0;
  border-top: 1px solid var(--line);
}
.scans li:first-child, .change-rows li:first-child { border-top: 0; }
.scans .time, .change-rows .time { color: var(--muted); }
.scans .name { font: var(--fs-body-strong); overflow-wrap: anywhere; }
.scans .reason { grid-column: 2 / -1; color: var(--muted); font: var(--fs-small); }
.change-rows li { grid-template-columns: 48px minmax(0, 1fr); align-items: baseline; }
/* Now's feed names the kiosk under the reason where there are several;
   its changes' times carry their date on another day ("10/31 11:20"),
   wrapping before the time where the date is a year's. Kiosk health is
   a scan list's row without the time: the kiosk, its chip at the end,
   its activity under. */
.scans .sub { grid-column: 2 / -1; }
.change-rows.dated li { grid-template-columns: 7rem minmax(0, 1fr); }
@media (max-width: 699px) {
  .change-rows.dated li { grid-template-columns: minmax(0, 1fr); } /* the time over its line, which takes the width */
}
.health-rows li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 0 var(--space-4);
  padding: var(--space-2) 0;
  border-top: 1px solid var(--line);
}
.health-rows li:first-child { border-top: 0; }
.health-rows .name { font: var(--fs-body-strong); overflow-wrap: anywhere; }
.health-rows .sub { grid-column: 1 / -1; }

/* The Now page's door monitor (now.html): a write's answer over its
   inside, the large slot over the scans in its column of the dashboard.
   Each scan has a small photo beside the name where there is one, and
   its reason and Not this student - or who flagged it - under the name;
   a phone wraps each row's line, the reason and the flag under it at
   full width. */
.door-monitor > .notice { margin-bottom: var(--space-4); }
.door-live { display: grid; gap: var(--space-6); }
.door-live > .scans { margin-top: 0; }
.door-live .scans li { grid-template-columns: 48px 40px minmax(0, 1fr) auto; align-items: start; }
.door-live .scans li > .time { grid-column: 1; grid-row: 1; }
.door-live .scans li > .photo { grid-column: 2; grid-row: 1 / span 3; }
.door-live .scans li > .name { grid-column: 3; grid-row: 1; }
.door-live .scans li > .chip { grid-column: 4; grid-row: 1; }
.door-live .scans li > .reason { grid-column: 3 / -1; grid-row: 2; }
.door-live .scans li > .flag { grid-column: 3 / -1; grid-row: 3; margin-top: var(--space-2); }
@media (min-width: 1400px) {
  .door-live .door-now > .photo { width: 240px; }
}
@media (max-width: 699px) {
  .door-live .scans li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-2); }
  .door-live .scans li > .name { flex: 1 1 0; min-width: 0; } /* the chip stays on the line; a long name wraps */
  .door-live .scans li > .reason, .door-live .scans li > .flag { flex-basis: 100%; }
}

/* Off campus now: an in/out board. Each student's marker sits in the
   OUT column with the time they left, and slides home to IN when they
   scan back, then leaves the board. An overdue marker is red and says
   how late, so the colour never carries it alone. */
.inout { --board-in: 72px; }
.inout-head {
  display: grid;
  grid-template-columns: var(--board-in) minmax(0, 1fr);
  gap: var(--space-2);
  margin-bottom: var(--space-2);
  color: var(--muted);
  font: var(--fs-eyebrow);
  letter-spacing: var(--eyebrow-spacing);
  text-transform: uppercase;
}
.inout-head span:first-child { text-align: center; }
.inout-head span:last-child { padding-left: var(--space-4); }
.board-rows {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  border-radius: var(--radius-md);
  /* The IN column: a channel the markers slide home into. */
  background: linear-gradient(to right, var(--ground) var(--board-in), transparent var(--board-in));
}
.board-rows li { display: grid; grid-template-columns: var(--board-in) minmax(0, 1fr); gap: var(--space-2); }
.marker {
  grid-column: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--pass-tint);
}
.marker::before { content: ""; flex: none; width: 10px; height: 10px; border-radius: 50%; background: var(--pass); }
.marker .name { font: var(--fs-body-strong); }
.marker .num { margin-left: auto; }
.late-note { flex-basis: 100%; padding-left: 18px; font: var(--fs-small); } /* under the name, past the dot */
.overdue .marker { background: var(--refused-tint); box-shadow: inset 0 0 0 2px var(--refused); }
.overdue .marker::before { background: var(--refused); }
/* Now's board (now.html): the grade under the name, past the dot, and
   Mark back under each marker, its form opening there. A student leaving
   it has nothing to press; with no one out, the board hides but for one
   now.js is playing out of it. */
.marker .sub { flex-basis: 100%; padding-left: 18px; }
.board-rows li > .disclose { grid-column: 2; }
.board-rows li.leaving > .disclose { display: none; }
.inout:has(> .board-rows:empty) { display: none; }
#board-live:has(li.leaving) > .note { display: none; } /* not "No students are off campus now" under a student still leaving */
.board-print { margin-bottom: var(--space-4); } /* Print the emergency roster, under the heading */
@keyframes marker-home {
  to { transform: translateX(calc(-1 * (var(--board-in) + var(--space-2)))); }
}
@keyframes leave-board {
  0% { opacity: 1; max-height: 96px; }
  100% { opacity: 0; max-height: 0; margin-top: calc(-1 * var(--space-2)); }
}

/* --- Setup (/setup): the time zone and schools beside access, and the
   history under them; an iPad held upright or a phone stacks them. --- */
.setup {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: start;
  gap: var(--space-6);
}
.setup-col { display: grid; gap: var(--space-6); min-width: 0; }
.setup-history { grid-column: 1 / -1; }
/* A form's answer opens at its section (the action's #fragment): clear
   of the sticky app bar - and at an anchor that stands in for one, a
   shut-out connector's on Connectors. */
.setup .panel, .grant-form, .student .panel, .missing, .bulk-add, .anchor, .lists .panel, .listpage .panel, .listpage .rule-form, .entry-rows > li {
  scroll-margin-top: calc(var(--appbar-height) + var(--space-4));
}
.setup .panel > * + *, .student .panel > * + *, .missing > * + *, .bulk-add > * + *, .lists .panel > * + *, .listpage .panel > * + * { margin-top: var(--space-4); }
.setup .panel > h2 + .note, .student .panel > h2 + .note, .listpage .panel > h2 + .note, .lists .panel > h2 + .note { margin-top: var(--space-1); }
/* A section inside a panel - a kind of mapping - opens under a line. */
.setup .panel > h3 { margin-top: var(--space-6); padding-top: var(--space-6); border-top: 1px solid var(--line); }
@media (max-width: 1023px) {
  .setup { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 699px) {
  .setup, .setup-col { gap: var(--space-4); }
}

/* A row per school or per person with access: what it is, and its
   buttons, which wrap under it on a phone. */
.setup-rows li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-4) 0;
  border-top: 1px solid var(--line);
}
.setup-rows li:first-child { padding-top: 0; border-top: 0; }
/* Setup's District panel: the district's logo, at most 160px high - a
   stored one's half - and never wider than the panel. */
.district-logo { display: block; max-height: 160px; }
/* A long list of short rows - a kind's mappings - packs tighter. */
.setup-rows.tight li { padding: var(--space-1) 0; }
.setup-rows.tight li:first-child { padding-top: 0; }
.setup-rows > li > input { flex: 1 1 240px; min-width: 0; }
.row-main { flex: 1 1 200px; min-width: 0; overflow-wrap: anywhere; }
.row-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.grant-form, .rule-form { display: grid; gap: var(--space-4); padding-top: var(--space-6); border-top: 1px solid var(--line); }
.form-stack { display: grid; gap: var(--space-4); }
.grant-form > .button, .rule-form > .button, .form-stack > .button { justify-self: start; }
/* Admin reaches every school, so its schools are asked for only with
   Office. */
.grant-form:has(input[name="role"][value="admin"]:checked) .grant-schools { display: none; }
/* History's last column is words: it takes the room the time and the
   email leave, and wraps, where they never do. */
.data td.words { width: 100%; min-width: 24ch; white-space: normal; }
/* A log - every History, a list's Removed - is a table whose last cell
   is words. On a phone each row stacks: its first cells, when and who,
   on one line in the small muted text, the words under them at full
   width, and no header row; an iPad or a desktop keeps the table. */
@media (max-width: 699px) {
  .data.log, .data.log tbody { display: block; }
  .data.log thead { display: none; }
  .data.log tr { display: flex; flex-wrap: wrap; gap: 0 var(--space-4); padding: var(--space-2) var(--space-4); }
  .data.log tbody tr:nth-child(even) { background: var(--ground); }
  .data.log tbody tr:nth-child(even) td { background: none; }
  .data.log td { padding: 0; color: var(--muted); font: var(--fs-small); } /* a Person ID's or a time's face: .num, at the end */
  .data.log td.words { flex-basis: 100%; min-width: 0; color: var(--ink); font: var(--fs-body); overflow-wrap: anywhere; }
}

/* Roster files (/setup/roster): Setup's columns. Each kind of mapping
   leads with the values on students it doesn't place yet, each a field
   and a button beside it, on the ground so they stand out - as a list's
   file's preview does. */
.unmapped, .preview { padding: var(--space-4); border-radius: var(--radius-md); background: var(--ground); }
.unmapped .setup-rows { margin-top: var(--space-2); }
.setup-rows > li > .map-form { flex: 1 1 100%; }
.map-form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-2) var(--space-4); }
.map-form > div { flex: 1 1 200px; min-width: 0; }
/* The school picker (Setup → Connectors) lays each SIS school out as a
   mapping form, its Exclude box at the end as wide as its words. */
.map-form > .choices { flex: none; }

/* Photos (/setup/photos): Setup's columns. The report lists the files
   it couldn't place, as many to a line as fit - as a bulk add's answer
   and a file's preview list the students they name. */
.file-list { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-6); }
.file-list li { min-width: 0; overflow-wrap: anywhere; }

/* --- Students (/students): the filters over the list, which scrolls
   inside its panel; an admin's Deactivate all missing under it. An
   iPad held upright sets the filters three across under the search, a
   phone stacks them. --- */
.filters { display: grid; gap: var(--space-4); margin-bottom: var(--space-6); }
.filter-fields { display: grid; grid-template-columns: minmax(0, 2fr) repeat(5, minmax(min-content, 1fr)); gap: var(--space-4); }
/* A report's dates hold "12/31/2026" and the calendar's icon whole
   (measured: 178px at 16px), their columns growing to hold them from
   the others' room, where a row is shared. */
.filter-fields input[type="date"] { min-width: 11.25em; }
/* On the list or not is asked only once a list is chosen. */
.filters:has(#f-list option[value=""]:checked) .filter-on { display: none; }
.filter-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.list-note { margin-bottom: var(--space-4); }
.missing, .bulk-add { margin-top: var(--space-6); }
.confirm-form { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-6); }
/* Add to list: each row's box leads it, in a column no wider than the
   box's 44px target; the panel's list beside the entry's dates and note
   where both fit. */
.data th.tick, .data td.tick { width: 1%; padding-left: var(--space-1); padding-right: var(--space-1); text-align: center; }
.data td.tick { padding-top: 0; padding-bottom: 0; }
.tick label { justify-content: center; min-width: 44px; }
.bulk-fields { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); align-items: start; gap: var(--space-4); }
@media (max-width: 1023px) {
  .filter-fields { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .filter-search { grid-column: 1 / -1; }
}
@media (max-width: 699px) {
  .filter-fields, .bulk-fields { grid-template-columns: minmax(0, 1fr); }
  .filters { margin-bottom: var(--space-4); }
  .missing, .bulk-add { margin-top: var(--space-4); }
}

/* --- a report (/reports/{name}): the Students page's filters over its
   table, which scrolls inside its panel; how many rows at its head, the
   CSV and the PDF beside them. A report of several tables sets them one
   under another, each under its title. --- */
.report-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-4); margin-bottom: var(--space-4); }
.report-exports { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.report-table + .report-table { margin-top: var(--space-10); }
/* A report's headings sort its table: each a link in the heading's own
   face and colour, a thumb tall inside the row it heads, its glyph
   beside its title so a touch screen shows it can be pressed: ↕
   muted, the sorted one's arrow in ink. Under a table of more than one
   page, its pager: Previous, the pages and Next as secondary buttons a
   thumb tall, in a row that wraps on a phone - the page shown, a step
   there is none to and a gap in words - then the rows a page, the one
   shown in words. */
.data th a { display: inline-flex; align-items: center; gap: var(--space-1); min-height: 44px; margin: calc(-1 * var(--space-2)) 0; color: inherit; text-decoration: none; }
.data th a span { color: var(--muted); }
.data th[aria-sort] a span { color: var(--ink); }
@media (hover: hover) {
  .data th a:hover { text-decoration: underline; }
}
.pager { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-top: var(--space-4); }
.pager .button { min-width: 44px; padding: 0 var(--space-4); }
.pager > span { display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; color: var(--muted); }
.pager > span[aria-current] { color: var(--ink); font: var(--fs-body-strong); }
.pager .per { margin-left: auto; color: var(--muted); }
.pager .per a { display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; }
.pager .per span { color: var(--ink); font: var(--fs-body-strong); }

/* --- a student's page (/students/{id}): their details beside their
   parent permission, lists and history, which take the width; an iPad
   held upright or a phone stacks them. --- */
.student {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  grid-template-rows: auto auto 1fr; /* the details run down beside all three */
  align-items: start;
  gap: var(--space-6);
}
.student > .panel:first-child { grid-row: 1 / span 3; }
.facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--space-2) var(--space-6); margin: 0; }
.facts dt { color: var(--muted); }
.facts dd { min-width: 0; margin: 0; overflow-wrap: anywhere; }
.student-action { display: grid; gap: var(--space-2); padding-top: var(--space-4); border-top: 1px solid var(--line); }
.student-action > .button { justify-self: start; }
@media (max-width: 1023px) {
  .student { grid-template-columns: minmax(0, 1fr); grid-template-rows: none; }
  .student > .panel:first-child { grid-row: auto; }
}
@media (max-width: 699px) {
  .student { gap: var(--space-4); }
}

/* --- Lists (/lists), and Activities (/activities) on the same skin:
   each school's in a panel - a district's lists first - and the retired
   last, as many across as fit beside an admin's New list or New
   activity; an iPad held upright or a phone stacks them. --- */
.lists {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  align-items: start;
  gap: var(--space-6);
}
.lists:not(:has(.list-new)) { grid-template-columns: minmax(0, 1fr); }
.list-groups { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 440px), 1fr)); align-items: start; gap: var(--space-6); }
.list-current { white-space: nowrap; } /* a count and its word on one line */
/* Named staff are asked for only beside office, not instead of it. */
.managed-form:has(input[name="managed_by"][value="office"]:checked) .named-staff { display: none; }
.named-staff label { align-items: flex-start; padding: var(--space-2) 0; }
.named-staff input { margin-top: 2px; }

/* --- a list (/lists/{id}): what it is and Add a student beside its
   entries, removed ones and history, which take the width, with Clear
   list and an admin's settings under the first column; an iPad held
   upright or a phone stacks them in that order. An activity
   (/activities/{id}) wears the same: what it is beside its rules, lists,
   closed dates and history, an admin's settings under the first
   column. --- */
.listpage {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  grid-template-rows: auto 1fr;
  align-items: start;
  gap: var(--space-6);
}
.list-side, .list-main { display: grid; gap: var(--space-6); min-width: 0; }
.list-main { grid-column: 2; grid-row: 1 / span 2; }
.list-side:last-child { grid-column: 1; grid-row: 2; }
h3.entry-group { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-6); padding-top: var(--space-6); border-top: 1px solid var(--line); }
.listpage .panel > h3.entry-group:first-of-type { margin-top: var(--space-4); padding-top: 0; border-top: 0; }
/* An entry: the student, then its dates, note and who added it, then
   the managers' Change dates and Remove, each opening its form across
   the row. */
.entry-facts { flex: 2 1 280px; min-width: 0; overflow-wrap: anywhere; }
.entry-rows > li > .notice { flex: 1 1 100%; }
.entry-actions { flex: 1 1 100%; display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--space-2); }
.disclose > summary { list-style: none; }
.disclose > summary::-webkit-details-marker { display: none; }
.disclose[open] { flex: 1 1 100%; }
.disclose[open] > summary { border-color: var(--ink); background: var(--ground); }
.entry-form { display: grid; gap: var(--space-4); }
.disclose > .entry-form { margin-top: var(--space-4); padding: var(--space-4); border-radius: var(--radius-md); background: var(--ground); }
.match-rows .entry-form { flex: 1 1 100%; }
.entry-form > .button { justify-self: start; }
/* Start and end side by side where both fit a date and its calendar,
   one above the other where they don't. */
.date-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 176px), 1fr)); gap: var(--space-4); }
.entry-rows .num { white-space: nowrap; } /* a time and its zone on one line */
/* Add from a file's preview and a bulk add's answer: each group its
   title and count over its names, as many to a line as fit (.file-list). */
.preview > * + *, .notice.batch > * + *, .disclose.past[open] > * + * { margin-top: var(--space-4); }
.preview .count, .notice.batch .count { background: var(--panel); } /* a count's pill on their ground */
.name-group > * + * { margin-top: var(--space-1); }
.name-group h3 { display: flex; align-items: center; gap: var(--space-2); }
@media (hover: hover) {
  .disclose > summary.button:hover { border-color: var(--ink); background: var(--ground); }
}
@media (max-width: 1023px) {
  .lists, .listpage { grid-template-columns: minmax(0, 1fr); grid-template-rows: none; }
  .list-main, .list-side:last-child { grid-column: auto; grid-row: auto; }
}
@media (max-width: 699px) {
  .lists, .listpage, .list-groups, .list-side, .list-main { gap: var(--space-4); }
}

/* --- the kiosk (docs/design-plan.md "The kiosk"): each outcome fills
   the screen, read from a step away; the full signal colours carry
   white words only. --- */
.kscreen {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-10);
  min-height: 100vh;
  min-height: 100dvh;
  padding: max(var(--space-10), env(safe-area-inset-top)) max(var(--space-10), env(safe-area-inset-right)) max(var(--space-10), env(safe-area-inset-bottom)) max(var(--space-10), env(safe-area-inset-left));
  color: var(--panel);
  text-align: center;
}
.kscreen.pass { background: var(--pass); }
.kscreen.late { background: var(--late); }
.kscreen.refused { background: var(--refused); }
.kscreen.offline { background: var(--muted); }
.k-words { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); max-width: 100%; }
.k-name { font: var(--fs-kiosk-name); overflow-wrap: anywhere; }
.k-say { max-width: 13em; font: var(--fs-kiosk-word); text-wrap: balance; }
.k-detail { margin-top: var(--space-4); font: var(--fs-display); }
/* An iPad on its side and wider: the door beside the words, as the
   kiosk reads. */
@media (min-width: 1024px) {
  .kscreen:not(.idle) { flex-direction: row; text-align: left; }
  .kscreen:not(.idle) .k-words { align-items: flex-start; }
}

/* The door: a tall rounded rectangle with a knob, in the screen's own
   white. An arrow passes out or in through it; a bar drops across it
   and stays. Each is drawn where it ends, so with reduced motion it is
   simply there. */
.door { flex: none; width: 360px; height: 270px; overflow: visible; }
.door-frame { fill: none; stroke: currentColor; stroke-width: 12; }
.door-knob, .door-bar { fill: currentColor; }
.door-arrow { fill: none; stroke: currentColor; stroke-width: 14; stroke-linecap: round; stroke-linejoin: round; }
@keyframes door-out {
  from { transform: translateX(-270px); opacity: 0; }
  20% { opacity: 1; }
  to { transform: none; opacity: 1; }
}
@keyframes door-in {
  from { transform: translateX(270px); opacity: 0; }
  20% { opacity: 1; }
  to { transform: none; opacity: 1; }
}
@keyframes door-bar {
  from { transform: translateY(-120px); opacity: 0; }
  30% { opacity: 1; }
  to { transform: none; opacity: 1; }
}

/* Idle: the ground, the district's logo leading, the call to scan, when
   open campus closes, and the plum wordmark small at the foot. */
.kscreen.idle {
  align-items: stretch;
  justify-content: space-between;
  background: var(--ground);
  color: var(--ink);
  text-align: left;
}
.k-top, .k-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--space-6); }
.k-logo {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 240px;
  height: 120px;
  padding: 0 var(--space-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--panel);
  color: var(--muted);
  font: var(--fs-body-strong);
  text-align: center; /* a school's name, while the district has no logo, on two lines if it needs them */
  overflow-wrap: anywhere;
}
/* The district's logo fills the box's width when wide, its height when
   tall; the box keeps its size, line and white ground. */
.k-logo-img { display: block; max-width: 100%; max-height: 100%; object-fit: contain; }
.k-door { font: var(--fs-h1); text-align: right; }
.k-prompt { font: var(--fs-kiosk-word); }
.k-open { font: var(--fs-h1); }
.k-mark { flex: none; width: 128px; }
@media (max-width: 1023px) {
  .door { width: 320px; height: 240px; }
}
@media (max-width: 699px) {
  .kscreen {
    gap: var(--space-6);
    padding: max(var(--space-6), env(safe-area-inset-top)) max(var(--space-4), env(safe-area-inset-right)) max(var(--space-6), env(safe-area-inset-bottom)) max(var(--space-4), env(safe-area-inset-left));
  }
  .door { width: 200px; height: 150px; }
  .k-name { font: var(--fs-display); }
  .k-say { font: var(--fs-h1); }
  .k-detail { margin-top: var(--space-2); font: var(--fs-h2); }
  .k-logo { width: 160px; height: 80px; }
  .k-door, .k-open { font: var(--fs-h2); }
  .k-prompt { font: var(--fs-h1); }
  .k-mark { width: 96px; }
}

/* A paired kiosk's one field: focused, so the scanner's keys land in it,
   and never seen - the screen says everything. It keeps the fields' 16px,
   so iOS never zooms in on its focus, and sits where focusing it scrolls
   nothing. */
.k-scan input {
  position: fixed;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  min-height: 0;
  padding: 0;
  border: 0;
  opacity: 0;
}

/* Setup → Kiosks: a pairing code, the one time it shows - large, its two
   halves on one line. */
.pair-code { font: var(--fs-h2); letter-spacing: var(--eyebrow-spacing); white-space: nowrap; }

/* The design reference's links under a kiosk screen. */
.kstrip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4) var(--space-6);
  padding: var(--space-6) max(var(--space-6), env(safe-area-inset-right)) max(var(--space-6), env(safe-area-inset-bottom)) max(var(--space-6), env(safe-area-inset-left));
  border-top: 1px solid var(--line);
  background: var(--panel);
}
.kstrip ul { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.kstrip ul a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--space-4);
  border: 1px solid var(--edge);
  border-radius: var(--radius-pill);
  font: var(--fs-body-strong);
  text-decoration: none;
}
.kstrip ul a[aria-current="page"] { border-color: var(--brand); background: var(--brand); color: var(--panel); }
.kstrip-end { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4) var(--space-6); }
.kstrip-end p { color: var(--muted); }
@media (hover: hover) {
  .kstrip ul a:hover { border-color: var(--ink); }
}
@media (max-width: 699px) {
  .kstrip { padding-left: max(var(--space-4), env(safe-area-inset-left)); padding-right: max(var(--space-4), env(safe-area-inset-right)); }
}

/* --- the design reference (/design) --- */
.ref + .ref { margin-top: var(--space-10); padding-top: var(--space-10); border-top: 1px solid var(--line); }
.ref > h2, .ref-head { margin-bottom: var(--space-4); }
.ref > .note { margin-bottom: var(--space-6); }
.ref-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-4); }

.marks { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-6); }
.tile {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-6);
  min-height: 220px;
  padding: var(--space-6);
  border-radius: var(--radius-lg);
}
.tile.on-panel { border: 1px solid var(--line); background: var(--panel); }
.tile.on-brand { background: var(--brand); }
.tile img { display: block; }
.mark figcaption { margin-top: var(--space-2); color: var(--muted); font: var(--fs-small); }
.file { display: block; color: var(--ink); font-weight: 700; overflow-wrap: anywhere; }
.marks + .note { margin-top: var(--space-4); }

.swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: var(--space-6) var(--space-4); font: var(--fs-small); }
.swatch { display: block; height: 72px; margin-bottom: var(--space-2); border-radius: var(--radius-md); }
.sw-ink { background: var(--ink); }
.sw-ground { background: var(--ground); }
.sw-panel { background: var(--panel); }
.sw-muted { background: var(--muted); }
.sw-line { background: var(--line); }
.sw-edge { background: var(--edge); }
.sw-brand { background: var(--brand); }
.sw-pass { background: var(--pass); }
.sw-pass-light { background: var(--pass-light); }
.sw-late { background: var(--late); }
.sw-refused { background: var(--refused); }
.sw-pass-tint { background: var(--pass-tint); }
.sw-late-tint { background: var(--late-tint); }
.sw-refused-tint { background: var(--refused-tint); }
.sw-ground, .sw-panel, .sw-line, .sw-pass-tint, .sw-late-tint, .sw-refused-tint { border: 1px solid var(--line); }
.token { font-weight: 700; }
.token, .hex { font-family: var(--font-mono); }
.hex, .contrast { color: var(--muted); }
.ref h3.eyebrow { margin: var(--space-10) 0 var(--space-4); }
.ref > .note + h3.eyebrow { margin-top: 0; }
/* An iPad held upright: the two wordmarks side by side, the icons under
   them, and the palette three across; a phone, one tile above the
   other and the palette two across. */
@media (max-width: 1023px) {
  .marks { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .marks .mark:last-child { grid-column: 1 / -1; }
  .swatches { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 699px) {
  .marks { grid-template-columns: minmax(0, 1fr); }
  .swatches { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.faces { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: var(--space-6); margin-bottom: var(--space-6); }
.face { display: grid; gap: var(--space-2); padding: var(--space-6); border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--panel); }
.typescale { display: grid; }
.type-row {
  display: grid;
  grid-template-columns: 280px minmax(0, 1fr);
  align-items: baseline;
  gap: var(--space-1) var(--space-6);
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--line);
}
.type-row > p:last-child { overflow-wrap: anywhere; }
.type-meta { color: var(--muted); font: var(--fs-small); }
.type-meta .token { display: block; color: var(--ink); }
.t-kiosk-name { font: var(--fs-kiosk-name); }
.t-kiosk-word { font: var(--fs-kiosk-word); }
.t-display { font: var(--fs-display); }
.t-h1 { font: var(--fs-h1); }
.t-h2 { font: var(--fs-h2); }
.t-body { font: var(--fs-body); }
.t-body-strong { font: var(--fs-body-strong); }
.t-small { font: var(--fs-small); }
.figures { margin-top: var(--space-6); }
.figures .note { margin-bottom: var(--space-4); }
.figures table { width: auto; background: var(--panel); }
@media (max-width: 699px) {
  .type-row { grid-template-columns: minmax(0, 1fr); }
}

.buttons, .chips { display: flex; flex-wrap: wrap; gap: var(--space-4); }
.chips { gap: var(--space-2); }
.field { max-width: 360px; margin-top: var(--space-6); }

/* --- numbers: every Person ID, time and count in the mono face, its
   figures one width. Last, so it wins over a step on the same element. --- */
.num, .data.log td.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; } /* a log's step resets them */

/* --- motion: the door at the kiosk, and the board's marker sliding
   home. With reduced motion neither moves; each simply appears in its
   end state. --- */
@media (prefers-reduced-motion: no-preference) {
  .door.out .door-arrow { animation: door-out 900ms cubic-bezier(.45, 0, .25, 1) 150ms both; }
  .door.in .door-arrow { animation: door-in 900ms cubic-bezier(.45, 0, .25, 1) 150ms both; }
  .door.barred .door-bar { animation: door-bar 500ms cubic-bezier(.3, 1.35, .6, 1) 150ms both; }
  .board-rows li.leaving { overflow: hidden; animation: leave-board 450ms ease-in 800ms both; }
  .board-rows li.leaving .marker { animation: marker-home 600ms cubic-bezier(.45, 0, .25, 1) both; }
}
