/* ============================================================
 * ClassPulse — global stylesheet
 * Friendly + warm defaults; --accent overridable per school via base.html.
 * ============================================================ */

:root {
  /* Surface */
  --bg:            hsl(40 20% 98%);
  --surface:       #ffffff;
  --surface-soft:  hsl(40 20% 96%);
  --surface-sunken:hsl(40 14% 94%);

  /* Text — verified ≥4.5:1 contrast against --bg */
  --text:          hsl(20 12% 12%);
  --text-muted:    hsl(20 10% 36%);
  --text-subtle:   hsl(20 6%  52%);

  /* Lines */
  --border:        hsl(30 15% 88%);
  --border-strong: hsl(30 12% 76%);

  /* Accent (default teal — override per school via inline <style>) */
  --accent:        hsl(173 80% 30%);
  --accent-hover:  hsl(173 80% 25%);
  --accent-soft:   hsl(173 70% 94%);
  --accent-fg:     #ffffff;

  /* Status */
  --success:       hsl(150 60% 35%);
  --success-soft:  hsl(150 55% 94%);
  --warning:       hsl(35 90% 45%);
  --warning-soft:  hsl(35 90% 94%);
  --danger:        hsl(0 70% 48%);
  --danger-soft:   hsl(0 70% 96%);

  /* Spacing scale (use as `var(--s-3)` etc) */
  --s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px;
  --s-5: 24px; --s-6: 32px; --s-7: 48px; --s-8: 64px;

  /* Radii */
  --r-sm: 6px;
  --r-md: 10px;
  --r-lg: 14px;
  --r-pill: 999px;

  /* Shadows */
  --shadow-sm: 0 1px 2px hsl(20 10% 10% / 0.06);
  --shadow-md: 0 4px 12px hsl(20 10% 10% / 0.08);
  --shadow-lg: 0 12px 32px hsl(20 10% 10% / 0.12);

  /* Type */
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", Roboto,
             "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  /* Evacuation FAB geometry — one source for the button's diameter and its
     inset from the viewport edge, so the breakpoint override below can resize
     it without any other rule restating the number. */
  --evac-fab-size: 56px;
  --evac-fab-offset: max(30px, env(safe-area-inset-bottom, 0px));
}

/* ============================================================ Reset */
*, *::before, *::after { box-sizing: border-box; }
/* The HTML `hidden` attribute relies on the UA rule `[hidden] { display: none }`,
   which is at attribute-selector specificity (0,1,0) — same as a class. Any
   later author rule that sets `display:` on a class wins the cascade, so
   `<div class="banner" hidden>` or `<button class="btn" hidden>` stays
   visible. Force `hidden` to always win so the attribute behaves as the
   HTML spec promises. (Discovered via the offline banner pinning open
   regardless of connectivity.) */
[hidden] { display: none !important; }
html, body { margin: 0; padding: 0; }
html {
  /* Sticky nav offset so anchor links / fragment scrolls don't land underneath. */
  scroll-padding-top: 80px;
  -webkit-text-size-adjust: 100%;
}
body {
  font-family: var(--font-ui);
  font-size: 16px;
  line-height: 1.5;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
img, svg { max-width: 100%; height: auto; display: block; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; padding: 0; }
/* Eliminate iOS Safari's 300ms tap delay on interactive surfaces. */
a, button, label, [role="button"] { touch-action: manipulation; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* Skip link — first focusable element, visible only on focus. */
.skip-link {
  position: absolute; top: 8px; left: 8px;
  padding: 8px 12px;
  background: var(--accent); color: var(--accent-fg);
  border-radius: var(--r-md);
  font-weight: 600;
  transform: translateY(-200%);
  transition: transform 120ms ease;
  z-index: 200;
}
.skip-link:focus {
  transform: translateY(0);
  text-decoration: none;
}

/* ============================================================ Typography */
h1, h2, h3, h4 { line-height: 1.2; margin: 0 0 var(--s-3); font-weight: 700; letter-spacing: -0.01em; }
h1 { font-size: clamp(1.5rem, 1.2rem + 1.4vw, 2rem); }
h2 { font-size: clamp(1.25rem, 1rem + 1vw, 1.5rem); }
h3 { font-size: 1.125rem; }
p  { margin: 0 0 var(--s-3); }
small, .text-sm { font-size: 0.875rem; }
.text-xs { font-size: 0.75rem; }
.text-muted { color: var(--text-muted); }
.text-subtle { color: var(--text-subtle); }
.text-strong { font-weight: 600; }
code, pre { font-family: var(--font-mono); font-size: 0.92em; }

/* ============================================================ Layout */
.page { min-height: 100dvh; display: flex; flex-direction: column; }
main { flex: 1; }
.container {
  width: 100%;
  max-width: 1100px;
  margin: 0 auto;
  padding-inline: clamp(16px, 4vw, 32px);
  padding-block: var(--s-5);
}
.stack > * + * { margin-top: var(--s-4); }
.stack-sm > * + * { margin-top: var(--s-3); }
.stack-lg > * + * { margin-top: var(--s-5); }
.row { display: flex; gap: var(--s-3); flex-wrap: wrap; align-items: center; }
.row--end { justify-content: flex-end; }
.row--between { justify-content: space-between; }
/* Bottom-align children. Use when a row mixes bare controls with .form-row
   blocks (label above input): the default align-items:center would centre the
   control against the taller label+input block, leaving it straddling the
   label instead of lining up with the inputs. */
.row--bottom { align-items: flex-end; }
.grid { display: grid; gap: var(--s-4); }
.grid-cards { grid-template-columns: 1fr; }
@media (min-width: 640px) {
  .grid-cards { grid-template-columns: repeat(2, 1fr); }
}

/* ============================================================ Card */
.card {
  --card-pad: var(--s-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--card-pad);
  box-shadow: var(--shadow-sm);
}
.card--soft { background: var(--surface-soft); box-shadow: none; }
.card--center { text-align: center; }
.card__head { margin-bottom: var(--s-4); }
.card__head h2, .card__head h3 { margin-bottom: var(--s-1); }
@media (max-width: 480px) {
  /* On phones, drop card padding to claw back the ~16px we lose on each
     side of the viewport. Wide list tables inside cards still get their
     own horizontal scroll via .table-wrap. */
  .card { --card-pad: var(--s-4); }
}

/* ============================================================ Buttons */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  min-height: 44px; padding: 0 var(--s-4);
  border-radius: var(--r-md);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-strong);
  font-weight: 600;
  line-height: 1;
  transition: background 120ms ease, border-color 120ms ease, transform 120ms ease;
  text-decoration: none;
}
.btn:hover { background: var(--surface-soft); text-decoration: none; }
.btn:active { transform: translateY(1px); }
.btn--primary {
  background: var(--accent); color: var(--accent-fg); border-color: var(--accent);
}
.btn--primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: var(--accent-fg); }
.btn--ghost { background: transparent; border-color: transparent; }
.btn--ghost:hover { background: var(--surface-soft); }
.btn--danger { background: var(--danger); border-color: var(--danger); color: white; }
.btn--danger:hover { filter: brightness(0.95); color: white; }
.btn--block { width: 100%; }
.btn--sm { min-height: 36px; padding: 0 var(--s-3); font-size: 0.9rem; }
.btn[disabled], .btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: none;
}
.btn[aria-current="page"] {
  background: var(--accent-soft);
  border-color: transparent;
  color: var(--accent);
}

/* ============================================================ Actions menu (dropdown)
   Used on detail pages (student dossier, class, teacher, …) to collapse
   a row of header buttons (Edit / Withdraw / Reset PIN / Disable / …)
   into a single "Actions" trigger. Mobile-first: a row of 2-4 buttons
   doesn't fit on a 360 px viewport; a dropdown does. Built on native
   <details>/<summary> so the open/close state needs no JS. Light JS in
   static/js/actions-menu.js adds click-outside + ESC. */
.actions-menu {
  position: relative;
  display: inline-block;
}
.actions-menu > summary.actions-menu__trigger {
  list-style: none;            /* hide native disclosure marker */
  cursor: pointer;
}
.actions-menu > summary.actions-menu__trigger::-webkit-details-marker {
  display: none;
}
.actions-menu > summary.actions-menu__trigger::after {
  content: " ▾";
  font-size: 0.75em;
  margin-left: 4px;
  opacity: 0.7;
}
.actions-menu[open] > summary.actions-menu__trigger::after {
  content: " ▴";
}
.actions-menu__panel {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  min-width: 220px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: 0 6px 20px hsl(0 0% 0% / 0.12), 0 0 0 1px hsl(0 0% 0% / 0.04);
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  z-index: 60;
}
.actions-menu__item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-radius: 6px;
  border: 0;
  background: transparent;
  text-align: left;
  cursor: pointer;
  color: var(--text);
  text-decoration: none;
  font-size: 0.9rem;
  font-weight: 500;
  white-space: nowrap;
  width: 100%;
  font-family: inherit;
  min-height: 40px; /* touch target */
}
.actions-menu__item:hover {
  background: var(--surface-soft);
  text-decoration: none;
}
.actions-menu__item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.actions-menu__item--danger {
  color: hsl(0 65% 42%);
}
.actions-menu__item--danger:hover {
  background: hsl(0 80% 96%);
  color: hsl(0 65% 38%);
}
.actions-menu__separator {
  height: 1px;
  background: var(--border);
  margin: 4px 2px;
}
/* POST actions live inside a <form> in the panel; display:contents lets the
   button participate directly in the panel's flex column, so a form-wrapped
   item looks identical to a plain button/link item. */
.actions-menu__panel form {
  display: contents;
}

/* On narrow screens, anchor the panel to the right edge of the screen
   instead of the trigger so it doesn't get clipped by the viewport. */
@media (max-width: 480px) {
  .actions-menu__panel {
    right: 0;
    max-width: calc(100vw - 16px);
  }
}

/* --- Kebab trigger (icon-only, for table rows) --- */
.actions-menu__trigger--kebab {
  list-style: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  background: transparent;
  border-radius: 6px;
  color: var(--text-muted);
  cursor: pointer;
}
.actions-menu__trigger--kebab:hover {
  background: var(--surface-soft);
  color: var(--text);
}
.actions-menu__trigger--kebab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.actions-menu__trigger--kebab::after {
  content: none;            /* no chevron — the dots ARE the affordance */
}
.actions-menu__trigger--kebab::-webkit-details-marker {
  display: none;
}

/* --- Detached panel (for rows inside overflow-clipping table wraps) ---
   Default panel uses position:absolute and lives inside its <details>
   ancestor — fine for page-header dropdowns where no ancestor clips
   overflow. Row kebabs sit inside `.table-wrap { overflow-x: auto }`
   which would clip the panel. Marking the menu `--detached` lets JS
   reposition the panel as `position: fixed` so it escapes the clip. */
.actions-menu--detached > .actions-menu__panel {
  position: fixed;          /* JS sets top + right + left:auto on open */
}

/* --- Responsive row actions ---
   Table rows can show their actions as an inline button group on wide
   screens and collapse them into the row kebab on narrow ones. Both are
   rendered; CSS shows exactly one. */
.row-actions-inline { display: inline-flex; gap: 0.4rem; align-items: center; }
.row-actions-kebab { display: none; }
@media (max-width: 640px) {
  .row-actions-inline { display: none; }
  .row-actions-kebab { display: inline-block; }
}

/* ============================================================ Tabs / Segmented control
   Used by the period picker (Today / Last 7 days / Last 30 days / Term / Year)
   on dashboards and detail pages. Renders as a pill-shaped segmented
   control: a sunken track, each tab a transparent pill, the active
   tab elevated with a white surface card and accent-coloured text. */
.tab-bar {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 4px;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 0.875rem;
  line-height: 1;
}

.tab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 32px;
  padding: 6px 14px;
  border-radius: 999px;
  color: var(--text-muted);
  text-decoration: none;
  font-weight: 500;
  white-space: nowrap;
  transition: background 80ms ease, color 80ms ease, box-shadow 80ms ease;
}

.tab:hover {
  color: var(--text);
  background: hsl(0 0% 100% / 0.6);
  text-decoration: none;
}

.tab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.tab--active,
.tab--active:hover {
  background: var(--surface);
  color: var(--accent);
  font-weight: 600;
  box-shadow: 0 1px 2px hsl(0 0% 0% / 0.06), 0 0 0 1px hsl(0 0% 0% / 0.04);
}

/* On narrow screens, let the segmented control span full width and
   distribute its children evenly so the labels stay readable. */
@media (max-width: 480px) {
  .tab-bar {
    display: flex;
    width: 100%;
  }
  .tab {
    flex: 1 1 auto;
    padding: 6px 8px;
  }
}

/* ============================================================ Period filter
   Renders both a <select> and the chip nav. CSS hides the inappropriate
   one based on viewport width. Anything <= 640px gets the dropdown so
   we don't have six chips wrapping to two lines. */
.period-filter__select-form { display: none; }
.period-filter__select {
  font-size: 0.875rem;
  padding: 6px 10px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  min-height: 32px;
  cursor: pointer;
}
@media (max-width: 640px) {
  .period-filter__chips { display: none; }
  .period-filter__select-form {
    display: flex;
    gap: var(--s-2, 8px);
    align-items: center;
  }
  .period-filter__select { width: 100%; }
}

/* Visually hidden label for screen readers — used by the mobile select. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* No-data marker — render in muted tone, never colour-graded. */
.no-data {
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.no-data-row td {
  color: var(--text-muted);
}

.att-metric-row--no-data .att-metric-row__rate { color: var(--text-muted); }

/* ============================================================ Inputs */
.input,
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="search"],
input[type="tel"],
input[type="url"],
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"],
input[type="week"],
textarea,
select {
  /* `display: block` + `min-width: 0` are load-bearing for date/datetime
     inputs. Chromium gives `<input type="date">` a user-agent intrinsic
     min-width based on the locale's formatted date string. Without
     `min-width: 0`, that intrinsic minimum overrides `width: 100%` on
     narrow containers (e.g. inside a modal on a 360 px viewport), and
     the date input renders wider than the sibling text inputs.
     Reproduced 2026-05-26 on the student edit modal. */
  display: block;
  width: 100%;
  min-width: 0;
  max-width: 100%;
  min-height: 44px;
  padding: var(--s-2) var(--s-3);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  outline: none;
  transition: border-color 120ms ease, box-shadow 120ms ease;
  font-family: inherit;
}
textarea { min-height: 96px; padding: var(--s-3); }
/* iOS Safari (WebKit) gives `<input type="date">` a native intrinsic
   min-width derived from the platform's date control. That min-width
   ignores `min-width: 0` and overrides `width: 100%` in narrow
   containers, so the input renders wider than its sibling text fields.
   Stripping the native appearance makes WebKit lay it out as a plain
   text input and respect the width. The calendar-picker indicator
   pseudo-element below still renders on Chromium. DevTools' iOS
   emulation uses Blink, so this only reproduces on a real device. */
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"],
input[type="week"] {
  -webkit-appearance: none;
  appearance: none;
  /* WebKit centres the rendered date inside the control and keeps doing so
     after `appearance: none` — that resets the native *widget*, not the text
     alignment. On iOS the result is a date sitting centred in its box next to
     left-aligned sibling selects, which reads as broken rather than as a
     field. Chromium already left-aligns, so this is a no-op there.
     Reported 2026-08-13 on the /manage/lessons/ filter bar. */
  text-align: left;
}
/* iOS/WebKit does not render `::-webkit-calendar-picker-indicator` at all, so
   the tinting rules below are Chromium-only and an iOS date field carries no
   affordance — nothing marks it as tappable. `-webkit-touch-callout` is
   implemented only by WebKit on iOS, so this feature query targets exactly the
   browsers missing the indicator (including iOS Chrome/Firefox, which are all
   WebKit) without double-drawing an icon next to Chromium's native one.
   The glyph is a fixed slate that holds contrast in both themes; it cannot use
   a CSS variable because a data: URI has no access to custom properties. */
@supports (-webkit-touch-callout: none) {
  input[type="date"],
  input[type="time"],
  input[type="datetime-local"],
  input[type="month"],
  input[type="week"] {
    /* Vertically centre the value. The shared input rule sets `min-height:
       44px` (touch target) with `display: block`, so the content box is taller
       than the single line of text inside it — and a block box aligns its
       inline content to the TOP. Chromium hides this because its native date
       widget centres internally, but `appearance: none` on WebKit strips that
       widget, leaving the text parked against the top padding edge.

       Flex is the fix rather than a line-height calculation: `min-height` means
       the box can grow (long localised date wrapping, a larger accessibility
       text size), and any line-height tuned to 44px would mis-centre the moment
       it does. `align-items: center` stays correct at any height.

       Scoped to this iOS-only block on purpose — the shared rule's
       `display: block` + `min-width: 0` are load-bearing against Chromium's
       intrinsic min-width for date inputs, so `display` must not change there. */
    display: flex;
    align-items: center;
    /* Reserve space so a long localised date can never run under the glyph. */
    padding-right: calc(var(--s-3) + 1.25rem);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cpath d='M16 2v4M8 2v4M3 10h18'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right var(--s-3) center;
    background-size: 1rem 1rem;
  }
}
.input:focus, input:focus, textarea:focus, select:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
/* Date/time inputs render the calendar/clock indicator at the right edge.
   Tint it to the accent so it doesn't read as a foreign default-blue glyph. */
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator,
input[type="datetime-local"]::-webkit-calendar-picker-indicator,
input[type="month"]::-webkit-calendar-picker-indicator,
input[type="week"]::-webkit-calendar-picker-indicator {
  cursor: pointer;
  opacity: 0.6;
  transition: opacity 120ms ease;
  filter: invert(35%) sepia(30%) saturate(800%) hue-rotate(140deg);
}
input[type="date"]:hover::-webkit-calendar-picker-indicator,
input[type="time"]:hover::-webkit-calendar-picker-indicator,
input[type="datetime-local"]:hover::-webkit-calendar-picker-indicator,
input[type="month"]:hover::-webkit-calendar-picker-indicator,
input[type="week"]:hover::-webkit-calendar-picker-indicator {
  opacity: 1;
}
.field { display: block; }
.field__label { display: block; margin-bottom: var(--s-2); font-weight: 600; font-size: 0.95rem; }
.field__hint { color: var(--text-muted); font-size: 0.85rem; margin-top: var(--s-1); }
.field__error { color: var(--danger); font-size: 0.85rem; margin-top: var(--s-1); }

/* ============================================================ Page header
   Standard header block used at the top of a page: small section/role
   badge, h1, optional muted subtitle. Replaces the older `.detail-header`
   pattern which only loaded inside the manage shell. */
.page-header {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  margin-bottom: var(--s-5);
}
.page-header > .badge { align-self: flex-start; }
.page-header h1 { margin: 0; }
.page-header p { margin: 0; }
.page-header .row { gap: var(--s-3); align-items: center; }

/* ============================================================ Form layout
   Vertical stack of label+control rows. Used by Django form-rendering
   templates that emit { label, widget, help, errors } per field. */
.form-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--s-5);
  box-shadow: var(--shadow-sm);
}
@media (max-width: 480px) { .form-card { padding: var(--s-4); } }

.form-row {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
.form-row > label { font-weight: 600; font-size: 0.95rem; }
.form-row > input:not([type="checkbox"]):not([type="radio"]):not([type="color"]),
.form-row > select,
.form-row > textarea {
  width: 100%;
  min-height: 44px;
  padding: var(--s-2) var(--s-3);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  font: inherit;
}
.form-row > textarea { min-height: 96px; padding: var(--s-3); resize: vertical; }
.form-row > input[type="color"] {
  width: 56px; height: 40px;
  padding: 2px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface);
  cursor: pointer;
}
.form-row--checkbox { flex-direction: row; align-items: flex-start; gap: var(--s-3); }
.form-row--checkbox > label { font-weight: 500; display: flex; align-items: center; gap: var(--s-2); }
.form-row--checkbox > p { margin: 0; flex-basis: 100%; }

.form-actions {
  display: flex;
  gap: var(--s-3);
  justify-content: flex-end;
  margin-top: var(--s-5);
  padding-top: var(--s-4);
  border-top: 1px solid var(--border);
}
.form-actions--start { justify-content: flex-start; }
.form-actions--between { justify-content: space-between; }

/* ============================================================ Badge / Pill */
.badge {
  display: inline-flex; align-items: center; gap: var(--s-1);
  padding: 2px var(--s-2);
  border-radius: var(--r-pill);
  font-size: 0.78rem; font-weight: 600;
  background: var(--surface-soft);
  color: var(--text-muted);
  border: 1px solid var(--border);
}
.badge--accent { background: var(--accent-soft); color: var(--accent); border-color: transparent; }
.badge--success { background: var(--success-soft); color: var(--success); border-color: transparent; }
.badge--warn { background: var(--warning-soft); color: var(--warning); border-color: transparent; }
.badge--danger { background: var(--danger-soft); color: var(--danger); border-color: transparent; }
/* Report-publication lifecycle status badges */
.badge--draft { background: var(--surface-soft); color: var(--text-muted); border-color: var(--border); }
.badge--endorsed { background: var(--accent-soft); color: var(--accent); border-color: transparent; }
.badge--published { background: var(--success-soft); color: var(--success); border-color: transparent; }

/* ============================================================ Alert / Banner */
.alert {
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface-soft);
}
.alert--info { background: var(--accent-soft); color: var(--accent); border-color: transparent; }
.alert--error { background: var(--danger-soft); color: var(--danger); border-color: transparent; }
.alert--success { background: var(--success-soft); color: var(--success); border-color: transparent; }

/* Persistent nag banner that asks the user to enable browser notifications. */
.notify-banner {
  background: var(--warning-soft, #fff4e0);
  border-bottom: 1px solid var(--border);
  color: var(--text);
}
.notify-banner__inner {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  max-width: 1200px;
  margin: 0 auto;
}
.notify-banner__icon { font-size: 1.25rem; line-height: 1; }
.notify-banner__copy { display: flex; flex-direction: column; gap: 2px; flex: 1 1 auto; min-width: 0; }
.notify-banner__copy > strong { font-weight: 600; }
.notify-banner__actions { flex-shrink: 0; }
@media (max-width: 600px) {
  .notify-banner__inner { flex-wrap: wrap; }
  .notify-banner__copy { flex-basis: 100%; }
}

/* Soft accent-tinted nudge to enable a passkey. Same structural shape as
   .notify-banner but tinted teal because it's a positive security nudge,
   not a warning. */
.passkey-banner {
  background: var(--accent-soft);
  border-bottom: 1px solid var(--border);
  color: var(--text);
}
.passkey-banner__inner {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  max-width: 1200px;
  margin: 0 auto;
}
.passkey-banner__icon { font-size: 1.25rem; line-height: 1; flex-shrink: 0; }
.passkey-banner__copy {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1 1 auto;
  min-width: 0;
}
.passkey-banner__copy > strong { font-weight: 600; }
.passkey-banner__actions {
  display: flex;
  gap: var(--s-2);
  align-items: center;
  flex-shrink: 0;
}
.passkey-banner__actions form.inline { display: inline; margin: 0; }
@media (max-width: 600px) {
  .passkey-banner__inner { flex-wrap: wrap; }
  .passkey-banner__copy { flex-basis: 100%; }
  .passkey-banner__actions { flex-basis: 100%; }
}

/* ============================================================ Tables */
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { padding: var(--s-3); border-bottom: 1px solid var(--border); text-align: left; }
.table th { font-weight: 600; color: var(--text-muted); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.04em; }
/* Horizontal-scroll wrapper for wide list tables on narrow viewports.
   Pair with `<div class="table-wrap">` around any `<table class="table">`.
   On mobile, the table scrolls inside the wrapper; the surrounding card
   stays put. Tap targets in the table keep their full size. */
.table-wrap {
  overflow-x: auto;
  /* No -webkit-overflow-scrolling: touch. It is obsolete (iOS 13+ scrolls
     with momentum natively) and on iOS it promoted this scroller to its own
     compositing layer, which then CLIPPED position:fixed descendants — the
     row kebab's panel rendered as a sliver cut off at the card edge, while
     desktop Chrome was fine. See static/js/actions-menu.js. */
}
/* When the wrap is a direct child of a card, bleed to the card's
   padding edges so the scroll affordance reads as table-only and not as
   horizontal page scroll. */
.card > .table-wrap {
  margin-inline: calc(var(--card-pad, var(--s-5)) * -1);
  padding-inline: var(--card-pad, var(--s-5));
}
.table-wrap > .table { min-width: 560px; }
/* Optional helper for action cells containing two+ inline buttons.
   Add `class="td-actions"` on the <td> to gain 8px gap + wrap so a
   second button drops to the next line on narrow screens instead of
   forcing the table wider.

   When applied to a <td>, we cannot use `display: flex` directly on the cell
   — that overrides `display: table-cell` and pulls the cell out of the
   table-row layout, breaking vertical alignment and the row's bottom border.
   So: keep the cell as `table-cell`, lay children out inline-block with
   margin-based spacing, and rely on natural inline-block wrap on narrow
   viewports. The flex variant still applies when the class is used on a
   non-cell wrapper (e.g. `<div class="td-actions">`). */
.td-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;
}
td.td-actions {
  display: table-cell;
  vertical-align: middle;
}
td.td-actions > * {
  display: inline-block;
  vertical-align: middle;
}
/* Buttons need their `inline-flex` from `.btn` back to keep
   `align-items: center` working for both <a> and <button>. The generic
   `inline-block` override above sits `<a>` text at the top of the box
   because anchors have no UA centring (unlike <button>). */
td.td-actions > .btn {
  display: inline-flex;
}
td.td-actions > * + * {
  margin-left: var(--s-2);
}

/* ============================================================ Colour swatch
   Inline coloured rectangle used in list views (e.g. houses, subjects) to
   show the configured colour without forcing the admin to mentally parse
   a hex code. */
.swatch {
  display: inline-block;
  width: 18px; height: 18px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  vertical-align: middle;
  margin-right: var(--s-1);
}
.swatch--lg { width: 28px; height: 28px; border-radius: var(--r-md); }

/* ============================================================ Plain data list */
.data-list { list-style: none; padding: 0; margin: 0; }
.data-list > li { padding: var(--s-2) 0; border-bottom: 1px solid var(--border); }
.data-list > li:last-child { border-bottom: 0; }

/* ============================================================ Data table
   Used for the bell-schedule periods table and any future read+edit table
   surfaces. Report-card overrides specific cells; this is the base. */
.data-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
}
.data-table thead th {
  text-align: left;
  font-weight: 600;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  background: var(--surface-soft);
  padding: var(--s-2) var(--s-3);
  border-bottom: 1px solid var(--border);
}
.data-table tbody td {
  padding: var(--s-3);
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
.data-table tbody tr:last-child td { border-bottom: 0; }
.data-table tbody tr:hover { background: var(--surface-soft); }
.data-table tbody tr.is-dragging { opacity: 0.5; }
.data-table .drag-handle {
  width: 32px;
  color: var(--text-muted);
  font-size: 1.1rem;
  text-align: center;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  /* A touch that starts on the handle is a drag, not a page scroll/zoom — the
     browser commits to no-scroll for the whole gesture from this element. */
  touch-action: none;
  cursor: grab;
}
.data-table .drag-handle:active { cursor: grabbing; }
/* Don't paint a text selection over the cells while dragging a row by touch. */
.data-table tbody[data-reorder-url] {
  -webkit-user-select: none;
  user-select: none;
}
.data-table__actions {
  display: flex;
  gap: var(--s-2);
  justify-content: flex-end;
}

/* Horizontal-scroll wrapper for tables that may exceed viewport width on
   narrow screens — wraps the whole <table> so the inner layout is
   preserved without forcing line-wraps. */
.table-wrap {
  overflow-x: auto;
  /* No -webkit-overflow-scrolling: touch. It is obsolete (iOS 13+ scrolls
     with momentum natively) and on iOS it promoted this scroller to its own
     compositing layer, which then CLIPPED position:fixed descendants — the
     row kebab's panel rendered as a sliver cut off at the card edge, while
     desktop Chrome was fine. See static/js/actions-menu.js. */
  border-radius: var(--r-md);
}
.table-wrap .data-table { border-radius: 0; border-left: 0; border-right: 0; }

/* Kind pills for bell-period rows (.kind-pill--lesson / --break / --assembly / --other) */
.kind-pill {
  display: inline-block;
  padding: 2px var(--s-2);
  border-radius: var(--r-pill);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.kind-pill--lesson   { background: var(--accent-soft);  color: var(--accent); }
.kind-pill--break    { background: var(--surface-sunken); color: var(--text-muted); }
.kind-pill--assembly { background: var(--success-soft); color: var(--success); }
.kind-pill--other    { background: var(--surface-sunken); color: var(--text-muted); }

/* Mobile: tighten the table, drop horizontal padding so we can fit more,
   let the action buttons wrap to a second line if needed. */
@media (max-width: 600px) {
  .data-table thead th,
  .data-table tbody td { padding: var(--s-2); }
  .data-table .drag-handle { width: 24px; padding-left: var(--s-1); padding-right: var(--s-1); }
  .data-table__actions { flex-wrap: wrap; justify-content: flex-start; }
  .kind-pill { font-size: 0.72rem; padding: 1px var(--s-1); }
}

/* ============================================================ Report card
   Used by /gradebook/reports/.../preview/ and /reports/student/.../. The
   stylesheet here covers both on-screen and print rendering. */
.report-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--s-5);
  margin-bottom: var(--s-4);
  box-shadow: var(--shadow-sm);
}
.report-card h2 { margin: 0 0 var(--s-1); }
.report-card .meta { color: var(--text-muted); font-size: 0.92rem; margin-bottom: var(--s-3); }
.report-card .data-table th { width: 200px; }
@media print {
  .no-print { display: none !important; }
  .report-card { page-break-after: always; box-shadow: none; border-color: #ddd; }
  body { background: #fff; }
  .nav, .site-footer, .evac-fab, #offline-banner { display: none !important; }
}

/* Period label as a secondary line beside the time in the timetable grid —
   the time answers "when", the label is what the school says out loud. */
.timetable-grid__period {
  display: inline-block;
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--text-muted);
  white-space: nowrap;
}
.timetable-grid--wide .timetable-grid__period { display: block; }

/* ==================================================== Free-periods day groups
   Used by the pupil timetable's free-periods band: a muted day label above a
   wrapping row of period pills. */
.free-day + .free-day { margin-top: var(--s-4); }
.free-day__label {
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  margin-bottom: var(--s-2);
}

/* ============================================================ Evacuation FAB
   Hold-to-trigger emergency button, fixed bottom-right on every page.
   Visible to admin + secretary roles when no evacuation is active. */
.evac-fab {
  position: fixed;
  bottom: var(--evac-fab-offset);
  right: max(30px, env(safe-area-inset-right, 0px));
  z-index: 60;
  margin: 0;
  display: flex;
  /* Reverse so the circle (button) anchors to the right and the hint pill
     appears to its left when revealed. */
  flex-direction: row-reverse;
  align-items: center;
  gap: var(--s-2);
  pointer-events: auto;
}
.evac-fab__hint {
  background: hsl(0 0% 100% / 0.9);
  backdrop-filter: blur(6px);
  color: hsl(0 60% 35%);
  font-size: 0.78rem;
  font-weight: 600;
  padding: 4px 10px;
  border-radius: var(--r-pill);
  border: 1px solid hsl(0 30% 80%);
  white-space: nowrap;
  opacity: 0;
  /* Slides in from the right (toward the circle) when revealed */
  transform: translateX(8px);
  transition: opacity 200ms ease, transform 200ms ease;
  pointer-events: none;
}
.evac-fab:hover .evac-fab__hint,
.evac-fab:focus-within .evac-fab__hint { opacity: 1; transform: translateX(0); }

.evac-fab__btn {
  position: relative;
  width: var(--evac-fab-size); height: var(--evac-fab-size);
  border-radius: 50%;
  border: 0;
  background: #b71c1c;
  color: #fff;
  cursor: pointer;
  box-shadow: 0 4px 14px hsl(0 60% 30% / 0.35), 0 2px 4px hsl(0 60% 20% / 0.2);
  /* The opacity leg is delayed so the button reappears as the overlay's disc
     lands back in the corner on release — see `.is-holding` below. */
  transition: transform 120ms ease, box-shadow 120ms ease, background 120ms ease,
              opacity 100ms ease 220ms;
  -webkit-tap-highlight-color: transparent;
  touch-action: none;
  /* A long press on iOS otherwise raises the selection callout mid-hold. */
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
.evac-fab__btn:hover { background: #c62828; transform: translateY(-1px); }
.evac-fab__btn:active { transform: translateY(0); }
.evac-fab__btn:focus-visible { outline: 3px solid hsl(0 80% 70%); outline-offset: 2px; }

.evac-fab__icon {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-size: 1.6rem; font-weight: 900;
  line-height: 1;
}
.evac-fab__ring {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
}
.evac-fab__ring circle {
  stroke: hsl(0 0% 100% / 0.25);
}
/* Held: the overlay's disc takes over. It starts life exactly on top of this
   button — same centre, same diameter — and flies to the middle of the screen,
   so the button must vanish instantly (no transition delay) or there would be
   two of it for the length of the flight. It stays the pointer target the
   whole time; opacity doesn't affect hit testing. */
.evac-fab__btn.is-holding,
.evac-fab__btn.is-firing { opacity: 0; transition: opacity 0s; }
/* Hover leaves the hint pill up; it has no business over the scrim. */
.evac-fab__btn.is-holding + .evac-fab__hint,
.evac-fab__btn.is-firing + .evac-fab__hint { opacity: 0; }
@media (max-width: 480px) {
  /* Set on the token, not on .evac-fab__btn, so anything else deriving from the
     FAB's diameter follows it down at this breakpoint automatically. */
  :root { --evac-fab-size: 52px; }
  .evac-fab__icon { font-size: 1.4rem; }
}
@media (prefers-reduced-motion: reduce) {
  /* The global reduce block collapses every transition to 0.01ms, which would
     also collapse the countdown sweep — that ring is the only signal of how
     much hold is left, so it stays timed. The swell becomes an instant step. */
  .evac-hold.is-visible .evac-hold__progress {
    transition: stroke-dashoffset 2.5s linear !important;
  }
}

/* ============================================================ Evacuation hold overlay
   Full-screen countdown while the FAB is held. Triggering an evacuation is the
   one action where nothing else on the page matters, so the hold takes the
   whole screen and the countdown ring gets as much of it as it can use —
   readable at a glance on a phone, where the FAB's own ring is under a thumb.

   Purely decorative: the FAB button stays the pointer target (see the
   pointer-events note below), and `.evac-fab` sits one z-index above so the
   circle under the finger stays lit above the scrim. */
.evac-hold {
  position: fixed; inset: 0;
  z-index: 59;
  display: grid; place-items: center;
  padding: var(--s-6);
  background: hsl(0 45% 7% / 0.97);
  backdrop-filter: blur(6px);
  /* A mouseup landing on this instead of on the button would strand the hold
     with the overlay stuck up — never take the pointer. */
  pointer-events: none;
  opacity: 0;
  /* visibility rather than display keeps the ring in the layout, so the sweep
     has a starting value to transition from on the very first hold. */
  visibility: hidden;
  /* The visibility delay has to outlast the disc's flight back to the corner,
     or it gets cut off mid-air on release. */
  transition: opacity 200ms ease, visibility 0s linear 280ms;
}
.evac-hold.is-visible {
  opacity: 1;
  visibility: visible;
  transition: opacity 200ms ease, visibility 0s;
}
.evac-hold__inner {
  --evac-hold-size: min(62vw, 46vh, 300px);
  display: flex; flex-direction: column; align-items: center;
  gap: var(--s-4);
  text-align: center;
}
/* The text is stationary — the disc is what moves — so it just fades up, a
   beat behind the flight so it doesn't compete with it. */
.evac-hold__eyebrow,
.evac-hold__title,
.evac-hold__sub {
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 160ms ease, transform 160ms ease;
}
.evac-hold.is-visible .evac-hold__eyebrow,
.evac-hold.is-visible .evac-hold__title,
.evac-hold.is-visible .evac-hold__sub {
  opacity: 1;
  transform: none;
  transition: opacity 220ms ease 140ms, transform 220ms ease 140ms;
}
.evac-hold__eyebrow {
  margin: 0;
  font-size: 0.75rem; font-weight: 700;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: hsl(0 0% 100% / 0.65);
}
/* The flight. JS measures the FAB on each hold and writes the three custom
   properties below, so the disc's resting state IS the FAB — same centre, same
   diameter — and `.is-visible` releases it to the middle of the screen. The
   fallback values only matter if the class somehow lands before the measure;
   they keep it a small circle rather than a full-screen pop. */
.evac-hold__ring-wrap {
  position: relative;
  width: var(--evac-hold-size); height: var(--evac-hold-size);
  transform: translate(var(--evac-hold-from-x, 0px), var(--evac-hold-from-y, 0px))
             scale(var(--evac-hold-from-scale, 0.2));
  transition: transform 260ms ease;
}
.evac-hold.is-visible .evac-hold__ring-wrap {
  transform: none;
  /* A touch of overshoot on arrival — it lands like a physical thing. */
  transition: transform 340ms cubic-bezier(0.34, 1.2, 0.64, 1);
}
/* The countdown has no meaning until the disc is roughly in place, and a
   ring scaled down to FAB size would just be a thick smudge. */
.evac-hold__ring {
  width: 100%; height: 100%; display: block;
  /* Above the face, or the face's red glow (absolutely positioned, so it would
     otherwise paint over this static SVG) washes the sweep out to pink. */
  position: relative; z-index: 1;
  opacity: 0;
  transition: opacity 160ms ease;
}
.evac-hold.is-visible .evac-hold__ring { opacity: 1; transition: opacity 240ms ease 120ms; }
/* The button face — same palette and dome as the FAB, sized to sit inside the
   countdown ring with a clear gap so the sweep never reads as a rim. Radius
   maths: the ring is at r=54/60 with a 4-wide stroke, so its inner edge is at
   ~87% of the wrap; 13% inset puts the face at 74%, leaving the sweep clear
   air rather than making it look like the button's rim. */
.evac-hold__face {
  position: absolute; inset: 13%;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 32%, #d94141 0%, #c62828 45%, #9e1616 100%);
  box-shadow:
    inset 0 -8px 20px hsl(0 65% 10% / 0.5),   /* underside shadow — domed, not flat */
    inset 0 6px 16px hsl(0 85% 80% / 0.22),   /* top highlight */
    0 12px 34px hsl(0 70% 8% / 0.65),         /* cast shadow onto the scrim */
    0 0 0 1px hsl(0 60% 18% / 0.9);           /* rim, so the edge reads on dark */
  animation: evac-hold-glow 2.4s ease-in-out infinite;
}
@keyframes evac-hold-glow {
  0%, 100% { box-shadow:
    inset 0 -8px 20px hsl(0 65% 10% / 0.5), inset 0 6px 16px hsl(0 85% 80% / 0.22),
    0 12px 34px hsl(0 70% 8% / 0.65), 0 0 0 1px hsl(0 60% 18% / 0.9),
    0 0 30px 4px hsl(0 80% 45% / 0.25); }
  50%      { box-shadow:
    inset 0 -8px 20px hsl(0 65% 10% / 0.5), inset 0 6px 16px hsl(0 85% 80% / 0.22),
    0 12px 34px hsl(0 70% 8% / 0.65), 0 0 0 1px hsl(0 60% 18% / 0.9),
    0 0 46px 10px hsl(0 80% 45% / 0.45); }
}
.evac-hold__track { stroke: hsl(0 0% 100% / 0.16); }
.evac-hold__progress {
  stroke: #fff;
  stroke-linecap: round;
  /* Only applies on release — `.is-visible` below swaps in the 2.5s sweep. */
  transition: stroke-dashoffset 220ms ease, stroke 200ms ease;
}
.evac-hold.is-visible .evac-hold__progress {
  transition: stroke-dashoffset 2.5s linear, stroke 200ms ease;
  stroke-dashoffset: 0;
}
.evac-hold__mark {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-size: calc(var(--evac-hold-size) * 0.4);
  font-weight: 900; line-height: 1;
  color: #fff;
}
.evac-hold__title {
  margin: 0;
  font-size: 1.25rem; font-weight: 700;
  color: #fff;
}
.evac-hold__sub {
  margin: calc(var(--s-4) * -0.5) 0 0;
  font-size: 0.9rem;
  color: hsl(0 0% 100% / 0.55);
}
/* Commit: the ring completes in green and the copy switches from an
   instruction to a status, since there is nothing left to cancel while the
   POST navigates. */
.evac-hold__title--firing { display: none; }
.evac-hold.is-firing .evac-hold__title--holding { display: none; }
.evac-hold.is-firing .evac-hold__title--firing { display: block; }
.evac-hold.is-firing .evac-hold__sub { visibility: hidden; }
.evac-hold.is-firing .evac-hold__progress { stroke: hsl(120 50% 60%); stroke-dashoffset: 0; }
/* The face follows the FAB's own commit colour, and the danger glow stops —
   the decision is made, there is nothing left to warn about. */
.evac-hold.is-firing .evac-hold__face {
  background: radial-gradient(circle at 50% 32%, hsl(120 45% 42%) 0%, hsl(120 50% 33%) 45%, hsl(120 55% 22%) 100%);
  animation: none;
  box-shadow:
    inset 0 -8px 20px hsl(120 60% 8% / 0.5),
    inset 0 6px 16px hsl(120 60% 80% / 0.22),
    0 12px 34px hsl(120 60% 6% / 0.6),
    0 0 0 1px hsl(120 45% 16% / 0.9);
}

/* ============================================================ Settings sub-page link list
   Used on settings/index pages to point at sub-pages (e.g. email config,
   integrations). Each item is a full-row clickable link with title +
   description and a trailing arrow. */
.settings-links { list-style: none; padding: 0; margin: 0; }
.settings-links > li { border-bottom: 1px solid var(--border); }
.settings-links > li:last-child { border-bottom: 0; }
.settings-link {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-4);
  padding: var(--s-3) 0;
  width: 100%;
  background: transparent;
  border: 0;
  text-align: inherit;
  cursor: pointer;
  font: inherit;
  color: inherit; text-decoration: none;
  transition: color 120ms ease;
}
.settings-link:hover { color: var(--accent); }
.settings-link:hover .settings-link__arrow { transform: translateX(2px); }
.settings-link__arrow {
  flex: 0 0 auto;
  font-size: 1.2rem; color: var(--text-muted);
  transition: transform 120ms ease;
}
.settings-link:hover .settings-link__arrow { color: var(--accent); }

/* ============================================================ Top navbar */
.nav {
  position: sticky; top: 0; z-index: 50;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  /* Notch / status-bar safe area on iOS */
  padding-top: env(safe-area-inset-top);
  /* Subtle elevation that fades at top of scroll for depth */
  box-shadow: 0 1px 0 hsl(20 10% 10% / 0);
  transition: box-shadow 200ms ease;
}
.nav__inner {
  display: flex; align-items: center; gap: var(--s-3);
  max-width: 1100px; margin: 0 auto;
  padding: var(--s-2) clamp(16px, 4vw, 32px);
  min-height: 56px;
}
.nav__brand {
  display: flex; align-items: center; gap: var(--s-2);
  font-weight: 700; color: var(--text); text-decoration: none;
  font-size: 1rem;
  min-height: 44px;
  padding-right: var(--s-2);
  border-radius: var(--r-md);
}
.nav__brand:hover { text-decoration: none; }
.nav__brand-mark {
  width: 32px; height: 32px;
  border-radius: var(--r-md);
  flex: 0 0 auto;
  /* Slight padding via background so the colored mark gets a soft halo on busy bgs */
  background: transparent;
}
/* Hide ClassPulse wordmark on small screens — school name is enough context */
.nav__brand-name { display: none; }
.nav__brand-school {
  color: var(--text);
  font-weight: 600;
  /* Two-word school names ("Acme High") wrapped and made the brand block two
     lines tall. Truncate instead — same pattern as .nav__user-email. */
  max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (min-width: 480px) {
  .nav__brand-name { display: inline; }
  .nav__brand-school {
    color: var(--text-muted); font-weight: 500;
    border-left: 1px solid var(--border-strong);
    padding-left: var(--s-3); margin-left: var(--s-2);
  }
}
.nav__links {
  display: none;
  gap: var(--s-1); margin-left: var(--s-3);
}
.nav__links a {
  display: inline-flex; align-items: center;
  min-height: 40px;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-md);
  color: var(--text-muted);
  text-decoration: none;
  font-weight: 500;
  /* Multi-word labels ("My Schedule", "Vehicle lookup", "Gate (entry)") must
     never wrap: a wrapped label doubles the nav bar's height and makes the
     active pill two lines tall. */
  white-space: nowrap;
  transition: background 120ms ease, color 120ms ease;
}
.nav__links a:hover { color: var(--text); background: var(--surface-soft); text-decoration: none; }
.nav__links a:active { background: var(--surface-sunken); }
.nav__links a[aria-current="page"] {
  background: var(--accent-soft); color: var(--accent);
}

/* Nav dropdown groups (desktop): a button toggles a dropdown menu of links. */
.nav__group { position: relative; }
.nav__group-toggle {
  display: inline-flex; align-items: center; gap: var(--s-1);
  min-height: 40px;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-md);
  color: var(--text-muted);
  font-weight: 500;
  white-space: nowrap;
  font-family: inherit;
  font-size: inherit;
  background: transparent;
  border: 0;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.nav__group-toggle:hover { color: var(--text); background: var(--surface-soft); }
.nav__group-toggle[aria-current="page"] { background: var(--accent-soft); color: var(--accent); }
.nav__group-toggle[aria-expanded="true"] { background: var(--surface-soft); color: var(--text); }
.nav__group-caret {
  transition: transform 160ms ease;
  margin-left: 2px;
}
.nav__group-toggle[aria-expanded="true"] .nav__group-caret { transform: rotate(180deg); }
.nav__group-menu {
  display: none;
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  min-width: 200px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-md);
  padding: var(--s-1);
  z-index: 50;
}
.nav__group-toggle[aria-expanded="true"] + .nav__group-menu { display: block; }
.nav__group-menu a {
  display: block;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  color: var(--text-muted);
  text-decoration: none;
  font-weight: 500;
  white-space: nowrap;
}
.nav__group-menu a:hover { color: var(--text); background: var(--surface-soft); text-decoration: none; }
.nav__group-menu a[aria-current="page"] { color: var(--accent); background: var(--accent-soft); }
.nav__group-menu-form { margin: 0; padding: 0; }
.nav__group-menu-button {
  display: block; width: 100%; text-align: left;
  padding: var(--s-2) var(--s-3);
  border: 0; background: transparent;
  border-radius: var(--r-sm);
  color: var(--text-muted);
  font: inherit; font-weight: 500; font-size: 1rem;
  white-space: nowrap;
  cursor: pointer;
}
.nav__group-menu-button:hover {
  color: var(--danger);
  background: var(--danger-soft);
}

/* Mobile drawer section grouping */
.nav__drawer-section { margin: var(--s-3) 0; }
.nav__drawer-section-title {
  display: block;
  padding: var(--s-2) var(--s-3) 4px;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-subtle);
}

.nav__spacer { flex: 1; }
.nav__user {
  display: none;
  align-items: center; gap: var(--s-1);
  position: relative;
  padding-left: var(--s-2);
  margin-left: var(--s-1);
}
.nav__user::before {
  content: ""; position: absolute;
  left: 0; top: 20%; bottom: 20%;
  width: 1px; background: var(--border);
}
.nav__user-email {
  font-size: 0.9rem; color: var(--text-muted);
  max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nav__user .btn[aria-current="page"] {
  background: var(--accent-soft); color: var(--accent);
}
.nav__logout-form { margin: 0; }
/* User dropdown anchors to the right edge of the nav, unlike the
   left-anchored Insights/Setup groups. */
.nav__user .nav__group-menu { left: auto; right: 0; }
.nav__user-toggle {
  background: transparent;
  border: 0;
  cursor: pointer;
  color: inherit;
  font: inherit;
  padding: var(--s-1) var(--s-2);
  border-radius: var(--r-md);
  display: inline-flex; align-items: center; gap: var(--s-1);
}
.nav__user-toggle:hover { background: var(--surface-soft); }

/* Hamburger toggle — 44×44 with animated bars that morph to X */
.nav__toggle {
  display: inline-grid; place-items: center;
  width: 44px; height: 44px;
  border-radius: var(--r-md);
  margin-left: auto;
  cursor: pointer;
  color: var(--text);
}
.nav__toggle:hover { background: var(--surface-soft); }
.nav__toggle:active { background: var(--surface-sunken); }
.nav__toggle-bars {
  position: relative;
  width: 22px; height: 16px;
}
.nav__toggle-bars > span {
  position: absolute; left: 0; right: 0; height: 2px;
  background: currentColor;
  border-radius: 2px;
  transition: transform 200ms ease, opacity 200ms ease, top 200ms ease;
}
.nav__toggle-bars > span:nth-child(1) { top: 0; }
.nav__toggle-bars > span:nth-child(2) { top: 7px; }
.nav__toggle-bars > span:nth-child(3) { top: 14px; }
.nav.is-open .nav__toggle-bars > span:nth-child(1) { top: 7px; transform: rotate(45deg); }
.nav.is-open .nav__toggle-bars > span:nth-child(2) { opacity: 0; }
.nav.is-open .nav__toggle-bars > span:nth-child(3) { top: 7px; transform: rotate(-45deg); }

@media (min-width: 768px) {
  .nav__links { display: inline-flex; }
  .nav__user { display: inline-flex; }
  .nav__toggle { display: none; }
}

/* Mobile drawer — toggled by `.nav.is-open` class via JS.
   Drawer floats over content (position: absolute) so it doesn't push main down. */
.nav__drawer {
  display: none;
  position: absolute;
  top: 100%; left: 0; right: 0;
  z-index: 50;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  padding: var(--s-2) clamp(16px, 4vw, 32px) calc(var(--s-4) + env(safe-area-inset-bottom));
  box-shadow: var(--shadow-md);
  max-height: calc(100dvh - 56px);
  overflow-y: auto;
}
.nav.is-open .nav__drawer { display: block; }
.nav__drawer-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--s-2) 0;
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--s-2);
}
.nav__drawer-close {
  display: inline-grid; place-items: center;
  width: 44px; height: 44px;
  margin-right: -8px;
  border-radius: var(--r-md);
  cursor: pointer; color: var(--text);
}
.nav__drawer-close:hover { background: var(--surface-soft); }
.nav__drawer-close svg { width: 20px; height: 20px; }
.nav__drawer > a,
.nav__drawer-section > a {
  display: block;
  padding: var(--s-3) 0;
  color: var(--text);
  border-bottom: 1px solid var(--border);
  text-decoration: none;
  font-weight: 500;
  min-height: 48px;
}
.nav__drawer > a:hover,
.nav__drawer-section > a:hover { background: var(--surface-soft); }
.nav__drawer > a:active,
.nav__drawer-section > a:active { background: var(--surface-sunken); }
.nav__drawer > a[aria-current="page"],
.nav__drawer-section > a[aria-current="page"] {
  color: var(--accent);
  border-left: 3px solid var(--accent);
  padding-left: var(--s-2);
}
/* Sign-out section: spatial separation from regular nav per #9 destructive-separation */
.nav__drawer-meta {
  margin-top: var(--s-4);
  padding-top: var(--s-3);
  border-top: 1px solid var(--border);
}
.nav__drawer form { margin-top: var(--s-3); }
@media (min-width: 768px) {
  .nav__drawer, .nav__toggle-input:checked ~ .nav__drawer { display: none; }
}

/* ============================================================ Auth shell (login pages) */
.auth-shell {
  display: grid; place-items: center;
  min-height: calc(100dvh - 64px);
  padding: var(--s-5) clamp(16px, 4vw, 32px);
}
.auth-card {
  width: 100%; max-width: 420px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--s-6);
  box-shadow: var(--shadow-md);
}
.auth-card h1 { margin-bottom: var(--s-2); }
.auth-card .lede { color: var(--text-muted); margin-bottom: var(--s-5); }
.auth-card__brand {
  display: grid; place-items: center;
  margin-bottom: var(--s-2);
}
.auth-card__brand img {
  width: 48px; height: 48px;
  border-radius: var(--r-md);
}
.auth-card .or-divider {
  display: flex; align-items: center; gap: var(--s-3);
  color: var(--text-subtle); font-size: 0.85rem; margin: var(--s-4) 0;
}
.auth-card .or-divider::before,
.auth-card .or-divider::after {
  content: ""; flex: 1; height: 1px; background: var(--border);
}

/* ============================================================ Action cards (home) */
.action-card {
  display: block;
  padding: var(--s-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  text-decoration: none;
  color: inherit;
  transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;
}
.action-card:hover {
  border-color: var(--accent);
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
  text-decoration: none;
}
.action-card__icon {
  display: inline-grid; place-items: center;
  width: 44px; height: 44px;
  border-radius: var(--r-md);
  background: var(--accent-soft); color: var(--accent);
  margin-bottom: var(--s-3);
}
.action-card__icon svg { width: 22px; height: 22px; }
.action-card h3 { margin: 0 0 var(--s-1); }
.action-card p { margin: 0; color: var(--text-muted); }

/* ============================================================ Radio cards (settings UI prefs) */
.radio-cards { display: grid; gap: var(--s-2); }
.radio-card {
  display: flex; gap: var(--s-3); align-items: flex-start;
  padding: var(--s-3) var(--s-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  cursor: pointer;
}
.radio-card:hover { border-color: var(--border-strong); }
.radio-card input { margin-top: 4px; accent-color: var(--accent); }
.radio-card:has(input:checked) {
  border-color: var(--accent); background: var(--accent-soft);
}
.radio-card__title { font-weight: 600; }
.radio-card__hint { color: var(--text-muted); font-size: 0.9rem; }

/* ============================================================ A11y / motion */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
.action-card:focus-visible {
  outline-offset: 4px;
  border-color: var(--accent);
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ============================================================ Lesson card (on /today/) */
.lesson-card {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  text-decoration: none;
  color: inherit;
  transition: border-color 120ms ease, box-shadow 120ms ease, transform 120ms ease;
}
.lesson-card:hover {
  border-color: var(--accent);
  box-shadow: var(--shadow-md);
  text-decoration: none;
  transform: translateY(-1px);
}
.lesson-card:active { transform: translateY(0); }
.lesson-card--done { background: linear-gradient(180deg, var(--success-soft), var(--surface) 60%); }
.lesson-card--partial { background: linear-gradient(180deg, var(--warning-soft), var(--surface) 60%); }
.lesson-card--now { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }

.lesson-card__time {
  display: flex; flex-direction: column; align-items: flex-start; min-width: 64px;
  font-variant-numeric: tabular-nums;
  border-right: 1px solid var(--border);
  padding-right: var(--s-3);
}
.lesson-card__time-h {
  font-weight: 700; font-size: 1.4rem; line-height: 1;
  color: var(--text);
}
.lesson-card__time-end {
  color: var(--text-muted); font-size: 0.78rem; margin-top: 2px;
}
.lesson-card__body { min-width: 0; }
.lesson-card__title {
  font-weight: 600; font-size: 1.05rem;
  display: flex; align-items: center; gap: var(--s-2);
}
.lesson-card__sub { color: var(--text-muted); font-size: 0.9rem; margin-top: 2px; }
.lesson-card__chev { width: 18px; height: 18px; color: var(--text-subtle); }
.lesson-card:hover .lesson-card__chev { color: var(--accent); }

/* Wrapper around a lesson card so admin-only actions can sit attached to
   the card without nesting inside the <a>. The card stays the main click
   target; the action button hangs in the top-right corner. */
.lesson-card-wrap { position: relative; }
.lesson-card__admin-actions {
  position: absolute;
  top: var(--s-2);
  right: var(--s-2);
  z-index: 2;
  display: flex;
  gap: var(--s-2);
}
.lesson-card__admin-actions .btn {
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.lesson-card__admin-actions .btn:hover {
  background: var(--surface-soft);
  border-color: var(--accent);
  color: var(--accent);
}

@media (max-width: 480px) {
  .lesson-card { grid-template-columns: auto 1fr auto; gap: var(--s-2); padding: var(--s-3); }
  .lesson-card__chev { display: none; }
  .lesson-card__time { min-width: 56px; padding-right: var(--s-2); }
  .lesson-card__time-h { font-size: 1.2rem; }
  /* On phones, drop the action to a full-width row below the card so it
     doesn't overlap the title. */
  .lesson-card__admin-actions {
    position: static;
    margin-top: var(--s-2);
    justify-content: flex-end;
  }
}

/* ============================================================ Mark page header */
.mark-head {
  display: flex; gap: var(--s-3); align-items: flex-end; justify-content: space-between;
  flex-wrap: wrap;
  margin-top: var(--s-2);
}
.mark-head__counter {
  display: flex; align-items: baseline; gap: var(--s-2);
  font-variant-numeric: tabular-nums;
  font-weight: 700; font-size: 1.4rem; color: var(--text);
}
.mark-progress {
  margin-top: var(--s-3);
  height: 6px; background: var(--surface-sunken); border-radius: var(--r-pill);
  overflow: hidden;
}
.mark-progress__bar {
  height: 100%; background: var(--accent); border-radius: inherit;
  transition: width 240ms ease;
}

/* ============================================================ Attendance row (shared) */
.att-row {
  display: flex; align-items: center; gap: var(--s-3);
  width: 100%;
  padding: var(--s-3);
  border-radius: var(--r-md);
  transition: background 150ms ease;
}
.att-row__student {
  display: flex; align-items: center; gap: var(--s-3);
  flex: 1; min-width: 0;
}
.att-row__name {
  font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.att-row__actions { display: flex; gap: var(--s-2); flex: 0 0 auto; }
.att-row--P { background: var(--success-soft); }
.att-row--L { background: var(--warning-soft); }
.att-row--A { background: var(--danger-soft); }
.att-row__reason-tag {
  color: var(--text-muted);
  font-size: 0.85rem;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 0 1 auto; min-width: 0;
}
/* Absence reason picker — single shared modal opened by every "A" pill */
.absent-reason-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--s-2);
  margin-bottom: var(--s-4);
}
.absent-reason-btn {
  padding: var(--s-3) var(--s-2);
  min-height: 56px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  color: var(--text);
  font-weight: 500;
  font-size: 0.95rem;
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.absent-reason-btn:hover { background: var(--danger-soft); border-color: var(--danger); }
.absent-reason-btn.is-current {
  background: var(--danger); color: white; border-color: var(--danger);
}
@media (max-width: 480px) {
  .absent-reason-grid { grid-template-columns: 1fr 1fr; }
}

/* Every interactive control inside an attendance row shares ONE height, so the
   P/L/A/VL pills and the reason <select> beside them line up.
   Without this the select falls through to the global 44px input min-height
   while the pills are 48px, and the 4px difference reads as sloppy when they
   sit side by side on the admin mark sheet. Declared on the row (not on each
   control) so a context that wants bigger controls — the roll-call card —
   changes one value and everything in the row follows. */
.att-row { --att-control-h: 48px; }
.att-row__reason select { min-height: var(--att-control-h, 48px); }
/* The mark-sheet row applies this modifier whenever the student is unmarked or
   Present, but the rule was never written — so the reason picker rendered on
   every row regardless, including rows with nothing to explain. `hx-include`
   targets the <select> by id and does not care about visibility, so hiding it
   keeps the reason submitting normally once a row is marked A/L/VL. */
.att-row__reason--hidden { display: none; }

/* Attendance pill — touch-friendly toggle */
.att-pill {
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
  min-width: 56px;
  /* Fallback covers a pill rendered outside .att-row, where the custom
     property would be undefined and the declaration invalid. */
  min-height: var(--att-control-h, 48px);
  padding: 4px 10px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  color: var(--text-muted);
  font-weight: 600;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease, transform 80ms ease;
  user-select: none;
}
.att-pill:hover { color: var(--text); border-color: var(--text-muted); }
.att-pill:active { transform: scale(0.97); }
.att-pill__letter { font-size: 1rem; line-height: 1; }
.att-pill__label { font-size: 0.7rem; font-weight: 500; opacity: 0.7; }
.att-pill--p.is-on { background: var(--success); color: white; border-color: var(--success); }
.att-pill--l.is-on { background: var(--warning); color: white; border-color: var(--warning); }
.att-pill--a.is-on { background: var(--danger); color: white; border-color: var(--danger); }
.att-pill.is-on .att-pill__label { opacity: 1; }

/* ============================================================ Mark / List layout */
.att-list {
  list-style: none; padding: 0; margin: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
  /* Watch the list's own width so the row layout reacts to the column,
     not the device — correct in split/landscape/desktop contexts too.
     Named so the @container query can only ever resolve against this list,
     even if the list is later nested inside another container.
     Note: container-type also establishes a containing block, so any
     position:absolute/fixed descendant (tooltip, dropdown) would be clipped
     to this boundary — none exist inside the list today. */
  container-type: inline-size;
  container-name: att-list;
}
.att-list__item + .att-list__item { border-top: 1px solid var(--border); }
.att-list__item { background: var(--surface); }
.att-list__item .att-row { border-radius: 0; }

/* Mobile-first default: stacked. Name on its own line, three full-width
   labelled pills below. Names never truncate; labels always show.
   This is also the safe fallback where @container is unsupported. */
.att-list .att-row {
  flex-direction: column;
  align-items: stretch;
  gap: var(--s-2);
}
/* Let the name wrap instead of clipping — in both the stacked and the
   one-line row layouts, so a long name is never hidden behind an ellipsis. */
.att-list .att-row__name {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}
.att-list .att-row__actions {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s-2);
}
.att-list .att-pill { width: 100%; }

/* Enhancement: once the column is wide enough, collapse back to the
   familiar one-line row. Threshold is calibrated separately so a 360px
   viewport gets the row and a 320px viewport stays stacked. */
@container att-list (min-width: 300px) {
  .att-list .att-row {
    flex-direction: row;
    align-items: center;
    gap: var(--s-3);
  }
  .att-list .att-row__actions {
    display: flex;
    gap: var(--s-2);
  }
  .att-list .att-pill { width: auto; }
}

/* ============================================================ Mark / Grid layout */
.att-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-2);
}
@media (min-width: 600px) { .att-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .att-grid { grid-template-columns: repeat(3, 1fr); } }
.att-grid__tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--s-2);
}
.att-grid__tile .att-row {
  flex-direction: column; align-items: stretch;
  gap: var(--s-3);
}
.att-grid__tile .att-row__student { justify-content: flex-start; }
.att-grid__tile .att-row__actions {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: var(--s-2);
}
.att-grid__tile .att-pill { width: 100%; }

/* ============================================================ Mark / Rollcall layout */
.att-rollcall {
  display: flex; flex-direction: column; gap: var(--s-3);
  max-width: 540px; margin: 0 auto; width: 100%;
}
.att-rollcall__progress {
  height: 6px; background: var(--surface-sunken); border-radius: var(--r-pill);
  overflow: hidden;
}
.att-rollcall__progress-bar {
  height: 100%; background: var(--accent); border-radius: inherit;
  transition: width 240ms ease;
}
.att-rollcall__counter {
  text-align: center; color: var(--text-muted);
  font-size: 0.85rem; font-variant-numeric: tabular-nums;
}
.att-rollcall__deck { position: relative; min-height: 180px; }
.att-rollcall__card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--s-5) var(--s-4);
  box-shadow: var(--shadow-md);
}
.att-rollcall__card .att-row {
  flex-direction: column; align-items: stretch; text-align: center;
  gap: var(--s-5); padding: 0;
}
.att-rollcall__card .att-row__student {
  flex-direction: column; align-items: center; gap: var(--s-3);
}
.att-rollcall__card .att-row__name {
  font-size: 1.4rem; font-weight: 600;
  white-space: normal;
}
.att-rollcall__card .att-row__actions {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: var(--s-3);
}
/* Roll-call shows one student at a time, so its controls are larger. Setting
   the shared token rather than the pill's min-height keeps the reason control
   in step if this layout ever grows one. */
.att-rollcall__card .att-row { --att-control-h: 64px; }
.att-rollcall__card .att-pill { padding: 8px 12px; }
.att-rollcall__card .att-pill__label { display: block; font-size: 0.85rem; }
.att-rollcall__nav {
  display: flex; gap: var(--s-2); justify-content: space-between;
}
.att-rollcall__nav .btn { flex: 1; }

/* ============================================================ Dropzone (file upload) */
.dropzone {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--s-2);
  padding: var(--s-6) var(--s-4);
  text-align: center;
  background: var(--surface-soft);
  border: 2px dashed var(--border-strong);
  border-radius: var(--r-lg);
  cursor: pointer;
  transition: background 150ms ease, border-color 150ms ease, transform 150ms ease;
  color: var(--text);
}
.dropzone:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.dropzone:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
.dropzone--active {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
  transform: scale(1.005);
}
.dropzone--filled {
  background: var(--success-soft);
  border-color: var(--success);
  border-style: solid;
  color: var(--success);
}
.dropzone__input {
  /* Hide visually but stay focusable via the wrapping label */
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0; pointer-events: none;
}
.dropzone__icon {
  width: 40px; height: 40px;
  display: grid; place-items: center;
  border-radius: var(--r-md);
  background: var(--accent-soft); color: var(--accent);
}
.dropzone--filled .dropzone__icon { background: var(--success-soft); color: var(--success); }
.dropzone__icon svg { width: 22px; height: 22px; }
.dropzone__title { font-weight: 600; font-size: 1.05rem; }
.dropzone__hint { color: var(--text-muted); font-size: 0.9rem; }
.dropzone--filled .dropzone__hint { color: var(--success); opacity: 0.8; }
.dropzone__filename {
  font-family: var(--font-mono); font-size: 0.9rem;
  background: var(--surface); border: 1px solid var(--border);
  padding: 2px var(--s-2); border-radius: var(--r-sm);
}
/* Hide the chip until a file has been dropped in (the JS fills the
   element on selection); an empty bordered box reads as a broken UI. */
.dropzone__filename:empty { display: none; }

/* ============================================================ Schedule editor */
/* Wider page wrapper — the default .container caps at 1100px which wastes
   real estate on a calendar grid; this lets the grid breathe. */
.sched-editor-page {
  width: 100%;
  max-width: 1600px;
  margin: 0 auto;
  padding-inline: clamp(16px, 2vw, 24px);
  padding-block: var(--s-5);
}
.sched-editor {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-4);
}
.sched-editor__head {
  display: flex; gap: var(--s-3); align-items: flex-end; justify-content: space-between;
  flex-wrap: wrap;
  margin-top: var(--s-2);
}
.sched-editor__actions { display: flex; gap: var(--s-2); flex-wrap: wrap; }

/* Palette: classes you can drag onto the grid */
.sched-palette {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--s-3);
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.sched-palette__head {
  display: flex; align-items: baseline; justify-content: space-between;
  margin-bottom: var(--s-2);
  flex: 0 0 auto;
}
.sched-palette > .input { margin-bottom: var(--s-2); }
.sched-palette__list {
  list-style: none; padding: 0; margin: 0;
  display: grid; gap: var(--s-2);
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
.sched-palette__item {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-2) var(--s-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  cursor: grab;
  user-select: none;
  transition: border-color 100ms ease, background 100ms ease;
}
.sched-palette__item:hover { background: var(--surface-soft); border-color: var(--border-strong); }
.sched-palette__item:active { cursor: grabbing; }
.sched-palette__item.is-dragging { opacity: 0.4; }
.sched-palette__dot {
  width: 10px; height: 10px; border-radius: 50%;
  flex: 0 0 10px;
}
.sched-palette__body { display: flex; flex-direction: column; min-width: 0; }
.sched-palette__name { font-weight: 600; font-size: 0.95rem; }
.sched-palette__teacher { color: var(--text-muted); font-size: 0.82rem; }
.sched-palette__empty { color: var(--text-muted); padding: var(--s-3); text-align: center; }

/* Grade quick-filter chips built from numeric tokens in class names
   ("Maths 9A" → "9"). Multi-select; ORed within chips, ANDed with search. */
.sched-grade-chips {
  display: flex; flex-wrap: wrap; gap: var(--s-1);
  margin-bottom: var(--s-2);
}
.sched-grade-chips:empty { display: none; }
.sched-grade-chip {
  font: inherit;
  padding: 2px 10px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-muted);
  cursor: pointer;
  font-size: 0.8rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  transition: background 100ms ease, color 100ms ease, border-color 100ms ease;
}
.sched-grade-chip:hover { border-color: var(--border-strong); color: var(--text); }
.sched-grade-chip.is-active {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
}

/* Grid wrap: horizontal scroll on small screens, sticky time column.
   max-height + min-height: 0 makes this element its own scroll viewport
   in both axes, so the sticky time column AND sticky day header have a
   bounded scroll ancestor to stick within (otherwise the day header has
   nothing to stick to — overflow-x:auto alone establishes the scroll
   context but without a height cap there's never any vertical scroll). */
.sched-grid-wrap {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  /* Padding tuned so the grid's --r-md (10px) is concentric with the wrap's --r-lg (14px). */
  padding: var(--s-1);
  overflow: auto;
  /* No -webkit-overflow-scrolling: touch here either. It is obsolete (iOS 13+
     scrolls with momentum natively), and on iOS it promotes the scroller to its
     own compositing layer — which clips position:fixed descendants (see
     static/js/actions-menu.js) and has historically broken position:sticky
     inside the scroller, which is exactly what the sticky day header and time
     column below depend on. */
  max-height: calc(100vh - 220px);
  min-height: 0;
}
.sched-grid {
  display: grid;
  gap: 1px;
  background: var(--border);
  padding: 1px;
  border-radius: var(--r-md);
  overflow: hidden;
  min-width: max-content;
}
/* Row wrappers exist purely to give print-mode a real table-row to break
   on and to mark the header row as <thead>-equivalent for page repetition.
   On screen they are transparent — their children participate directly in
   the parent CSS Grid. */
.sched-grid__row { display: contents; }
.sched-grid__head {
  background: var(--surface-soft);
  padding: var(--s-2) var(--s-2);
  font-weight: 700; text-align: center; font-size: 0.92rem;
  color: var(--text);
  position: sticky; top: 0;
  z-index: 1;
}
/* The top-left cell sits at the intersection of the sticky header row and
   the sticky time column; bump z-index so it stays above both when the
   user scrolls in either axis. left:0 pins it during horizontal scroll. */
.sched-grid__head--corner {
  background: var(--surface-soft);
  left: 0;
  z-index: 2;
}
.sched-grid__time {
  background: var(--surface-soft);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  font-variant-numeric: tabular-nums;
  font-size: 0.78rem; color: var(--text-muted);
  position: sticky; left: 0;
  padding: var(--s-2);
  min-height: 64px;
  z-index: 1;
}
.sched-grid__time-start { font-weight: 700; color: var(--text); font-size: 0.82rem; }
.sched-grid__time-end { font-size: 0.7rem; }
.sched-grid__cell {
  background: var(--surface);
  padding: var(--s-2);
  min-height: 64px;
  display: flex; flex-direction: column; gap: var(--s-1);
  position: relative;
  cursor: pointer;
}
.sched-grid__cell:hover { background: var(--accent-soft); }
.sched-grid__cell.is-drag-over {
  background: var(--accent-soft);
  outline: 2px solid var(--accent); outline-offset: -2px;
}
.sched-grid__cell:empty::before {
  content: "+";
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  color: var(--text-subtle); opacity: 0;
  font-size: 1.4rem; pointer-events: none;
  transition: opacity 100ms ease;
}
.sched-grid__cell:hover:empty::before { opacity: 0.5; }

/* Slot chip — a class scheduled in a particular cell */
.sched-chip {
  /* --chip-color is set inline from {{ slot.class_obj.subject.colour }}.
     Default fallback matches the previous all-teal look so chips render
     even if a class has no subject colour configured. */
  --chip-color: var(--accent);
  position: relative;
  background: color-mix(in srgb, var(--chip-color) 14%, white);
  border: 1px solid color-mix(in srgb, var(--chip-color) 35%, transparent);
  color: color-mix(in srgb, var(--chip-color) 75%, black);
  border-radius: var(--r-sm);
  padding: 6px 28px 6px 8px;
  font-size: 0.85rem;
  cursor: grab;
  user-select: none;
  transition: transform 100ms ease, box-shadow 100ms ease;
}
.sched-chip:hover { box-shadow: var(--shadow-sm); }
.sched-chip:active { cursor: grabbing; }
.sched-chip.is-dragging { opacity: 0.45; }
.sched-chip__name { font-weight: 600; line-height: 1.2; }
.sched-chip__sub { color: var(--text-muted); font-size: 0.75rem; line-height: 1.2; }
.sched-chip__room { color: var(--text-muted); font-size: 0.7rem; }
.sched-chip__delete {
  position: absolute; top: 4px; right: 4px;
  width: 20px; height: 20px;
  display: grid; place-items: center;
  border-radius: var(--r-sm);
  color: inherit;
  opacity: 0;
  transition: opacity 100ms ease, background 100ms ease;
}
.sched-chip:hover .sched-chip__delete,
.sched-chip:focus-within .sched-chip__delete { opacity: 1; }
.sched-chip__delete:hover { background: var(--danger); color: white; opacity: 1; }
@media (hover: none) {
  /* Touch devices: always show delete button (no hover state). */
  .sched-chip__delete { opacity: 1; }
}
/* Conflict highlight — applied by editor.html JS after a Validate response
   that lists this slot's ID under data-conflict-slot-ids. Cleared on the
   next slot mutation or clean Validate. */
.sched-chip--conflict {
  background: rgba(220, 38, 38, 0.08);
  border-color: rgba(220, 38, 38, 0.55);
  box-shadow: 0 0 0 1px rgba(220, 38, 38, 0.45);
}

/* Wider right padding when the cancel button is also visible */
.sched-chip--has-cancel { padding-right: 52px; }

/* Cancelled lesson chip — dimmed with a strikethrough on the class name */
.sched-chip--cancelled {
  opacity: 0.55;
  cursor: default;
}
.sched-chip--cancelled .sched-chip__name {
  text-decoration: line-through;
  color: var(--text-muted);
}
.sched-chip__cancelled-label {
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--danger, #dc2626);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-top: 2px;
}
/* Cancel button — same positioning/style as the delete button but sits
   immediately to the left of it (right: 28px = delete-right 4 + width 20 + gap 4).
   Uses a warning colour on hover to distinguish from the destructive delete. */
.sched-chip__cancel {
  position: absolute; top: 4px; right: 28px;
  width: 20px; height: 20px;
  display: grid; place-items: center;
  border-radius: var(--r-sm);
  color: inherit;
  opacity: 0;
  transition: opacity 100ms ease, background 100ms ease;
}
.sched-chip:hover .sched-chip__cancel,
.sched-chip:focus-within .sched-chip__cancel { opacity: 1; }
.sched-chip__cancel:hover { background: var(--warning, #f59e0b); color: white; opacity: 1; }
@media (hover: none) {
  .sched-chip__cancel { opacity: 1; }
}

/* Class-picker dialog (mobile tap-to-add) */
.sched-picker-list {
  display: grid; gap: var(--s-2);
  max-height: 50vh; overflow-y: auto;
  margin-top: var(--s-3);
  margin-bottom: var(--s-4);
}
.sched-picker-list__item {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  cursor: pointer; text-align: left;
  width: 100%;
}
.sched-picker-list__item:hover { background: var(--surface-soft); border-color: var(--accent); }
.sched-picker-list__dot { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 10px; }

/* Conflict panel uses the standard alert styling */
#sched-conflict-panel:empty { display: none; }
#sched-conflict-panel ul.conflicts {
  list-style: none; padding: 0; margin: var(--s-3) 0 0;
  display: grid; gap: var(--s-2);
}
#sched-conflict-panel .conflict {
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-md);
  background: var(--danger-soft);
  border-left: 3px solid var(--danger);
  font-size: 0.9rem;
}
#sched-conflict-panel .conflict-room_double_booked {
  background: var(--warning-soft); border-left-color: var(--warning);
}
#sched-conflict-panel .conflict-time_overlap {
  background: var(--surface-soft); border-left-color: var(--text-muted);
}

/* --- Layout: tablet+ puts the palette to the left of the grid */
@media (min-width: 900px) {
  .sched-editor {
    grid-template-columns: 240px 1fr;
    grid-template-areas:
      "head    head"
      "palette grid"
      ".       panel";
    align-items: start;
  }
  .sched-editor > div:first-child { grid-area: head; }
  .sched-palette {
    grid-area: palette;
    /* Sticky next to the grid; bounded to viewport so an extra-long class
       list doesn't dominate the page. The conflict panel sits in column 2
       only (empty cell in column 1) so the sticky palette never overlaps
       conflict text — sticky on grid items is bounded by the grid container,
       not the cell, so it can otherwise extend visually into later rows. */
    position: sticky;
    top: 72px;
    max-height: calc(100vh - 96px);
  }
  .sched-grid-region { grid-area: grid; }
  #sched-conflict-panel { grid-area: panel; }
}
.sched-grid-region {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  min-width: 0;
  min-height: 0;
}

/* ============================================================ Schedule print header
   Hidden on screen; revealed and pinned at the top of every printed page so
   the school name, schedule name, and active filter summary repeat across
   page breaks when a long timetable spans multiple pages. */
.sched-print-header { display: none; }

@media print {
  @page { size: A4 landscape; margin: 8mm; }
  body { background: #fff; }
  /* Hide everything that doesn't belong on a printed timetable */
  .nav, .site-footer, .evac-fab, #offline-banner { display: none !important; }
  .sched-editor-page .sched-palette,
  .sched-editor-page .sched-filter-bar,
  .sched-editor-page #sched-conflict-panel,
  .sched-editor-page #manage-modal-container,
  .sched-editor-page .sched-editor__head,
  .sched-editor-page .sched-editor > div > a.btn--ghost,
  .sched-chip__delete,
  .sched-chip__cancel,
  .sched-chip__view {
    display: none !important;
  }
  /* Page-1 inline header. The table's column-header row (display:table-header-group)
     handles per-page repetition; this one stays on page 1 only as document context. */
  .sched-print-header {
    display: block !important;
    padding: 0 0 3mm;
    margin-bottom: 4mm;
    background: #fff;
    border-bottom: 1px solid #333;
    font-family: inherit;
  }
  .sched-print-header__title {
    font-size: 12pt;
    font-weight: 700;
    color: #000;
    line-height: 1.2;
  }
  .sched-print-header__sep { color: #666; font-weight: 400; }
  .sched-print-header__name { font-weight: 700; }
  .sched-print-header__filters {
    font-size: 9pt;
    color: #222;
    margin-top: 0.5mm;
    font-style: italic;
  }
  /* Let the grid render across pages, not in a scrollable box */
  .sched-editor-page,
  .sched-editor { display: block !important; padding: 0 !important; }
  .sched-grid-region { display: block !important; }
  .sched-grid-wrap {
    overflow: visible !important;
    max-height: none !important;
    border: 0 !important;
    padding: 0 !important;
    background: transparent !important;
  }
  /* Switch the CSS Grid to a real table so browsers can repeat the column
     header row on every page (display: table-header-group) and avoid
     splitting a single bell-period row across a page break. */
  .sched-grid {
    display: table !important;
    background: transparent !important;
    font-size: 9pt;
    min-width: 0 !important;
    width: 100% !important;
    padding: 0 !important;
    border-radius: 0 !important;
    overflow: visible !important;
    border-collapse: collapse !important;
    border-top: 1px solid #444 !important;
    border-left: 1px solid #444 !important;
    table-layout: fixed !important;
  }
  .sched-grid__row { display: table-row !important; break-inside: avoid; page-break-inside: avoid; }
  .sched-grid__row--head { display: table-header-group !important; }
  /* Explicit cell borders — clearer in print than the gap+background trick,
     and survives browsers that ignore background colours when printing. */
  .sched-grid__head,
  .sched-grid__time,
  .sched-grid__cell {
    display: table-cell !important;
    vertical-align: top !important;
    border-right: 1px solid #444 !important;
    border-bottom: 1px solid #444 !important;
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
  }
  .sched-grid__head,
  .sched-grid__time {
    position: static !important;
    background: #ececec !important;
    color: #000 !important;
    padding: 1.5mm 1.5mm !important;
    font-size: 9pt !important;
    text-align: center !important;
    overflow: visible !important;
    vertical-align: middle !important;
  }
  .sched-grid__time { width: 22mm; }
  .sched-grid__head--corner { width: 22mm; }
  .sched-grid__time-label,
  .sched-grid__time-start,
  .sched-grid__time-end { display: block; }
  .sched-grid__time-label { white-space: normal !important; word-break: normal; }
  .sched-grid__cell {
    background: #fff !important;
    padding: 1.5mm !important;
    height: 14mm;
    min-height: 14mm !important;
  }
  .sched-grid__cell:hover { background: #fff !important; }
  .sched-grid__cell--non-lesson { background: #fafafa !important; }
  .sched-chip {
    box-shadow: none !important;
    padding: 1.5mm !important;
    /* Preserve subject colours when printing */
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
    padding-right: 1.5mm !important;
    break-inside: avoid;
    page-break-inside: avoid;
  }
  .sched-chip__name { font-size: 9pt; }
  .sched-chip__sub, .sched-chip__room { font-size: 8pt; }
}

/* ============================================================ Modal / confirm dialog */
/* Uses native <dialog>; styled to match the rest of the design system. */
.modal {
  border: 0;
  padding: 0;
  background: transparent;
  max-width: min(440px, calc(100vw - 32px));
  width: 100%;
}
/* For modals containing tables, multi-column choices, or per-row pickers
   that need horizontal room. Apply alongside .modal: <dialog class="modal modal--wide"> */
.modal--wide { max-width: min(820px, calc(100vw - 32px)); }
.modal::backdrop {
  background: hsl(20 10% 10% / 0.45);
  backdrop-filter: blur(2px);
}
.modal__inner {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--s-5);
}
.modal__title {
  margin: 0 0 var(--s-2);
  font-size: 1.15rem; font-weight: 600;
}
.modal__body {
  margin: 0 0 var(--s-5);
  color: var(--text-muted);
}
.modal__actions {
  display: flex; justify-content: flex-end; gap: var(--s-2);
}
@media (prefers-reduced-motion: no-preference) {
  .modal[open] { animation: modalIn 160ms ease-out; }
  @keyframes modalIn {
    from { transform: translateY(8px); opacity: 0; }
    to   { transform: translateY(0);   opacity: 1; }
  }
}

/* ============================================================ Covers-terms panel + link-term modal */
/* Inline strip under the editor title showing which terms this template
   covers. Term chips match the system's pill style (cf. .sched-grade-chip)
   but are persistent metadata, not toggleable filters — so they sit on
   .term-chip with a baked-in remove × inside. */
.covers-terms {
  display: flex; align-items: center; gap: var(--s-2);
  flex-wrap: wrap;
  margin-top: var(--s-2);
}
.covers-terms__label {
  color: var(--text-muted);
  font-size: 0.85rem;
  font-weight: 500;
}
.covers-terms__list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--s-1);
}
.covers-terms__empty { font-style: italic; }
.term-chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 4px 2px 10px;
  border: 1px solid var(--border);
  background: var(--surface-soft);
  border-radius: 999px;
  font-size: 0.82rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.term-chip__name { line-height: 1.4; }
.term-chip__remove {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px;
  border: 0; padding: 0;
  background: transparent; cursor: pointer;
  color: var(--text-muted);
  border-radius: 999px;
  transition: background 100ms ease, color 100ms ease;
}
.term-chip__remove:hover { background: var(--danger); color: white; }
.term-chip__remove:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.covers-terms__add { gap: 4px; }
.covers-terms__add svg { margin-right: -2px; }

/* Modal: term picker (multi-select). Uses .modal--wide for room. */
.link-term-form .modal__body { margin-bottom: var(--s-3); }
.link-term-list {
  display: flex; flex-direction: column; gap: var(--s-3);
  max-height: 60vh; overflow-y: auto;
  margin-bottom: var(--s-4);
  padding-right: 4px;
}
.link-term-year {
  border: 0; padding: 0; margin: 0;
}
.link-term-year > legend {
  font-weight: 600; font-size: 0.85rem; color: var(--text-muted);
  letter-spacing: 0.02em; text-transform: uppercase;
  padding: 0 0 var(--s-1);
}
.link-term-row {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  gap: var(--s-2) var(--s-3);
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  cursor: pointer;
  margin-bottom: 6px;
  background: var(--surface);
  transition: border-color 100ms ease, background 100ms ease;
}
.link-term-row:hover { border-color: var(--border-strong); background: var(--surface-soft); }
.link-term-row:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 6%, var(--surface));
}
.link-term-row.is-linked-here {
  background: color-mix(in srgb, var(--accent) 10%, var(--surface));
}
.link-term-row input[type="checkbox"] {
  width: 18px; height: 18px;
  accent-color: var(--accent);
}
.link-term-row__name { font-weight: 600; }
.link-term-row__dates { white-space: nowrap; }
.link-term-row__state { text-align: right; }
@media (max-width: 640px) {
  .link-term-row {
    grid-template-columns: auto 1fr;
    grid-template-rows: auto auto;
  }
  .link-term-row__dates { grid-column: 2; }
  .link-term-row__state { grid-column: 2; text-align: left; }
}

/* ============================================================ Audit / multi-field filter grid */
/* Used for the cross-tenant audit filters. Two columns on tablet, single
   column on phones — selects + date inputs get full width instead of
   squeezing into 50px each. */
.audit-filters {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-3);
}
@media (min-width: 600px) {
  .audit-filters { grid-template-columns: 1fr 1fr; }
}
@media (min-width: 1000px) {
  .audit-filters { grid-template-columns: repeat(3, 1fr); }
}

/* ============================================================ Flash region */
.flash-region {
  max-width: 1100px;
  margin: var(--s-3) auto 0;
  padding-inline: clamp(16px, 4vw, 32px);
  display: grid; gap: var(--s-2);
}

/* ============================================================ Utilities */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.hidden { display: none !important; }
.mt-0 { margin-top: 0 !important; }
.mt-1 { margin-top: var(--s-1) !important; }
.mt-2 { margin-top: var(--s-2) !important; }
.mt-3 { margin-top: var(--s-3) !important; }
.mt-4 { margin-top: var(--s-4) !important; }
.mt-5 { margin-top: var(--s-5) !important; }
.mb-0 { margin-bottom: 0 !important; }
.mb-1 { margin-bottom: var(--s-1) !important; }
.mb-2 { margin-bottom: var(--s-2) !important; }
.mb-3 { margin-bottom: var(--s-3) !important; }
.mb-4 { margin-bottom: var(--s-4) !important; }
.mb-5 { margin-bottom: var(--s-5) !important; }
.flex-1 { flex: 1; }

/* ============================================================ Smart-group criteria grid (real table) */
/* The criteria list is a real <table> so row backgrounds, header
   underline, and zebra striping paint natively. Empty cells (paren
   columns in thead, the value cell before a type is picked) inherit
   the row background; the row's height is set by its tallest cell. */
.criteria-grid {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  table-layout: auto;
}
.criteria-grid thead th {
  text-align: left;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding: var(--s-2) var(--s-2);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.criteria-grid tbody td {
  padding: var(--s-2);
  vertical-align: middle;
}
.criteria-grid tbody tr {
  border-bottom: 1px solid var(--border);
}
.criteria-grid tbody tr:nth-child(even) {
  background: var(--surface-soft);
}
/* First data row has no prior to connect to — hide the AND/OR select. */
.criteria-grid tbody tr:first-child td.criterion-row__conn select {
  visibility: hidden;
}
/* Red × delete button. */
.criterion-row__remove--danger {
  background: transparent;
  border: 0;
  color: var(--danger);
  font-size: 1.25rem;
  line-height: 1;
  padding: 0 var(--s-2);
  cursor: pointer;
  border-radius: var(--r-sm);
}
.criterion-row__remove--danger:hover {
  background: var(--danger-soft);
}
/* Drag-to-reorder handle (criteria builder) */
.criterion-row__drag-handle {
  background: transparent;
  border: 0;
  cursor: grab;
  padding: 0.5rem;
  min-width: 44px;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  font-size: 1.1rem;
  line-height: 1;
  border-radius: var(--r-sm);
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  /* A touch starting on the handle drags the row; it never scrolls/zooms. */
  touch-action: none;
}
.criterion-row__drag-handle:hover { color: var(--text); }
.criterion-row__drag-handle:focus { outline: 2px solid var(--accent); outline-offset: 2px; }
.criterion-row--ghost { opacity: 0.4; }
.criterion-row--chosen { transform: rotate(2deg); box-shadow: 0 8px 24px rgba(0,0,0,0.15); }
.criterion-row--dragging { cursor: grabbing; }
.criterion-row { transition: transform 200ms; }
/* Multi-control value cells interlace inputs with small connector
   words (in, %, etc.) so the row reads as a sentence, e.g.
   `Absences in window  >  5 in Last 7 days`. The connectors are
   muted and inline; no labels above or below. */
.value-cell--multi {
  display: flex;
  gap: var(--s-2);
  align-items: center;
  flex-wrap: nowrap;
}
.value-cell__connector {
  color: var(--text-muted);
  white-space: nowrap;
}

.value-cell--multi .input {
  width: auto;
  flex: 0 0 auto; /* 👈 prevents stretching */
}

/* Mobile: collapse the 7-column criteria table into a stack of cards so
   each criterion gets full-width controls instead of cramming AND/OR,
   parens, type, operator, value, close-paren, and × onto one row. The
   table semantics stay intact — only the visual layout changes. */
@media (max-width: 720px) {
  .criteria-grid,
  .criteria-grid thead,
  .criteria-grid tbody,
  .criteria-grid tr,
  .criteria-grid td {
    display: block;
    width: 100%;
  }
  .criteria-grid thead { display: none; }
  .criteria-grid tbody tr {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    padding: var(--s-3);
    margin-bottom: var(--s-3);
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--s-2);
    position: relative;
  }
  .criteria-grid tbody tr:nth-child(even) { background: var(--surface-soft); }
  .criteria-grid tbody td { padding: 0; }
  /* AND/OR + delete row sit side by side at the top */
  .criteria-grid tbody td.criterion-row__conn { grid-column: 1; }
  .criteria-grid tbody td.criterion-row__remove,
  .criteria-grid tbody td:last-child {
    grid-column: 2;
    justify-self: end;
  }
  /* Open/close paren cells share a row */
  .criteria-grid tbody td.criterion-row__paren { grid-column: span 1; }
  /* Type, operator, value each occupy the full width */
  .criteria-grid tbody td:nth-child(3),
  .criteria-grid tbody td:nth-child(4),
  .criteria-grid tbody td.criterion-row__value {
    grid-column: span 2;
  }
  /* Allow the value cell's flex row to wrap so e.g. "5 in Last 7 days"
     doesn't push the page wider. */
  .value-cell--multi { flex-wrap: wrap; }
  /* First-row AND/OR is hidden via visibility — claim the grid space back
     so the delete × sits flush at the top right. */
  .criteria-grid tbody tr:first-child td.criterion-row__conn {
    visibility: hidden;
    height: 0;
    overflow: hidden;
  }
}

/* ============================================================ Chip */
.chip {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--s-2);
  border-radius: var(--r-md);
  background: var(--surface-soft);
  color: var(--text);
  font-size: 0.8125rem;
  font-weight: 500;
  line-height: 1.4;
  border: 1px solid var(--border);
  white-space: nowrap;
}
.chip--ok   { background: rgba(16, 185, 129, 0.12); border-color: rgba(16, 185, 129, 0.4); color: #047857; }
.chip--warn { background: rgba(245, 158, 11, 0.12); border-color: rgba(245, 158, 11, 0.4); color: #92400e; }

/* ============================================================ Placements (worklist + per-student) */
.placements__students {
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
}
.placements__student-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
}
.placements__student-head h2 {
  margin: 0;
  font-size: 1.125rem;
}
.placements__gap-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-4);
}
@media (min-width: 720px) {
  .placements__gap-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1100px) {
  .placements__gap-grid { grid-template-columns: repeat(3, 1fr); }
}
.placements__gap-card {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}
.placements__gap-card--covered { background: rgba(16, 185, 129, 0.06); border-color: rgba(16, 185, 129, 0.25); }
.placements__gap-head {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
}
.placements__gap-head { row-gap: var(--s-2); column-gap: var(--s-3); }
/* Only space the head off the radio list when one follows; the no-candidates
   branch ends with a <p> that already has its own top margin from typography. */
.placements__gap-head + .placements__choices { margin-top: var(--s-3); }
.placements__place-btn {
  margin-left: auto;
  margin-bottom: 10px;
  padding-inline: var(--s-4);
}
.placements__choices {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
.placements__choice {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  padding: var(--s-3);
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface);
  cursor: pointer;
  transition: border-color 120ms, background 120ms;
}
.placements__choice:hover { border-color: var(--border-strong); }
.placements__choice:has(input:checked) { border-color: var(--accent); background: rgba(13, 148, 136, 0.06); }
.placements__choice input[type="radio"] { margin-top: 2px; flex: 0 0 auto; }
.placements__choice-body { display: flex; flex-direction: column; gap: 2px; }
.placements__choice-name { font-weight: 600; }
.placements__covered-classes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}

/* --- Marketing site (landing, about, contact, privacy, terms) -------------- */
.marketing-hero {
  text-align: center;
  padding: var(--s-6) var(--s-3);
}
.marketing-hero__title {
  font-size: 2.4rem;
  line-height: 1.15;
  margin: 0 auto var(--s-3);
  max-width: 18ch;
}
.marketing-hero__lede {
  max-width: 60ch;
  margin: 0 auto var(--s-4);
}
.marketing-hero__ctas {
  display: flex;
  gap: var(--s-2);
  justify-content: center;
  flex-wrap: wrap;
  margin: 0;
}

.marketing-article {
  max-width: 70ch;
  margin-inline: auto;
}
.marketing-article h2 {
  margin-top: var(--s-5);
  margin-bottom: var(--s-2);
}
.marketing-article p,
.marketing-article ul {
  line-height: 1.65;
}
.marketing-article ul {
  padding-left: var(--s-5);
}

.site-footer {
  border-top: 1px solid var(--border);
  /* No margin and no padding of its own. The rule and the tinted background
     already separate the footer from the page, and __inner is a .container —
     so anything set here stacks on top of that container's own inset and reads
     as slack. The footer's whole vertical rhythm lives on __inner below. */
  margin: 0;
  padding: 0;
  background: var(--surface-soft);
}
/* The footer deliberately reserves NO room for the evacuation FAB. Scrolled to
   the very bottom, the fixed FAB does land on the footer's bottom-right corner
   and can cover a nav link — that's accepted. Both remedies cost more than the
   overlap: reserving height (~98px) padded every page for a button only admins
   and secretaries ever see, and reserving a column squeezed the links on narrow
   screens. Don't reintroduce either without a stronger reason than the overlap
   itself. */
.site-footer__inner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-3);
  /* Tighter than the global .container rhythm (--s-5). The footer is chrome,
     not content — it doesn't need a page body's breathing room. Overridden here
     rather than on .container so no other page's spacing moves. */
  padding-block: var(--s-4);
}
.site-footer__brand {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.site-footer__links {
  display: flex;
  gap: var(--s-4);
  flex-wrap: wrap;
}
.site-footer__links a {
  color: var(--text-muted);
}
.site-footer__links a:hover {
  color: var(--text);
}

/* --- Pricing tiers (/pricing/) -------------------------------------------- */
.pricing-tiers {
  margin-top: var(--s-4);
}
.pricing-tier {
  display: flex;
  flex-direction: column;
}
.pricing-tier__label {
  margin: 0 0 var(--s-2);
}
.pricing-tier__price {
  margin: 0 0 var(--s-2);
  line-height: 1.1;
}
.pricing-tier__price-amount {
  font-size: 2rem;
  font-weight: 700;
}
.pricing-tier__price-period {
  display: block;
  font-size: 0.85rem;
  color: var(--text-muted);
  margin-top: 4px;
}
.pricing-tier__list {
  list-style: none;
  padding: 0;
  margin: var(--s-3) 0;
  flex: 1;
}
.pricing-tier__list > li {
  padding: var(--s-2) 0;
  border-bottom: 1px solid var(--border);
}
.pricing-tier__list > li:last-child {
  border-bottom: 0;
}
.pricing-tier--featured {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

/* --- Leaderboard pure-CSS radio tabs --- */
.lb-tabs input[name="lb-tab"] { display: none; }
.lb-tabs__bar { display: flex; gap: 0; border-bottom: 2px solid var(--border); margin-bottom: var(--s-4); }
.lb-tabs__bar label { padding: var(--s-2) var(--s-4); cursor: pointer; color: var(--text-muted); font-weight: 500; border-bottom: 3px solid transparent; margin-bottom: -2px; }
#lb-tab-houses:checked ~ .lb-tabs__bar label[for="lb-tab-houses"],
#lb-tab-students:checked ~ .lb-tabs__bar label[for="lb-tab-students"] {
  color: var(--accent); border-bottom-color: var(--accent);
}
.lb-tabs__panel { display: none; }
#lb-tab-houses:checked ~ .lb-tabs__panel--houses,
#lb-tab-students:checked ~ .lb-tabs__panel--students { display: block; }

/* =========================================================================
   Evacuation Roll-Call — mobile-first styles (≥44×44px tap targets)
   ========================================================================= */

/* Scope-and-role notice — surfaced on every evac entry point so schools
   never read this feature as a substitute for their emergency plan. */
.evac-scope-banner {
  margin-top: var(--s-2);
  line-height: 1.5;
}

/* Incident callout banner */
.evac-callout {
  background: #fff3cd;
  border: 2px solid #f0ad4e;
  border-radius: 8px;
  padding: 1.25rem;
}
.evac-callout__title { font-size: 1.3rem; margin-bottom: 0.25rem; }
.evac-callout__protocol { margin-top: 0.75rem; }
.evac-callout__protocol p { margin: 0; white-space: pre-line; }

/* Filter bar */
.evac-filters { display: flex; flex-direction: column; gap: 0.75rem; }
.evac-grade-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}
.chip {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 1rem;
  border: 1.5px solid var(--border);
  border-radius: 20px;
  background: #fff;
  font-size: 0.875rem;
}
button.chip, a.chip {
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
button.chip:hover, a.chip:hover { background: var(--surface-subtle); }
.chip--active { background: var(--accent); color: #fff; border-color: var(--accent); }

/* Lesson filter dropdown */
.evac-lesson-filter { position: relative; }
.evac-lesson-list {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 50;
  margin: 0;
  padding: 0;
  list-style: none;
  background: #fff;
  border: 1.5px solid var(--border);
  border-radius: 6px;
  max-height: 240px;
  overflow-y: auto;
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.evac-lesson-list__item {
  padding: 0.75rem 1rem;
  min-height: 44px;
  display: flex;
  align-items: center;
  cursor: pointer;
}
.evac-lesson-list__item:hover { background: var(--surface-subtle); }

/* Active-filter summary */
.evac-filter-summary { padding: 0.25rem 0; }

/* Polling meta strip — "Updated Ns ago" + connection-issue pill.
   Sits above the roll-call list. The pill is hidden until backoff kicks in. */
.evac-list-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  padding: 0.25rem 0.125rem;
  min-height: 1.5rem;
}
.evac-list-meta > #evac-last-updated {
  font-variant-numeric: tabular-nums;
  cursor: default;
  user-select: none;
}
.evac-status-pill {
  display: inline-flex;
  align-items: center;
  padding: 2px 10px;
  border-radius: 999px;
  font-size: 0.8rem;
  font-weight: 500;
}
.evac-status-pill--warn {
  background: hsl(40 92% 92%);
  color: hsl(28 70% 28%);
}
.evac-status-pill--error {
  background: hsl(0 80% 94%);
  color: hsl(0 60% 30%);
}

/* Roll-call list */
.evac-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* Student row card */
.evac-row {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1rem;
  border: 1.5px solid var(--border);
  border-radius: 8px;
  background: #fff;
  transition: background 0.2s, border-color 0.2s;
}
.evac-row--accounted { background: #d4edda; border-color: #c3e6cb; }
.evac-row--missing   { background: #f8d7da; border-color: #f5c6cb; }
.evac-row--readonly  { opacity: 0.8; }

.evac-row__info {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 0.75rem;
  align-items: baseline;
}
.evac-row__name  { font-size: 1rem; }
.evac-row__grade { font-size: 0.8rem; }
.evac-row__by    { width: 100%; }

/* Action buttons — full-width on narrow screens, min 44px height */
.evac-row__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.evac-row__btn {
  flex: 1 1 auto;
  min-height: 44px;
  font-size: 0.9rem;
  white-space: nowrap;
}
.evac-row__btn--accounted { background: #28a745; color: #fff; border-color: #28a745; }
.evac-row__btn--missing   { background: #dc3545; color: #fff; border-color: #dc3545; }

/* Incident type selection modal */
.evac-incident-list {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin: 1rem 0;
}
.evac-incident-btn {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 1rem;
  min-height: 64px;
  text-align: left;
  border: 1.5px solid var(--border);
  border-radius: 8px;
  background: #fff;
  cursor: pointer;
  transition: background 0.15s;
}
.evac-incident-btn:hover  { background: var(--surface-subtle); }
.evac-incident-btn--default {
  border-color: var(--danger, #dc3545);
  background: #fff5f5;
}
.evac-incident-btn__name    { font-size: 1rem; }
.evac-incident-btn__preview { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

/* Offline banner */
.banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.5rem 1rem;
  font-size: 0.875rem;
}
.banner--warning {
  background: #fff3cd;
  color: #856404;
  border-bottom: 1px solid #ffe083;
}
.banner--info {
  background: #e7f3ff;
  color: #1a4d7a;
  border-bottom: 1px solid #b8dcff;
}
.banner__text { flex: 1; }

/* ===================================================
 * Portal-only components
 * Used by apps/portals/. Admin templates do not load these.
 * =================================================== */

/* --- Bottom nav (fixed at viewport bottom) ---
   Fixed (not sticky) because position:sticky is unreliable inside the
   portal_base flex column on iOS Safari and on very short pages. The
   .portal-main below adds bottom padding to clear the bar.
   Grid adapts to N items via grid-auto-columns: 1fr so adding/removing
   a nav item doesn't break the layout (parents see 5 items, students 3). */
.bnav {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  background: var(--surface);
  border-top: 1px solid var(--border);
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  height: 64px;
  z-index: 50;
  /* Safe-area inset for iOS home indicator. Bar stays 64px tall above the
     indicator without the indicator overlapping the touch targets. */
  padding-bottom: env(safe-area-inset-bottom, 0);
  box-sizing: content-box;
}
.bnav__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  color: var(--text-muted);
  font-size: 0.7rem;
  font-weight: 600;
  text-decoration: none;
  min-height: 44px; /* touch target */
  /* Keep labels on one line; truncate with ellipsis if a translated label
     is unexpectedly long. Prevents an oversized label pushing item height. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  padding: 0 4px;
}
.bnav__item--active { color: var(--accent); }
.bnav__item-ico { width: 22px; height: 22px; display: block; }

/* --- Kid card (parent home) --- */
.kid-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--s-4);
  box-shadow: var(--shadow-sm);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  text-decoration: none;
  color: inherit;
}
.kid-card:hover { text-decoration: none; }
.kid-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
}
.kid-card__name-block { display: flex; align-items: center; gap: var(--s-3); }
.kid-card__name { font-weight: 700; font-size: 0.95rem; }
.kid-card__meta { font-size: 0.7rem; color: var(--text-muted); }
.kid-card__stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
}
.kid-card__stat {
  background: var(--surface-soft);
  padding: var(--s-2) var(--s-1);
  text-align: center;
  min-width: 0;
}
.kid-card__stat-value {
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.01em;
}
.kid-card__stat-label {
  font-size: 0.625rem;
  color: var(--text-muted);
  margin-top: 1px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 600;
}
.kid-card__hw-strip {
  background: var(--surface-sunken);
  border-radius: var(--r-md);
  padding: var(--s-2) var(--s-3);
  display: flex;
  align-items: center;
  gap: var(--s-3);
}
.kid-card__hw-text { flex: 1; }
.kid-card__hw-title { font-size: 0.75rem; font-weight: 600; }
.kid-card__hw-meta { font-size: 0.7rem; color: var(--text-muted); margin-top: 1px; }

/* --- Kid header (kid-detail / student home) --- */
.kid-header {
  display: block;
  background: linear-gradient(180deg, var(--accent-soft), var(--bg));
  padding: var(--s-3) var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 5;
}
.kid-header__back {
  font-size: 0.8rem;
  color: var(--accent);
  font-weight: 600;
  margin-bottom: var(--s-2);
  display: inline-block;
  text-decoration: none;
}
.kid-header__row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-4);
}
.kid-header__avatar {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-fg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.35rem;
  font-weight: 700;
  flex-shrink: 0;
  box-shadow: 0 2px 8px hsl(0 0% 0% / 0.1);
}
.kid-header__info { flex: 1; min-width: 0; }
.kid-header__name {
  font-size: 1.25rem;
  font-weight: 700;
  margin: 0;
  letter-spacing: -0.01em;
  line-height: 1.1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.kid-header__sub {
  font-size: 0.75rem;
  color: var(--text-muted);
  margin-top: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.kid-header__stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
  width: 100%;
}
.kid-header__stat {
  background: var(--surface);
  padding: var(--s-2) var(--s-1);
  text-align: center;
  min-width: 0;
}
.kid-header__stat-value {
  font-size: 1.125rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.kid-header__stat-label {
  font-size: 0.625rem;
  color: var(--text-muted);
  margin-top: 2px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: 600;
}

/* --- Activity feed item --- */
.activity-list {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.activity-item {
  padding: var(--s-3) var(--s-4);
  display: flex;
  gap: var(--s-3);
  align-items: flex-start;
  border-bottom: 1px solid var(--border);
  text-decoration: none;
  color: inherit;
}
.activity-item:last-child { border-bottom: 0; }
.activity-item:hover { background: var(--surface-sunken); text-decoration: none; }
.activity-item__icon {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.85rem;
}
.activity-item__icon--success { background: var(--success-soft); color: var(--success); }
.activity-item__icon--warning { background: var(--warning-soft); color: var(--warning); }
.activity-item__icon--danger  { background: var(--danger-soft);  color: var(--danger); }
.activity-item__icon--accent  { background: var(--accent-soft);  color: var(--accent); }
.activity-item__text { flex: 1; font-size: 0.8rem; line-height: 1.4; }
.activity-item__time { font-size: 0.7rem; color: var(--text-subtle); margin-top: 2px; }

/* --- Lazy collapsible section (kid-detail / student home) --- */
.lazysec {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.lazysec[open] { box-shadow: var(--shadow-sm); }
.lazysec__h {
  padding: var(--s-3) var(--s-4);
  display: flex;
  align-items: center;
  justify-content: space-between;
  cursor: pointer;
  list-style: none;
}
.lazysec__h::-webkit-details-marker { display: none; }
.lazysec__left { display: flex; align-items: center; gap: var(--s-3); }
.lazysec__icon {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: var(--accent-soft);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.85rem;
}
.lazysec__title { font-size: 0.85rem; font-weight: 600; }
.lazysec__count { font-size: 0.7rem; color: var(--text-muted); margin-top: 1px; }
.lazysec__chev {
  color: var(--text-subtle);
  font-size: 1rem;
  transition: transform 150ms ease;
}
.lazysec[open] .lazysec__chev { transform: rotate(180deg); }
.lazysec__body {
  border-top: 1px solid var(--border);
  padding: var(--s-3) var(--s-4);
}

/* --- Day pager (weekly schedule) --- */
.day-pager {
  display: flex;
  gap: var(--s-2);
  overflow-x: auto;
  padding-bottom: var(--s-2);
  scroll-snap-type: x mandatory;
}
.day-pager::-webkit-scrollbar { display: none; }
.day-chip {
  flex-shrink: 0;
  background: var(--surface-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--s-2) var(--s-3);
  text-align: center;
  min-width: 56px;
  cursor: pointer;
  scroll-snap-align: start;
  text-decoration: none;
  color: inherit;
}
.day-chip__abbr {
  font-size: 0.625rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  font-weight: 600;
  display: block;
}
.day-chip__num {
  font-size: 1.125rem;
  font-weight: 700;
  margin-top: 1px;
  letter-spacing: -0.02em;
  display: block;
}
.day-chip--today {
  background: var(--accent-soft);
  border-color: var(--accent);
}
.day-chip--today .day-chip__abbr,
.day-chip--today .day-chip__num { color: var(--accent); }
.day-chip--active {
  background: var(--accent);
  border-color: var(--accent);
}
.day-chip--active .day-chip__abbr,
.day-chip--active .day-chip__num { color: var(--accent-fg); }

/* --- Homework mark-as-done (student only) --- */
.hw-check {
  width: 22px;
  height: 22px;
  border-radius: 6px;
  border: 2px solid var(--border);
  background: var(--surface);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
}
.hw-check--done {
  background: var(--success);
  border-color: var(--success);
  color: var(--accent-fg);
}
.hw-check--done::before {
  content: '✓';
  font-size: 0.9rem;
  font-weight: 700;
}

/* --- Notify-absence FAB (parent home) --- */
.fab {
  position: fixed;
  /* Clear the bottom nav (64px) + iOS home-indicator safe area + a bit of breathing room. */
  bottom: calc(64px + env(safe-area-inset-bottom, 0px) + var(--s-3));
  right: var(--s-4);
  background: var(--accent);
  color: var(--accent-fg);
  border-radius: var(--r-pill);
  padding: var(--s-3) var(--s-5);
  font-size: 0.8rem;
  font-weight: 600;
  box-shadow: var(--shadow-md);
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  text-decoration: none;
  z-index: 40;
}
.fab:hover { text-decoration: none; }

/* --- Portal page layout --- */
.page--portal { min-height: 100dvh; display: flex; flex-direction: column; }
.portal-main {
  flex: 1;
  padding: var(--s-4);
  /* Clear the fixed bottom nav (64px) plus its safe-area inset, plus the
     FAB. Without this, the last bit of page content sits under the bar. */
  padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px) + var(--s-7));
  max-width: 720px;
  margin: 0 auto;
  width: 100%;
}
.portal-messages { margin-bottom: var(--s-4); display: flex; flex-direction: column; gap: var(--s-2); }
.portal-message {
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-md);
  font-size: 0.85rem;
  background: var(--surface);
  border: 1px solid var(--border);
}
.portal-message--success { background: var(--success-soft); border-color: hsl(150 55% 80%); }
.portal-message--warning { background: var(--warning-soft); border-color: hsl(35 90% 80%); }
.portal-message--error   { background: var(--danger-soft);  border-color: hsl(0 70% 80%); }

/* --- Portal top bar (greeting block) --- */
.portal-top {
  padding: var(--s-2) 0 var(--s-3);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--s-4);
}
.portal-top__school {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-muted);
  font-weight: 600;
}
.portal-top__greeting {
  font-size: 1.4rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 2px 0 0;
}
.portal-top__date { font-size: 0.75rem; margin: 2px 0 0; }

/* ============================================================ Subscription modal
   Used by /smartgroups/<id>/subscriptions/create + edit. Stacked form with a
   segmented cadence control whose conditional fields (weekday / day-of-month)
   show only when the matching cadence radio is selected — driven entirely by
   :has(), no JS toggles. */
.sub-form { display: flex; flex-direction: column; gap: var(--s-3); }
.sub-form__field { display: flex; flex-direction: column; gap: var(--s-1); }
.sub-form__label { font-weight: 600; font-size: 0.9rem; }
.sub-form__multiselect {
  width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--s-2);
  background: var(--surface);
  font: inherit;
}
.sub-form__multiselect:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.sub-form input[type="time"],
.sub-form input[type="number"],
.sub-form select {
  min-height: 44px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 0 var(--s-3);
  background: var(--surface);
  font: inherit;
}
.sub-form input[type="time"]:focus-visible,
.sub-form input[type="number"]:focus-visible,
.sub-form select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Segmented control (Daily / Weekly / Monthly). Pill-shaped track that
   echoes .tab-bar, but driven by native radio inputs so it works without
   JS. Active pill highlight comes from :has(input:checked) on the label. */
.seg-control {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 4px;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: 999px;
}
.seg-control__option {
  flex: 1 1 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 36px;
  padding: 0 var(--s-3);
  border-radius: 999px;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--text-muted);
  cursor: pointer;
  white-space: nowrap;
  transition: background 80ms ease, color 80ms ease, box-shadow 80ms ease;
}
/* Hide the radio circle without breaking accessibility — the label IS the hit area. */
.seg-control__option input[type="radio"] {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}
.seg-control__option:hover { color: var(--text); }
.seg-control__option:has(input:checked) {
  background: var(--surface);
  color: var(--accent);
  font-weight: 600;
  box-shadow: 0 1px 2px hsl(0 0% 0% / 0.06), 0 0 0 1px hsl(0 0% 0% / 0.04);
}
.seg-control__option:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Cadence-conditional fields: hidden by default; revealed via :has() on the
   sibling radio. The form itself is the :has() scope. */
.sub-form__weekly-only,
.sub-form__monthly-only { display: none; }
.sub-form:has(input[name="cadence"][value="weekly"]:checked) .sub-form__weekly-only { display: flex; }
.sub-form:has(input[name="cadence"][value="monthly"]:checked) .sub-form__monthly-only { display: flex; }

/* Push the action row away from the field above it so Cancel + Save don't
   look glued to the previous input. */
.sub-form__actions { margin-top: var(--s-3); }

/* ============================================================ Teacher → student attendance comparison */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
}
.card--link { display: block; text-decoration: none; color: inherit; }
.card--link:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.card__title { margin: 0 0 4px; font-size: 1.05rem; }

.roster-row {
  display: block;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
  text-decoration: none;
  color: inherit;
}
.roster-row:hover { background: var(--surface-soft); }

.period-chips { display: flex; gap: 6px; flex-wrap: wrap; }

.compare { margin-top: 8px; }
.compare__title { font-size: 1rem; margin: 0 0 6px; }
.compare__table { width: 100%; border-collapse: collapse; }
.compare__table th,
.compare__table td { padding: 8px 10px; text-align: right; border-bottom: 1px solid var(--border); }
.compare__table th:first-child,
.compare__table td:first-child { text-align: left; }
.compare__table tr.is-highlight { background: var(--warning-soft); font-weight: 600; }

@media (max-width: 600px) {
  .compare__table { font-size: 0.8125rem; }
  .compare__table th,
  .compare__table td { padding: 6px 6px; }
}

/* Class metrics roll-up rows (teacher class page) */
.att-list--metrics .searchable-list__search { margin: var(--s-3); width: calc(100% - 2 * var(--s-3)); }
.att-list--metrics .searchable-list__row + .searchable-list__row .att-metric-row { border-top: 1px solid var(--border); }
.att-metric-row { text-decoration: none; color: inherit; }
.att-metric-row:hover { background: var(--surface-sunken); }
.att-metric-row__stats {
  display: flex; flex-direction: column; align-items: flex-end; gap: 2px;
  flex: 0 0 auto; text-align: right;
}
.att-metric-row__rate { font-weight: 700; font-variant-numeric: tabular-nums; }
.att-metric-row__counts { color: var(--text-muted); font-size: 0.8rem; white-space: nowrap; }
.rate--low  { color: var(--danger, #dc2626); }
.rate--mid  { color: var(--warning, #d97706); }
.rate--high { color: var(--accent); }
.rate--none { color: var(--text-muted); }

@media (max-width: 420px) {
  .att-metric-row__counts { font-size: 0.72rem; }
}

/* Pupil timetable: table on desktop, stacked lists on a phone. */
.timetable-grid--narrow { display: none; }
@media (max-width: 640px) {
  .timetable-grid--wide { display: none; }
  .timetable-grid--narrow { display: block; }
}

/* ============================================================ Request failure
   Raised by static/js/request-failure-banner.js when an htmx request is
   rejected. Fixed to the bottom so it cannot be scrolled past mid-roll-call,
   and it stays until dismissed — a mark that silently failed to save is a
   safety problem, not a transient nicety. */
.request-failure {
  position: fixed;
  left: var(--s-3);
  right: var(--s-3);
  /* Clear of the iOS home indicator and any FAB. */
  bottom: calc(var(--s-3) + env(safe-area-inset-bottom, 0px));
  z-index: 1000;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-md);
  background: var(--danger);
  color: #fff;
  font-weight: 600;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
.request-failure[hidden] { display: none; }
.request-failure__text { flex: 1 1 auto; }
.request-failure__dismiss {
  flex: 0 0 auto;
  min-height: 44px;
  padding: 0 var(--s-3);
  border-radius: var(--r-md);
  border: 1px solid rgba(255, 255, 255, 0.6);
  background: transparent;
  color: #fff;
  font-weight: 600;
  cursor: pointer;
}
.request-failure__dismiss:hover { background: rgba(255, 255, 255, 0.15); }
/* Queued-while-offline is reassurance, not an error — the mark IS stored and
   will sync. Danger-red here would teach staff to distrust a safe mark. */
.request-failure--info { background: var(--accent); }
.request-failure--info .request-failure__dismiss { border-color: rgba(255,255,255,0.7); }
/* A mark made offline: stored on the device, not yet on the server. The row
   keeps its previous status because there is no server render to swap in, so
   say so explicitly rather than letting the tap look ignored. */
.evac-row--pending {
  outline: 2px dashed var(--accent);
  outline-offset: -2px;
}
.evac-row--pending .evac-row__status::after {
  content: " · queued offline";
  color: var(--accent);
  font-weight: 600;
  font-size: 0.85rem;
}

/* htmx's own indicator rules, normally injected as an inline <style> tag —
   moved here because htmx.config.includeIndicatorStyles = false (base.html)
   turns that off, since an inline style would force style-src 'unsafe-inline'. */
.htmx-indicator{opacity:0;transition:opacity 200ms ease-in}
.htmx-request .htmx-indicator{opacity:1}
.htmx-request.htmx-indicator{opacity:1}

/* ---------------------------------------------------------------------------
   Copyable code block
   ---------------------------------------------------------------------------
   <div class="code-block"> wrapping a <pre> or <code>, with a copy button
   pinned top-right. The copy itself is delegated in static/js/modal.js via
   data-action="copy-text" — no inline handler, so this survives the CSP
   tightening. The button is a real <button> so it is keyboard reachable and
   announced; its label lives in .visually-hidden text, not in the SVG.

   Padding-right on the scroller reserves the button's gutter, otherwise a long
   single line slides underneath it and the last characters are unreadable. */
.code-block { position: relative; }
.code-block > pre,
.code-block > code {
  display: block;
  margin: 0;
  overflow-x: auto;
  padding: var(--s-3);
  padding-right: calc(var(--s-3) + 2.75rem);
  background: var(--surface-soft);
  border-radius: 4px;
  font-size: 0.85rem;
}
/* For values with no spaces to break on (URLs), wrap instead of scrolling. */
.code-block--wrap > pre,
.code-block--wrap > code { word-break: break-all; white-space: pre-wrap; }

.code-block__copy {
  position: absolute;
  top: var(--s-2);
  right: var(--s-2);
  min-height: 32px;
  padding: 0 var(--s-2);
  line-height: 0;
}
/* The two icons are stacked in the button; only one shows at a time, so the
   button's width never changes and the layout can't jump on copy. */
.code-block__copy .code-block__icon--done,
.code-block__copy.is-copied .code-block__icon--copy { display: none; }
.code-block__copy.is-copied .code-block__icon--done { display: inline; }
.code-block__copy.is-copied { color: var(--success); }
/* Clipboard unavailable (plain http on a LAN host) or refused: the text has
   been selected for a manual copy, so warn rather than claim success. */
.code-block__copy.is-copy-failed { color: var(--warning); }
