/* ==========================================================================
   GoSchool — Admin Web Application Stylesheet
   Modern, glassmorphic UI design system with dynamic CSS variables
   ========================================================================== */

:root {
  /* Default Branding Colors (Overridden dynamically by applyTheme) */
  --color-primary: #1e40af;
  --color-secondary: #3b82f6;
  --color-primary-rgb: 30, 64, 175;
  --color-secondary-rgb: 59, 130, 246;

  /* UI Color Tokens */
  --bg-dark: #0f172a;
  --bg-surface: #1e293b;
  --card-bg: rgba(30, 41, 59, 0.7);
  --card-border: rgba(255, 255, 255, 0.1);
  /* ── The text ladder ──────────────────────────────────────────────────────
     Three steps, and every one of them now passes WCAG AA for body text
     (4.5:1) against the card, the page and the inside of an input. The
     bottom step used to be #64748b at 3.27:1, which is below the line for
     anything that has to be read — and it was the colour of every hint,
     placeholder and empty-state sentence in the app.

     The ladder moved up a step rather than being flattened: muted is what
     secondary used to be, and secondary is lighter again, so the hierarchy
     a reader relies on survives (1.73:1 between the two, which the eye
     separates comfortably without either one being faint). */
  --text-primary: #f8fafc;    /* 14.9:1 on a card */
  --text-secondary: #cbd5e1;  /* 10.5:1 — labels, which is where the report started */
  --text-muted: #94a3b8;      /*  6.1:1 — hints, placeholders, empty states */

  /* ── Short names the inline styles in app.js use ──────────────────────────
     These were written from the beginning and defined nowhere, and an
     undefined custom property makes the whole declaration invalid — so the
     browser dropped every one of them. 25 panels rendered with no background,
     12 delete and drop buttons rendered in the ordinary text colour rather
     than red, 10 bordered boxes drew no border, and the bulk-intake progress
     bar had no fill at all: it advanced invisibly.

     Defined as aliases rather than rewritten across fifty inline styles, and
     as var() references rather than copies, so a school's own primary colour
     set by applyTheme() still reaches everything that names it. */
  --bg-surface-subtle: rgba(255, 255, 255, 0.05);
  --border-color: var(--card-border);

  /* ── The edge of something you have to find ───────────────────────────────
     A text field was drawn with --card-border, 10% white, which measures
     1.36:1 against the card behind it. WCAG wants 3:1 for the boundary of a
     control, and the gap between those two numbers is the whole of the
     report from the school: the fields were not hard to read, they were hard
     to *locate*. 34% white measures 3.04:1.

     Kept separate from --card-border on purpose. A card's edge is decoration
     and has no threshold to meet; drawing every panel this strongly would
     turn the page into a grid of boxes. */
  --input-border: rgba(255, 255, 255, 0.34);
  --input-border-hover: rgba(255, 255, 255, 0.52);
  --input-bg: rgba(15, 23, 42, 0.6);

  /* ── A colour for text that links somewhere ───────────────────────────────
     --color-primary is whatever a school picked for its own branding, so
     nothing legible can depend on it; the default alone measures 1.78:1 as
     text on a dark card. This one is fixed and measures 6.1:1. */
  --link-color: #60a5fa;
  --danger: var(--color-danger);
  --primary: var(--color-primary);

  /* Semantic Colors */
  --color-success: #10b981;
  --color-success-bg: rgba(16, 185, 129, 0.15);
  --color-warning: #f59e0b;
  --color-warning-bg: rgba(245, 158, 11, 0.15);
  /* Lightened from #ef4444, which measured 4.14:1 as text — just under the
     line, on the one thing in the app that must always be read. The fill for
     a destructive *button* goes the other way, darker, because what needs
     contrast there is the white sitting on top of it. */
  --color-danger: #f87171;        /* 5.6:1 as text on a card */
  --color-danger-strong: #dc2626; /* as a fill: white on it is 4.8:1 */
  --color-danger-bg: rgba(248, 113, 113, 0.15);
  --color-info: #06b6d4;
  --color-info-bg: rgba(6, 182, 212, 0.15);

  /* Typography */
  --font-sans: 'Outfit', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  
  /* Layout Geometry */
  --sidebar-width: 260px;
  --header-height: 70px;
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 20px;

  /* Elevation & Shadows */
  --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.2);
  --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.3);
  --shadow-lg: 0 20px 40px rgba(0, 0, 0, 0.4);
  --glass-glow: 0 0 25px rgba(var(--color-secondary-rgb), 0.15);

  /* Animations */
  --transition-fast: 0.15s ease;
  --transition-normal: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Reset & Base Setup */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: var(--font-sans);
  background-color: var(--bg-dark);
  color: var(--text-primary);
  min-height: 100vh;
  overflow-x: hidden;
  line-height: 1.5;
  background-image: 
    radial-gradient(circle at 10% 20%, rgba(var(--color-primary-rgb), 0.18) 0%, transparent 40%),
    radial-gradient(circle at 90% 80%, rgba(var(--color-secondary-rgb), 0.15) 0%, transparent 40%);
  background-attachment: fixed;
}

/* Typography Utilities */
h1, h2, h3, h4, h5, h6 {
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.02em;
}

/* Pre-login Auth Screen */
.auth-wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: 20px;
}

.auth-card {
  width: 100%;
  max-width: 440px;
  background: var(--card-bg);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-lg);
  padding: 40px;
  box-shadow: var(--shadow-lg), var(--glass-glow);
  animation: fadeIn 0.4s ease-out;
}

.auth-header {
  text-align: center;
  margin-bottom: 32px;
}

.auth-logo {
  width: 56px;
  height: 56px;
  border-radius: 14px;
  background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-size: 28px;
  margin-bottom: 16px;
  box-shadow: 0 8px 16px rgba(var(--color-primary-rgb), 0.4);
}

.auth-title {
  font-size: 24px;
  margin-bottom: 6px;
}

.auth-subtitle {
  color: var(--text-secondary);
  font-size: 14px;
}

/* Forms */
.form-group {
  margin-bottom: 20px;
}

.form-label {
  display: block;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-secondary);
  margin-bottom: 8px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.form-input, .form-select, .form-textarea {
  width: 100%;
  padding: 12px 16px;
  background: var(--input-bg);
  border: 1px solid var(--input-border);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: 15px;
  transition: var(--transition-fast);
}

.form-input:focus, .form-select:focus, .form-textarea:focus {
  outline: none;
  border-color: var(--color-secondary);
  box-shadow: 0 0 0 3px rgba(var(--color-secondary-rgb), 0.25);
  background: rgba(15, 23, 42, 0.8);
}

/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px 20px;
  border-radius: var(--radius-sm);
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  border: none;
  transition: var(--transition-fast);
  text-decoration: none;
}
/* A button that cannot be pressed has to look it (owner, 2026-10-28): with no
   rule here, a disabled button looked exactly like a live one, so a sign-in in
   progress read as a box nobody had touched. */
.btn:disabled,
.btn[disabled] {
  opacity: 0.55;
  cursor: not-allowed;
  filter: saturate(0.6);
}
.btn:disabled:hover,
.btn[disabled]:hover { transform: none; }

.btn-primary {
  background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
  color: white;
  box-shadow: 0 4px 12px rgba(var(--color-primary-rgb), 0.3);
}

.btn-primary:hover {
  transform: translateY(-1px);
  box-shadow: 0 6px 16px rgba(var(--color-primary-rgb), 0.4);
  filter: brightness(1.08);
}

.btn-secondary {
  background: rgba(255, 255, 255, 0.08);
  color: var(--text-primary);
  border: 1px solid var(--card-border);
}

.btn-secondary:hover {
  background: rgba(255, 255, 255, 0.14);
}

.btn-danger {
  background: var(--color-danger-strong);
  color: white;
}

.btn-danger:hover {
  filter: brightness(1.1);
}

.btn-sm {
  padding: 6px 12px;
  font-size: 13px;
  border-radius: 6px;
}

.btn-block {
  width: 100%;
}

/* Layout Shell */
.app-layout {
  display: flex;
  min-height: 100vh;
}

/* Sidebar */
.sidebar {
  width: var(--sidebar-width);
  background: rgba(15, 23, 42, 0.85);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border-right: 1px solid var(--card-border);
  display: flex;
  flex-direction: column;
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  z-index: 100;
  transition: var(--transition-normal);
}

.sidebar-brand {
  height: var(--header-height);
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 24px;
  border-bottom: 1px solid var(--card-border);
}

.brand-icon {
  width: 38px;
  height: 38px;
  border-radius: 10px;
  background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-size: 20px;
  font-weight: bold;
}

.brand-title {
  font-size: 17px;
  font-weight: 700;
  line-height: 1.2;
}

.brand-subtitle {
  font-size: 11px;
  color: var(--color-secondary);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.nav-menu {
  list-style: none;
  padding: 20px 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 1;
  /* The sidebar is fixed to the window height and cannot scroll itself, so
     when the menu and the account controls together are taller than the
     window, something has to give. It must not be the footer: Sign out and
     Change password live there, and at 520px they sat below the edge with no
     way to reach them. The menu scrolls instead. overflow-y also zeroes this
     flex item's automatic minimum height, which is what lets it shrink at
     all. Checked by tools/check-modal-scroll.php. */
  overflow-y: auto;
  scrollbar-width: thin;
}

.nav-item a {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  color: var(--text-secondary);
  text-decoration: none;
  border-radius: var(--radius-sm);
  font-size: 14px;
  font-weight: 500;
  transition: var(--transition-fast);
}

.nav-item a:hover {
  color: var(--text-primary);
  background: rgba(255, 255, 255, 0.05);
}

.nav-item.active a {
  color: white;
  background: linear-gradient(90deg, rgba(var(--color-primary-rgb), 0.5), rgba(var(--color-secondary-rgb), 0.2));
  border-left: 3px solid var(--color-secondary);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}

.nav-icon {
  font-size: 18px;
  width: 22px;
  text-align: center;
}

.sidebar-footer {
  padding: 16px;
  border-top: 1px solid var(--card-border);
}

.user-badge {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, 0.03);
  border-radius: var(--radius-sm);
}

.user-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--color-primary);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  color: white;
}

.user-info {
  flex: 1;
  overflow: hidden;
}

.user-name {
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.user-role {
  font-size: 11px;
  color: var(--text-muted);
}

/* Account actions under the user badge. Stacked full-width so the labels
   fit the 260px sidebar without wrapping mid-word. */
.sidebar-account-actions {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 10px;
}

.sidebar-account-actions .btn {
  width: 100%;
  justify-content: center;
}

/* Main Content Area */
.main-wrapper {
  margin-left: var(--sidebar-width);
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.header {
  height: var(--header-height);
  background: rgba(15, 23, 42, 0.6);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--card-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 32px;
  position: sticky;
  top: 0;
  z-index: 90;
}

.page-title {
  font-size: 20px;
  font-weight: 700;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 16px;
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 20px;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.badge-success { background: var(--color-success-bg); color: var(--color-success); border: 1px solid rgba(16, 185, 129, 0.3); }
.badge-warning { background: var(--color-warning-bg); color: var(--color-warning); border: 1px solid rgba(245, 158, 11, 0.3); }
.badge-danger  { background: var(--color-danger-bg); color: var(--color-danger); border: 1px solid rgba(239, 68, 68, 0.3); }
.badge-info    { background: var(--color-info-bg); color: var(--color-info); border: 1px solid rgba(6, 182, 212, 0.3); }

.content-area {
  padding: 32px;
  flex: 1;
}

/* Dashboard Cards Grid */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 20px;
  margin-bottom: 32px;
}

.stat-card {
  background: var(--card-bg);
  backdrop-filter: blur(12px);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-md);
  padding: 20px;
  box-shadow: var(--shadow-sm);
  transition: var(--transition-fast);
  position: relative;
  overflow: hidden;
}

.stat-card:hover {
  transform: translateY(-2px);
  border-color: rgba(255, 255, 255, 0.2);
  box-shadow: var(--shadow-md);
}

.stat-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 4px;
  height: 100%;
  background: var(--color-secondary);
}

.stat-card.primary::before { background: var(--color-primary); }
.stat-card.success::before { background: var(--color-success); }
.stat-card.warning::before { background: var(--color-warning); }
.stat-card.danger::before  { background: var(--color-danger); }

.stat-title {
  font-size: 13px;
  color: var(--text-secondary);
  font-weight: 500;
  margin-bottom: 8px;
}

.stat-value {
  font-size: 26px;
  font-weight: 700;
  color: var(--text-primary);
}

/* Section Cards & Tables */
.card {
  background: var(--card-bg);
  backdrop-filter: blur(12px);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  margin-bottom: 24px;
  overflow: hidden;
}

.card-header {
  padding: 20px 24px;
  border-bottom: 1px solid var(--card-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.card-title {
  font-size: 16px;
  font-weight: 600;
}

.card-body {
  padding: 24px;
}

/* Data Table */
.table-responsive {
  overflow-x: auto;
}

.table {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
  font-size: 14px;
}

.table th {
  padding: 14px 20px;
  background: rgba(15, 23, 42, 0.4);
  color: var(--text-secondary);
  font-weight: 600;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  border-bottom: 1px solid var(--card-border);
}

/* ── A column you can sort by ───────────────────────────────────────
   The control sits in the header of the column it sorts. Anywhere else — a
   row of buttons above the table, a dropdown of column names — and the reader
   has to work out which control goes with which column, which is work the
   header does for free by being in the right place.

   Search already answered "is this one here". These answer "who owes the
   most", "who joined last", "who has not paid since July" — questions about a
   group, which no amount of searching will answer. */
.table th.sortable {
  cursor: pointer;
  user-select: none;
  position: relative;
  padding-right: 32px;   /* the arrow's room, reserved, so the label never
                            shifts sideways when a column becomes sorted */
  white-space: nowrap;
}

.table th.sortable:hover {
  color: var(--text-primary);
  background: rgba(255, 255, 255, 0.07);
}

/* Keyboard focus needs its own mark. A hover colour is not a focus ring, and
   somebody tabbing through the headers has no pointer to follow. */
.table th.sortable:focus-visible {
  outline: none;
  color: var(--text-primary);
  box-shadow: inset 0 0 0 2px var(--link-color);
}

/* Unsorted: a dim pair of arrows, which is the only thing telling a reader
   that the header is a control and not just a label. */
.table th.sortable::after {
  content: '⇅';
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 12px;
  font-weight: 400;
  /* 3.34:1 against the composited header background, measured in a browser.
     Not decoration: this glyph is the only thing telling a reader the header
     is a control at all, so it has to meet the 3:1 this file already holds
     every control's boundary to — the school's report was that things were
     hard to *locate*, not hard to read. 0.4 measured 2.92:1. */
  opacity: 0.45;
  transition: opacity 120ms ease;
}

.table th.sortable:hover::after,
.table th.sortable:focus-visible::after { opacity: 0.85; }

/* Sorted: one arrow, pointing the way the column now reads.
   --link-color rather than --color-primary: the primary is whatever colour a
   school picked for its branding and measures as little as 1.78:1 here, so
   nothing a reader has to see can be drawn in it. This one is fixed at
   6.1:1 — the same reasoning as every other legible accent in this file. */
.table th.sortable.is-sorted {
  color: var(--text-primary);
  background: rgba(96, 165, 250, 0.12);
}

.table th.sortable.is-sorted::after {
  content: '↑';
  opacity: 1;
  font-size: 14px;
  font-weight: 700;
  color: var(--link-color);
}

.table th.sortable.is-sorted.is-desc::after { content: '↓'; }

.table td {
  padding: 16px 20px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  color: var(--text-primary);
}

/* A code is one identifier and never reads as several.
   Measured on the Books screen: "BK-MTH-B5" broke at its own hyphens onto
   three lines, which both reads as three things and made that row 99px tall
   against 62px for the row beneath it — a ragged column where the eye is
   meant to run straight down. */
.table td code {
  white-space: nowrap;
}

.table tr:hover td {
  background: rgba(255, 255, 255, 0.02);
}

/* Tabs & Filters */
.toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}

.filter-group {
  display: flex;
  align-items: center;
  gap: 12px;
}

.tabs {
  display: flex;
  gap: 4px;
  background: rgba(15, 23, 42, 0.6);
  padding: 4px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--card-border);
}

.tab {
  padding: 8px 16px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-secondary);
  background: transparent;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: var(--transition-fast);
}

/* ── A tab that is still fetching ────────────────────────────────────────────
 *
 * Reported from the school: switching between Confirmed, Pending and All
 * looked like nothing had happened, and the only way to know the click had
 * registered was to wait and find out. The table says so too, but the tab is
 * where the finger just was, and on a narrow screen the table may not even be
 * in view.
 *
 * A ring on the tab itself rather than a replacement for the label, so the
 * count beside it does not jump and the tab does not change width mid-click.
 */
.tab.is-loading::after {
  content: '';
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-left: 7px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: inline-spin 0.7s linear infinite;
  vertical-align: -1px;
}

/* No animation for somebody who has asked for none. The ring still appears,
   so the tab still says "working" — it simply does not move. */
@media (prefers-reduced-motion: reduce) {
  .tab.is-loading::after { animation: none; opacity: 0.6; }
}

.tab.active {
  color: white;
  background: var(--color-primary);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}

/* Modals */
.modal-backdrop {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(15, 23, 42, 0.8);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
  padding: 24px;
  overflow-y: auto;
}

.modal-backdrop.show {
  opacity: 1;
  pointer-events: auto;
}

/* 640px was an inline style on the markup until the grid needed a wider modal —
   an inline style beats a class, so .modal-wide could never have won. Same
   number, moved here, so every modal looks exactly as it did. */
.modal {
  width: 100%;
  max-width: 640px;
  max-height: calc(100vh - 48px);
  display: flex;
  flex-direction: column;
  background: var(--bg-surface);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  transform: translateY(20px);
  transition: transform 0.2s ease;
  margin: auto;
}

.modal-backdrop.show .modal {
  transform: translateY(0);
}

/* For a modal that is a table of fields rather than a form: filling in twenty
   pupils inside 640px means typing into a horizontal scrollbar. Bounded by the
   viewport as well, so a narrow laptop still sees the whole width. */
/* 1240 rather than a round 1200: with every optional column shown the grid is
   1157px of columns, and anything narrower leaves it scrolling sideways inside
   its own box. Capped against the viewport so a 1280-wide laptop still fits. */
.modal.modal-wide {
  max-width: min(1240px, 96vw);
}

/* The entry grid. Cells are deliberately tight — a row has to read as a row,
   and the inputs carry their own borders in the table already. */
.grid-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.grid-table th {
  text-align: left;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  font-weight: 600;
  padding: 6px 6px;
  white-space: nowrap;
  border-bottom: 1px solid var(--card-border);
  position: sticky;
  top: 0;
  background: var(--bg-surface);
  z-index: 1;
}

.grid-table td {
  padding: 3px 4px;
  vertical-align: middle;
}

.grid-table .form-input,
.grid-table .form-select {
  padding: 6px 8px;
  font-size: 13px;
  min-width: 0;
  width: 100%;
}

/* A cell the pre-check flagged. Not colour alone — the reason is printed above
   the grid too, because a red outline says something is wrong and never what. */
.grid-table .cell-flagged {
  border-color: var(--danger, #c0392b);
  box-shadow: 0 0 0 1px var(--danger, #c0392b) inset;
}

/* Half the subtle surface: a stripe that separates rows without turning every
   other one into a panel. */
.grid-table tbody tr:nth-child(even) {
  background: rgba(255, 255, 255, 0.025);
}

/* Date of birth and gender are optional on the single-pupil form too. Off by
   default so the grid opens narrow — a wall of fields is where somebody decides
   this is not for them.

   Relationship used to be in here, and that was a bug rather than a saving: the
   intake refuses a row whose relationship does not say which member of the
   family the parent is, so hiding the column meant being refused for a field
   with nowhere on screen to answer it. */
.grid-table.hide-optional .g-optional {
  display: none;
}

/* Surnames the school already holds, offered as one is typed.

   `fixed`, not absolute: the grid is a scrolling box with `overflow: auto`, so
   a list positioned inside it would be clipped at the edge or would scroll away
   from the field it belongs to. Positioned over the input on each keystroke
   instead — see showSurnameSuggestions(). */
.surname-suggest {
  position: fixed;
  z-index: 1200; /* above the modal, which is where every caller of this lives */
  background: var(--bg-surface, #fff);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-sm);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
  overflow: hidden;
  font-size: 13px;
  max-width: 280px;
}

.surname-suggest-item {
  padding: 6px 10px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.surname-suggest-item:hover,
.surname-suggest-item.is-active {
  background: var(--bg-surface-subtle);
}

.modal-header {
  padding: 18px 24px;
  border-bottom: 1px solid var(--card-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
}

.modal-close {
  background: transparent;
  border: none;
  color: var(--text-secondary);
  font-size: 24px;
  cursor: pointer;
  padding: 0 4px;
  line-height: 1;
}

.modal-body {
  padding: 24px;
  overflow-y: auto;
  flex: 1 1 auto;
  -webkit-overflow-scrolling: touch;
}

.modal-footer {
  padding: 16px 24px;
  background: rgba(15, 23, 42, 0.4);
  border-top: 1px solid var(--card-border);
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  flex-shrink: 0;
}

/* Toast Notifications */
.toast-container {
  position: fixed;
  bottom: 24px;
  right: 24px;
  z-index: 1100;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.toast {
  padding: 14px 20px;
  border-radius: var(--radius-sm);
  background: var(--bg-surface);
  color: white;
  border: 1px solid var(--card-border);
  box-shadow: var(--shadow-md);
  font-size: 14px;
  display: flex;
  align-items: center;
  gap: 10px;
  animation: slideInRight 0.3s ease;
}

.toast-success { border-left: 4px solid var(--color-success); }
.toast-error   { border-left: 4px solid var(--color-danger); }

/* ── The bar that confirms or rejects what is ticked ─────────────────────────
 *
 * Stays just under the header while the list scrolls past. Ticking pupils is
 * done going down a list, and the buttons used to stay at the top of it — a
 * super admin ticking the fortieth pending pupil had to scroll all the way back
 * up to press Confirm. Solid, not the cards' see-through glass, because rows
 * slide underneath it and must not show through the buttons.
 */
/* Fixed to the bottom of the screen, over the content column, buttons in the
   middle and large — see the note on #students-bulk-bar (owner, 2026-10-28). */
.bulk-action-bar {
  position: fixed;
  left: calc(var(--sidebar-width) + 24px);
  right: 24px;
  bottom: 16px;
  z-index: 90;
  background: var(--bg-surface, #1e293b);
  border: 2px solid var(--color-primary, #3b82f6);
  border-radius: var(--radius-md, 12px);
  box-shadow: 0 -6px 32px rgba(2, 6, 23, 0.6);
  animation: bulk-bar-in 0.25s ease-out;
}
.bulk-action-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 16px 20px;
  text-align: center;
}
.bulk-action-count { font-size: 18px; line-height: 1.4; }
.bulk-action-buttons {
  display: flex;
  justify-content: center;
  gap: 12px;
  flex-wrap: wrap;
}
.bulk-action-buttons .btn {
  font-size: 17px;
  padding: 14px 24px;
  min-width: 170px;
}
/* Room under the list so its last rows are never hidden behind the bar. */
body.has-bulk-bar .content-area { padding-bottom: 190px; }
@keyframes bulk-bar-in {
  from { transform: translateY(24px); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .bulk-action-bar { animation: none; }
}
@media (max-width: 768px) {
  .bulk-action-bar { left: 12px; right: 12px; bottom: 12px; }
  .bulk-action-buttons .btn { min-width: 0; flex: 1 1 140px; }
}

/* ── Something went wrong: top and centre ────────────────────────────────────
 *
 * Errors used to share the corner with "Saved", at the same size, and went
 * after four seconds — watched at a school, a secretary looking at the form
 * she was filling in never saw one. A failure is the message somebody most
 * needs and was the easiest to miss. So it comes down from the top of the
 * screen, in the middle, above any open dialog — for five seconds, or until
 * closed (showErrorAlert in app.js).
 * "Saved" keeps the corner: it is good news, and nobody has to act on it.
 */
.toast-alerts {
  position: fixed;
  top: 18px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1450; /* above dialogs (1000) and questions (1400) */
  width: min(640px, calc(100vw - 32px));
  display: flex;
  flex-direction: column;
  gap: 10px;
  pointer-events: none;
}

.toast-alert {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 16px 18px;
  border-radius: var(--radius-md, 12px);
  background: #7f1d1d;
  border: 2px solid #fca5a5;
  color: #fff;
  font-size: 15.5px;
  line-height: 1.5;
  box-shadow: 0 18px 48px rgba(2, 6, 23, 0.6);
  animation: alertDrop 0.25s ease;
}

.toast-alert-icon { font-size: 20px; line-height: 1.2; }
.toast-alert-text { flex: 1; }

.toast-alert-close {
  flex-shrink: 0;
  border: 1px solid rgba(255, 255, 255, 0.55);
  background: transparent;
  color: #fff;
  border-radius: var(--radius-sm, 8px);
  padding: 4px 12px;
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
}

.toast-alert-close:hover { background: rgba(255, 255, 255, 0.12); }

@keyframes alertDrop {
  from { opacity: 0; transform: translateY(-12px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Sign in again over whatever is on the screen — above the error toasts too,
   since it is the answer to the error they would show. */
.signin-again-backdrop { z-index: 1500; }
/* While it is open the red notices say only what it already says, and on a
   narrow screen they cover its heading. They come back if it is dismissed. */
body:has(.signin-again-backdrop) .toast-alerts { display: none; }
.signin-again-who {
  margin: 0 0 4px;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text-primary, #f8fafc);
}

/* ── A toast that is also a timer ────────────────────────────────────────────
 *
 * The end of the bulk intake, where the dialog closes itself. Saying so, and
 * showing how long is left, is the difference between a screen that finished
 * and a screen that did something unexplained. Clickable, so anyone who has
 * read it can take the rest of the wait away.
 */
.toast-countdown {
  position: relative;
  overflow: hidden;
  padding-bottom: 17px;
  cursor: pointer;
  min-width: 300px;
}

.toast-countdown .toast-count {
  min-width: 20px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  font-weight: 700;
  color: var(--color-success);
  border: 1px solid var(--color-success);
  border-radius: 10px;
  padding: 1px 7px;
}

.toast-progress {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 3px;
  background: rgba(255, 255, 255, 0.12);
}

.toast-progress-bar {
  height: 100%;
  width: 100%;
  background: var(--color-success);
}

/* Keyframe Animations */
@keyframes fadeIn {
  from { opacity: 0; transform: scale(0.98); }
  to { opacity: 1; transform: scale(1); }
}

@keyframes slideInRight {
  from { opacity: 0; transform: translateX(30px); }
  to { opacity: 1; transform: translateX(0); }
}

/* Responsive adjustments */
@media (max-width: 768px) {
  .sidebar {
    transform: translateX(-100%);
  }
  .sidebar.open {
    transform: translateX(0);
  }
  .main-wrapper {
    margin-left: 0;
  }
  .header {
    padding: 0 16px;
  }
  .content-area {
    padding: 16px;
  }
}

/* ── Utility nav items ───────────────────────────────────────────────────────
   Settings is configuration a user visits occasionally, not one of the views
   they work in all day. `margin-top: auto` drops it to the bottom of the nav
   column (which is already a flex column with `flex: 1`), and the lighter
   weight keeps it from competing with the items above for attention. It still
   highlights normally when active — quieter, not hidden. */
.nav-item-utility {
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid var(--card-border);
}

.nav-item-utility a {
  color: var(--text-muted);
  font-weight: 400;
}

.nav-item-utility a:hover {
  color: var(--text-primary);
}

/* ── Settings sections ───────────────────────────────────────────────────── */
.settings-tabs {
  display: flex;
  gap: 4px;
  border-bottom: 1px solid var(--card-border);
  margin-bottom: 20px;
  flex-wrap: wrap;
}

.settings-tab {
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  color: var(--text-secondary);
  padding: 10px 16px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: var(--transition-fast);
}

.settings-tab:hover { color: var(--text-primary); }

.settings-tab.active {
  color: var(--text-primary);
  border-bottom-color: var(--color-secondary);
}

/* ── Settings → Backup & restore ─────────────────────────────────────────────
 * Last tab, pushed to the far end and coloured as a warning, so nobody lands
 * on it on the way to the logo. */
.settings-tab-danger { margin-left: auto; color: var(--color-danger); }
.settings-tab-danger.active { color: var(--color-danger); border-bottom-color: var(--color-danger); }

.data-gate { border-left: 4px solid var(--color-warning); }
.data-gate h3 { margin: 0 0 8px; font-size: 16px; }
.data-gate p { font-size: 14px; color: var(--text-secondary); margin: 0 0 12px; }

.data-notes { font-size: 13px; color: var(--text-secondary); margin: 0 0 14px; padding-left: 18px; }
.data-notes li { margin-bottom: 6px; }

.data-password-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
.data-done { margin: 10px 0 0; font-size: 13px; color: var(--color-success); }

.data-danger-card { border-left: 4px solid var(--color-danger); }
.data-danger-box {
  background: var(--color-danger-bg);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  margin: 10px 0 18px;
  font-size: 13px;
  color: var(--text-primary);
}
.data-danger-box ul { margin: 8px 0 0; padding-left: 18px; }
.data-danger-box li { margin-bottom: 6px; }

.data-step { border-top: 1px solid var(--border-color); padding-top: 14px; margin-top: 14px; }
.data-step-title { font-weight: 600; font-size: 14px; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
.data-step-n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--bg-surface-subtle); border: 1px solid var(--border-color);
  font-size: 12px;
}
.data-step-hint { font-size: 12.5px; color: var(--text-muted); margin: 6px 0 0; }
.data-step.is-done .data-step-n { background: var(--color-success-bg); border-color: var(--color-success); }

.data-compare { width: 100%; border-collapse: collapse; font-size: 13px; margin: 12px 0 4px; }
.data-compare th, .data-compare td { padding: 6px 8px; border-bottom: 1px solid var(--border-color); text-align: right; }
.data-compare th:first-child, .data-compare td:first-child { text-align: left; }
.data-compare .data-loss { color: var(--color-danger); font-weight: 600; }

.data-understand { display: flex; gap: 8px; align-items: flex-start; font-size: 13.5px; margin: 4px 0 14px; }
.data-understand input { margin-top: 3px; }
.data-warn-line { color: var(--color-danger); font-size: 13px; margin: 8px 0 0; }
.data-ok-line { color: var(--text-secondary); font-size: 13px; margin: 8px 0 0; }

@media (max-width: 600px) {
  .settings-tab-danger { margin-left: 0; }
}

.logo-preview {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px;
  border: 1px dashed var(--card-border);
  border-radius: var(--radius-sm);
  margin-bottom: 12px;
}

.logo-preview img {
  max-height: 64px;
  max-width: 180px;
  /* Receipts print on white; a logo previewed on the app's dark chrome can
     look fine here and be invisible on paper. */
  background: #fff;
  padding: 6px;
  border-radius: 4px;
}

/* Settings → Calendar */
.cal-add { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; margin-top: 12px; }
.cal-add .form-group { flex: 1; min-width: 140px; margin-bottom: 0; }
.cal-public { margin-top: 16px; border-top: 1px solid var(--card-border); padding-top: 12px; }
.cal-public > summary { cursor: pointer; font-size: 13.5px; font-weight: 600; }
.cal-holiday { display: flex; gap: 10px; align-items: flex-start; padding: 6px 0; border-bottom: 1px solid var(--card-border); font-size: 13.5px; }
.cal-holiday input { margin-top: 3px; }
.cal-holiday .cal-note { color: var(--text-muted); font-size: 12.5px; }
.cal-notes { color: var(--text-muted); font-size: 12.5px; margin: 10px 0; padding-left: 18px; }
#cal-days-off td { vertical-align: middle; }

/* A small "working on it" mark for anything that waits on the server.
   Saves can take a few seconds, and a control that changes nothing for that
   long reads as broken — a user reported having to guess whether a section
   change had registered at all. */
.inline-spinner {
  display: inline-block;
  width: 12px;
  height: 12px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: inline-spin 0.7s linear infinite;
  vertical-align: -2px;
  flex-shrink: 0;
}

@keyframes inline-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .inline-spinner { animation-duration: 2s; }
}

.saving-note {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-left: 8px;
  font-size: 11px;
  color: var(--text-muted);
  text-transform: none;
}

/* Author display rules beat the hidden attribute, so restate it — otherwise
   the note above would show permanently. */
.saving-note[hidden] {
  display: none;
}

/* ── An error somebody has to act on ─────────────────────────────────────────
 *
 * The old inline error was a subtle surface with a 3px bar: a school
 * administrator and the school's owner stood in front of the screen together
 * and neither of them saw it. On a dark theme `--bg-surface-subtle` against
 * the card behind it is almost no contrast at all, and 13px grey text below a
 * tall grid reads as a footnote.
 *
 * So an error now looks like one — a filled danger panel, a solid bar, a
 * marker, and text at body size rather than caption size. `showInlineError()`
 * also scrolls it into view, because contrast cannot help with something that
 * is off screen.
 *
 * Deliberately not a toast: these list several things to fix and stay on
 * screen while they are being fixed. Toasts are for what just happened.
 */
.form-error-box {
  display: none;
  margin: 12px 0;
  padding: 12px 14px 12px 42px;
  position: relative;
  border-radius: var(--radius-sm);
  background: var(--color-danger-bg, rgba(239, 68, 68, 0.15));
  border: 1px solid var(--color-danger, #ef4444);
  border-left-width: 5px;
  color: var(--text-primary, inherit);
  font-size: 14px;
  line-height: 1.45;
}

/* The marker is content, not decoration — it survives a copy-paste of the
   message into an email asking for help, and it reads to a screen reader as
   part of the alert. */
.form-error-box::before {
  content: "!";
  position: absolute;
  left: 13px;
  top: 12px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--color-danger, #ef4444);
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}

.form-error-box strong { color: inherit; }

/* Shown by script, so the class alone must not reveal it. */
.form-error-box.is-shown { display: block; }

/* ── A panel held still while something happens to it ────────────────────────
 *
 * See beginBlockingWait() in @school-suite/core. Reported from a sign-in on a
 * poor connection: the button said "Signing in…" and nothing else changed, so
 * the form was still typeable, Enter still resubmitted it, and a motionless
 * label was indistinguishable from a frozen page. The user pressed it again.
 *
 * The cover is the part that does the work. Disabling the controls underneath
 * matters for a keyboard and a screen reader; for everybody else, not being
 * able to reach the form is the whole message.
 */
.blocking-wait {
  position: absolute;
  inset: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  border-radius: inherit;
  /* Translucent rather than opaque: the form stays visible behind it, so it
     reads as "this is busy" rather than "this has been replaced". */
  background: rgba(10, 14, 20, 0.72);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  animation: blocking-wait-in 0.18s ease;
  cursor: progress;
}

/* No fade for anybody who has asked for less motion — it still appears, it
   just does not animate in. */
@media (prefers-reduced-motion: reduce) {
  .blocking-wait { animation: none; }
}

@keyframes blocking-wait-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.blocking-wait-inner {
  text-align: center;
  max-width: 300px;
}

.blocking-wait-spinner {
  width: 30px;
  height: 30px;
  margin: 0 auto 14px;
  border: 3px solid rgba(255, 255, 255, 0.25);
  border-top-color: var(--color-primary, #3b82f6);
  border-radius: 50%;
  animation: blocking-wait-spin 0.8s linear infinite;
}

/* Slowed rather than stopped: a spinner that does not move is the thing this
   replaced, so it keeps turning even here — just gently. */
@media (prefers-reduced-motion: reduce) {
  .blocking-wait-spinner { animation-duration: 2.4s; }
}

@keyframes blocking-wait-spin {
  to { transform: rotate(360deg); }
}

.blocking-wait-message {
  margin: 0;
  color: #fff;
  font-size: 15px;
  font-weight: 600;
}

.blocking-wait-slow {
  margin: 8px 0 0;
  color: rgba(255, 255, 255, 0.75);
  font-size: 13px;
  line-height: 1.45;
}

.blocking-wait-cancel {
  margin-top: 16px;
  padding: 7px 16px;
  border-radius: var(--radius-sm, 6px);
  border: 1px solid rgba(255, 255, 255, 0.4);
  background: transparent;
  color: #fff;
  font-size: 13px;
  cursor: pointer;
}

.blocking-wait-cancel:hover {
  background: rgba(255, 255, 255, 0.12);
}

/* ── A small action inside a grid cell ───────────────────────────────────────
 *
 * "＋ the other parent" and its ✕. A button rather than a link because it
 * does something on this page rather than going anywhere, and styled down to
 * a link because a second real button in every cell of a twelve-column grid
 * reads as a wall of buttons.
 *
 * Underlined, not colour alone: the surrounding text is already muted, and a
 * difference of shade is exactly the contrast problem reported from the
 * school.
 */
.cell-link {
  display: inline-block;
  margin-top: 3px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--link-color, #60a5fa);
  font-size: 11.5px;
  text-decoration: underline;
  cursor: pointer;
  white-space: nowrap;
}

.cell-link:hover { color: var(--text-primary, #fff); }

/* ── The parent and relationship cells of the entry grid ─────────────────────
 *
 * Both hold one dropdown, or two when a pupil has both parents to enter. The
 * two stack, and **the stack is the only thing in the cell that has height** —
 * which is what makes the row work:
 *
 *   · the cells are `vertical-align: middle`, so a stack of one centres on the
 *     row's centreline and a stack of two centres as a block on the same line;
 *   · the control that adds or removes the second parent sits in its own
 *     column beside the stack rather than under it. As a line beneath the
 *     select it made this cell taller than its neighbours, so the parent
 *     dropdown sat above the centreline every other field in the row sat on —
 *     and revealing the second parent moved it again;
 *   · the same stack, with the same gap, is used by the relationship cell, so
 *     the first parent lines up with the first relationship and the second
 *     with the second. They are one thought split across two columns.
 */
/* One dropdown and, on the line it belongs to, the control that acts on it.
 *
 * A grid rather than a flex row so the control's column is reserved on every
 * line whether or not the control is on it. As a flex row the dropdown with
 * no control beside it stretched into the space, and the two parents of one
 * pupil ended up different widths — which is the opposite of reading as a
 * pair. */
.g-line {
  display: grid;
  grid-template-columns: 1fr 26px;
  align-items: center;
  gap: 6px;
}

.g-line > .form-select { min-width: 0; }

.g-stack {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* ── The one option in a list that does something else ───────────────────────
 *
 * "＋ Add a new parent…" used to be the last entry in a list of two or three
 * hundred, which is a thing you have to already know is there before you can
 * scroll to it. It is second now, right under the placeholder, with a ruled
 * line beneath it.
 *
 * The colour is a bonus rather than the mechanism: `class` on an `<option>` is
 * honoured by Chrome and Firefox on Windows and Linux and ignored where the
 * platform draws its own menus. What carries the meaning on every platform is
 * the position, the ＋, and the rule — a disabled option, which every browser
 * renders in its own muted colour.
 */
.opt-action {
  color: var(--link-color, #60a5fa);
  font-weight: 600;
}

.opt-rule { color: var(--text-muted); }

/* An invisible stand-in for the "say who" box in the relationship cell. It is
   a real `.form-input`, so it is exactly as tall as the thing it mirrors
   without anybody writing a number down — `visibility: hidden` keeps the
   space, which is the whole job. */
.g-mirror {
  visibility: hidden;
  pointer-events: none;
}

/* One control, in one place, changing what it offers. Two that swapped with
   each other is what made the cell change shape on every click. */
.g-parent2-toggle {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--input-border);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--link-color, #60a5fa);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  transition: var(--transition-fast);
}

.g-parent2-toggle:hover {
  border-color: var(--input-border-hover);
  background: var(--bg-surface-subtle);
}

/* Taking the second parent away is the destructive half, and says so without
   shouting — it is one click from being put back. */
.g-parent2-toggle[data-state="remove"] { color: var(--color-danger); }

.g-parent2-toggle:focus-visible {
  outline: none;
  border-color: var(--color-secondary);
  box-shadow: 0 0 0 3px rgba(var(--color-secondary-rgb), 0.25);
}

/* ── A count on a tab ────────────────────────────────────────────────────────
 *
 * "Pending 7" answers the question the Pending tab exists for without it
 * having to be opened. Rendered as a pill rather than bare digits so a number
 * beside a word does not read as part of the word.
 *
 * Only Pending gets the loud treatment, and only when it is not zero: there,
 * the number is a request for somebody's attention. Everywhere else it is a
 * fact about the school, and a row of five urgent badges is a row of none.
 */
.tab-count {
  /* Hidden until a count has actually arrived, so a tab never shows an empty
     pill while the request is in flight. Revealed by a class, never by
     clearing an inline style: `el.style.display = ''` *removes* the inline
     declaration and hands the element straight back to this rule, which is
     how these badges shipped invisible and passed their own test. */
  display: none;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: 10px;
  background: var(--bg-surface-subtle, rgba(255, 255, 255, 0.08));
  border: 1px solid var(--card-border, rgba(255, 255, 255, 0.12));
  color: var(--text-primary, inherit);
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.5;
  vertical-align: 1px;
}

.tab-count.is-shown { display: inline-block; }

.tab-count-urgent {
  background: var(--color-warning-bg, rgba(245, 158, 11, 0.15));
  border-color: var(--color-warning, #f59e0b);
  color: var(--color-warning, #f59e0b);
}

/* ── The tick column on the Pending tab ──────────────────────────────────────
 *
 * Present in the markup on every row and revealed only where a bulk decision
 * is actually available, so there is one table rather than two versions of
 * one. See syncStudentSelectColumn().
 */
.table .select-col { display: none; }
.table.show-select .select-col { display: table-cell; }

.table .select-col input[type="checkbox"] {
  width: 16px;
  height: 16px;
  cursor: pointer;
  /* Browsers draw an unchecked box in the page's foreground colour, which on
     this theme is nearly the surface it sits on. The reported contrast
     problem, in the one control whose entire job is to show a state. */
  accent-color: var(--color-primary, #3b82f6);
}

/* ── One strip per thing waiting ─────────────────────────────────────────────
 *
 * The Overview used to write a single sentence about the most consequential
 * thing, which meant the second and third were invisible until the first was
 * dealt with — and a school setting itself up has four at once.
 *
 * Each can be put aside with its ✕. That hides the strip and nothing else:
 * the dot on the sidebar stays until the matter is actually dealt with, so
 * putting aside is "not now" rather than "done".
 */
.attention-strip {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 12px 14px;
  margin-bottom: 8px;
  border-radius: var(--radius-sm);
  background: var(--bg-surface-subtle);
  border-left: 3px solid var(--color-warning, #f59e0b);
  font-size: 15px;
  line-height: 1.45;
}

.attention-strip:last-child { margin-bottom: 0; }

/* Nothing waiting, or everything put aside. No bar, no urgency — it is a
   statement rather than a request. */
.attention-strip-quiet {
  border-left-color: var(--border-color, rgba(255, 255, 255, 0.12));
  color: var(--text-muted);
  font-size: 14px;
}

.attention-strip-text { flex: 1 1 320px; min-width: 0; }

.attention-strip-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

/* Deliberately quiet next to the action beside it. Putting something aside
   should never be the easier of the two things to do. */
.attention-dismiss {
  border: 0;
  background: none;
  color: var(--text-muted);
  font-size: 15px;
  line-height: 1;
  padding: 6px 8px;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.attention-dismiss:hover {
  background: var(--bg-surface-subtle);
  color: var(--text-primary, #fff);
}

/* ── The dot on a sidebar section ────────────────────────────────────────────
 *
 * Set from everything waiting, including what has been put aside on the
 * Overview — a dot that disappeared with its strip would make "not now"
 * indistinguishable from "done", which is the one thing this must not do.
 */
.nav-attention {
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-left: 7px;
  border-radius: 50%;
  background: var(--color-warning, #f59e0b);
  /* A ring, so it reads as a marker rather than as part of the label on a
     busy background. */
  box-shadow: 0 0 0 2px var(--bg-sidebar, rgba(0, 0, 0, 0.3));
  vertical-align: middle;
  flex-shrink: 0;
}

.nav-attention[hidden] { display: none; }

/* ── Settings → Graduation ───────────────────────────────────────────────── */
.graduation-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 14px;
}

.graduation-row:hover { background: var(--bg-surface-subtle); }

.graduation-row input[type="checkbox"] {
  width: 16px;
  height: 16px;
  cursor: pointer;
  accent-color: var(--color-primary, #3b82f6);
  flex-shrink: 0;
}

.graduation-row-name { flex: 1; }

.graduation-row-count {
  color: var(--text-muted);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

/* ── Asking the user something ───────────────────────────────────────────────
 *
 * Replaces the browser's own confirm/prompt/alert (createDialogs() in
 * @school-suite/core). A native dialog titles itself with the *site* — a
 * question about rejecting four pupils arrived as "goschool.techeous.com
 * says", which reads like a warning from somewhere else — cannot show which
 * button is the dangerous one, and freezes the tab including whatever
 * progress the user was watching.
 *
 * Above .modal-backdrop (z-index 1000) on purpose: most of these are asked
 * from inside an open dialog.
 */
.ask-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1400;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(2, 6, 23, 0.72);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}

.ask {
  width: 100%;
  max-width: 440px;
  padding: 24px;
  border-radius: var(--radius-md, 12px);
  background: var(--bg-surface, #1e293b);
  border: 1px solid var(--card-border, rgba(255, 255, 255, 0.1));
  /* Lifted well clear of whatever it was opened over, so it reads as the
     thing being answered rather than as part of the page behind it. */
  box-shadow: 0 24px 64px rgba(2, 6, 23, 0.65);
}

/* A destructive question is marked before it is read, not after. */
.ask-danger { border-top: 3px solid var(--color-danger, #ef4444); }

.ask-title {
  margin: 0 0 10px;
  font-size: 17px;
  font-weight: 650;
  line-height: 1.35;
  color: var(--text-primary, #f8fafc);
}

.ask-message {
  margin: 0 0 10px;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--text-secondary, #94a3b8);
}

/* The consequence — what somebody needs *after* deciding they want the thing.
   Set apart rather than run on, and never the first thing read. */
.ask-detail {
  margin: 0 0 4px;
  padding: 10px 12px;
  border-radius: var(--radius-sm, 8px);
  background: var(--bg-surface-subtle, rgba(255, 255, 255, 0.05));
  border-left: 3px solid var(--color-warning, #f59e0b);
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text-secondary, #94a3b8);
  /* A detail may be a list — every bill a payment is being recorded against,
     one per line. `pre-line` honours those breaks while still collapsing
     ordinary runs of spaces, so a one-line detail is unaffected. */
  white-space: pre-line;
}

.ask-field-label {
  display: block;
  margin: 14px 0 6px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-primary, #f8fafc);
}

.ask-field {
  width: 100%;
  padding: 11px 13px;
  border-radius: var(--radius-sm, 8px);
  border: 1px solid var(--input-border, rgba(255, 255, 255, 0.18));
  background: var(--input-bg, rgba(15, 23, 42, 0.6));
  color: var(--text-primary, #f8fafc);
  font-family: inherit;
  font-size: 14.5px;
}

.ask-field:focus {
  outline: none;
  border-color: var(--color-primary, #3b82f6);
  box-shadow: 0 0 0 3px rgba(var(--color-secondary-rgb, 59, 130, 246), 0.28);
}

/* Said on the field. A dialog that answers a dialog is where people start
   clicking blindly. */
.ask-error {
  margin: 8px 0 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--color-danger, #ef4444);
}

.ask-error[hidden] { display: none; }

.ask-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 20px;
  flex-wrap: wrap;
}

.ask-actions .btn { min-width: 104px; justify-content: center; }

/* ── Pointing at a field ─────────────────────────────────────────────────────
 * Part of the same report: a box you cannot see is one you find by waving the
 * pointer around. The border lifts to 52% white under the pointer, which is
 * well clear of the 3:1 the resting state already meets.
 */
.form-input:hover, .form-select:hover, .form-textarea:hover,
.ask-field:hover {
  border-color: var(--input-border-hover);
}

/* A placeholder is a hint, not content — the muted step, which now passes
 * 4.5:1 rather than the 3.3:1 it did when it was #64748b. Spelled out because
 * browsers do not inherit a colour into it. */
.form-input::placeholder,
.form-textarea::placeholder,
.ask-field::placeholder {
  color: var(--text-muted);
  opacity: 1;   /* Firefox dims placeholders by default, on top of the colour */
}

/* ── When a list will not load ───────────────────────────────────────────────
 *
 * Every view could already say a load had failed. None offered to try again,
 * so the only way out of a dropped request was reloading the whole page —
 * which on a school's connection means refetching everything and losing
 * whatever was half typed into a form on the way.
 *
 * Centred in the space the rows would have filled, so it reads as "the list
 * is not here" rather than as one odd row among real ones.
 */
.load-failure {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 26px 16px;
  text-align: center;
}

.load-failure-text {
  margin: 0;
  max-width: 48ch;
  font-size: 14.5px;
  line-height: 1.55;
  color: var(--text-primary);
}

/* Why trying again is not on offer. Quieter than the sentence above it,
   because it is the explanation rather than the news. */
.load-failure-why {
  margin: 0;
  max-width: 48ch;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-muted);
}

.load-failure-action { min-width: 120px; justify-content: center; }

/* ── Settings → Subjects ─────────────────────────────────────────────────────
 *
 * One block per subject, with the classes that take it ticked underneath. The
 * classes are ticked on the subject rather than the subjects on a class
 * because the question a school asks out loud is "who takes Further Maths",
 * and a new subject is added far more often than a new class.
 */
.subject-row {
  padding: 12px 14px;
  margin-bottom: 8px;
  border-radius: var(--radius-sm);
  background: var(--bg-surface-subtle);
  border: 1px solid var(--card-border);
}

.subject-row-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 10px;
  color: var(--text-primary);
}

/* Still legible, still editable — a subject the school has stopped offering is
   not gone, and the scores filed against it are still real. */
.subject-row.is-retired { opacity: 0.72; }

.subject-code {
  margin-left: 8px;
  padding: 1px 6px;
  border-radius: 4px;
  background: var(--bg-surface);
  border: 1px solid var(--card-border);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.subject-classes {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
}

.subject-class {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--text-secondary);
  cursor: pointer;
}

.subject-class input { cursor: pointer; }

/* ── Settings → How a term is marked ─────────────────────────────────────────
 *
 * A row per component: what it is called, what it is out of, and — for a test,
 * never for the exam — a way to drop it. The exam has no remove control
 * because there is always exactly one.
 */
.marks-row {
  display: grid;
  grid-template-columns: 1fr auto 26px;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}

.marks-out-of {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-muted);
  white-space: nowrap;
}

.marks-out-of .marks-max { width: 86px; }

/* ── Settings → Marking → What a mark is called ──────────────────────────────
 *
 * A row per grade: what it is called, the mark it starts at, and what the
 * result says about it. A grade runs up to just below the one above it, so
 * only the floor is asked for — storing both ends invites the two errors that
 * are hardest to see on a screen, a gap and an overlap.
 */
.grade-row {
  display: grid;
  grid-template-columns: 90px auto 1fr 26px;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}

.grade-from {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--text-muted);
  white-space: nowrap;
}

.grade-from .grade-min { width: 74px; }

/* The scales a school can start from. Offered as cards rather than a
   dropdown: a school recognises its own arrangement by reading it, and a
   dropdown shows one at a time. */
.grade-suggestions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.grade-suggestion {
  flex: 1 1 180px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 10px 12px;
  text-align: left;
  border-radius: var(--radius-sm);
  border: 1px solid var(--input-border);
  background: var(--bg-surface-subtle);
  color: var(--text-primary);
  font-family: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: var(--transition-fast);
}

.grade-suggestion span { color: var(--text-muted); font-size: 12px; line-height: 1.4; }

.grade-suggestion:hover {
  border-color: var(--input-border-hover);
  background: var(--bg-surface);
}

/* ── Large type for weighty decisions (owner, 2026-10-28) ───────────────────
   Confirming or turning away pupils, and similar owner decisions, are made by
   somebody who may be reading with glasses. `ask-large` (createDialogs'
   `large`) makes the question bigger and its words full-contrast; `page-wait`
   (beginPageWait) covers the whole page in large type while it is carried out. */
.ask.ask-large { max-width: 560px; padding: 30px; }
.ask-large .ask-title { font-size: 23px; }
.ask-large .ask-message { font-size: 18px; line-height: 1.55; color: var(--text-primary, #f8fafc); }
.ask-large .ask-detail { font-size: 17px; color: var(--text-primary, #f8fafc); white-space: pre-line; }
.ask-large .ask-actions .btn { font-size: 17px; padding: 13px 22px; }

.page-wait {
  position: fixed;
  inset: 0;
  z-index: 3000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(2, 6, 23, 0.82);
  backdrop-filter: blur(3px);
}
.page-wait-box {
  max-width: 520px;
  width: 100%;
  padding: 34px 30px;
  border-radius: var(--radius-md, 12px);
  background: var(--bg-surface, #1e293b);
  border: 2px solid var(--color-primary, #3b82f6);
  text-align: center;
  box-shadow: 0 24px 64px rgba(2, 6, 23, 0.65);
}
.page-wait-spinner {
  width: 48px;
  height: 48px;
  margin: 0 auto 18px;
  border: 5px solid rgba(255, 255, 255, 0.25);
  border-top-color: var(--color-primary, #3b82f6);
  border-radius: 50%;
  animation: page-wait-spin 0.9s linear infinite;
}
.page-wait-message { margin: 0 0 8px; font-size: 24px; font-weight: 650; line-height: 1.35; color: var(--text-primary, #f8fafc); }
.page-wait-note { margin: 0; font-size: 17px; color: var(--text-primary, #f8fafc); opacity: 0.85; }
@keyframes page-wait-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .page-wait-spinner { animation-duration: 2.4s; } }

/* ── Help: the button, and the guides it opens (owner, 2026-10-28) ──────────
   createHelp() in core. Large, plain and high-contrast on purpose: these are
   read by newcomers, some of them reading with glasses. */
.help-button {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 16px 7px 8px;
  border-radius: 999px;
  border: 2px solid var(--color-primary, #3b82f6);
  background: rgba(59, 130, 246, 0.16);
  color: var(--text-primary, #f8fafc);
  font-family: inherit;
  font-size: 16px;
  font-weight: 650;
  cursor: pointer;
}
.help-button:hover { background: rgba(59, 130, 246, 0.3); }
.help-button:focus-visible { outline: 3px solid var(--color-primary, #3b82f6); outline-offset: 2px; }
.help-button[hidden] { display: none; }
.help-button-icon {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--color-primary, #3b82f6);
  color: #fff;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  font-weight: 800;
}

.help-layer { position: fixed; inset: 0; z-index: 2500; }
.help-layer-dim { background: rgba(2, 6, 23, 0.74); }
.help-spotlight {
  position: fixed;
  border-radius: 12px;
  box-shadow: 0 0 0 4px var(--color-primary, #3b82f6), 0 0 0 9999px rgba(2, 6, 23, 0.74);
  pointer-events: none;
  transition: top 0.2s, left 0.2s, width 0.2s, height 0.2s;
}
.help-card {
  position: fixed;
  width: min(460px, calc(100vw - 24px));
  padding: 22px 24px;
  border-radius: 14px;
  background: var(--bg-surface, #1e293b);
  border: 2px solid var(--color-primary, #3b82f6);
  box-shadow: 0 24px 64px rgba(2, 6, 23, 0.7);
  color: var(--text-primary, #f8fafc);
}
.help-card-centred { top: 50% !important; left: 50% !important; transform: translate(-50%, -50%); }
.help-card-count { margin: 0 0 6px; font-size: 15px; color: var(--text-primary, #f8fafc); opacity: 0.85; }
.help-card-title { margin: 0 0 10px; font-size: 22px; line-height: 1.3; color: var(--text-primary, #f8fafc); }
.help-card-text { margin: 0 0 18px; font-size: 18px; line-height: 1.55; color: var(--text-primary, #f8fafc); }
.help-card-actions { display: flex; justify-content: space-between; gap: 12px; }
.help-card-actions .btn { font-size: 17px; padding: 12px 22px; }
.help-card-foot { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-top: 14px; }
.help-card-link {
  padding: 4px 0;
  border: none;
  background: none;
  color: var(--text-primary, #f8fafc);
  font-family: inherit;
  font-size: 15px;
  text-decoration: underline;
  cursor: pointer;
}
@media (prefers-reduced-motion: reduce) { .help-spotlight { transition: none; } }

/* ── The pupils list: columns by view, counts, attendance (owner, 2026-10-28) ──
   setStudentColumns() hides what the view makes redundant; the counts say the
   whole selection, not the page; the term days are always in view. */
.students-cols.hide-class .col-class,
.students-cols.hide-arm .col-arm,
.students-cols.hide-att .col-att,
.students-cols.hide-status .col-status,
.students-cols.hide-created .col-created { display: none; }
.students-cols .col-att { white-space: nowrap; }
.joined-late { display: block; font-size: 12px; color: var(--color-warning, #f59e0b); }
.roster-count { margin: 4px 0 12px; font-size: 17px; color: var(--text-primary, #f8fafc); }
.roster-count:empty { display: none; }
.roster-count-note { font-size: 14px; color: var(--text-secondary, #94a3b8); font-weight: 400; }
.term-days-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 10px;
  margin: 0 0 16px;
  padding: 12px 16px;
  border-radius: var(--radius-sm, 8px);
  background: var(--bg-surface, #1e293b);
  border-left: 4px solid var(--color-primary, #3b82f6);
  font-size: 16px;
  color: var(--text-primary, #f8fafc);
}
.term-days-strip:empty { display: none; }
.term-days-label { font-weight: 650; }
.term-days-number { font-size: 22px; }
