/* GW Operational Medicine Training Center — Clinicals Admin Console
 *
 * THIS FILE NO LONGER DEFINES A PALETTE. It maps the console's long-standing
 * variable names onto the GW OMTC design system tokens in css/omtc-tokens.css,
 * which must be linked BEFORE this file on every page. Change a colour, a
 * radius or a typeface in the design system and re-pull the tokens; do not
 * reintroduce hex literals below.
 *
 * The webfont @import that used to sit here is gone: each page now <link>s
 * Source Sans 3 + IBM Plex Mono directly, which can be preconnected — an
 * @import inside a stylesheet blocks and cannot be.
 *
 * Same system as the student portal (public/css/style.css), and both now read
 * the same tokens, so the two apps cannot drift apart again the way they had:
 * they were running two different navies, two different blues and two
 * different radius scales that were only ever meant to be one. */

/* The cross-app switcher, vendored byte-identical into the testing sim as well
 * — see the header of the file itself. @import rather than a <link> in each of
 * the fifteen pages, for the same reason nav.js exists: fifteen copies of one
 * line is fifteen chances for a page to miss it. It blocks, but it is 2KB from
 * the same origin, and the alternative was fifteen edits. */
@import url('appswitcher.css');

:root {
  /* The system uses ONE family — there is no separate display face.
     --font-display survives only as an alias for the rules that reference it.
     --font-mono comes from the tokens (IBM Plex Mono). */
  --font-main: var(--font-core);
  --font-display: var(--font-core);

  --ink: var(--text-body);
  --ink-heading: var(--text-heading);
  --ink-soft: var(--text-muted);

  /* ── GROUND AND FIGURE ────────────────────────────────────────────────────
     This console used to be flat white on near-white: --surface-page is
     #fafbfc and --surface-card is #ffffff, a 2% step, so nothing looked like a
     surface — every panel, table, tile and toolbar had to earn its edge with a
     shadow, and six of them on one screen read as everything floating.
     The page ground now steps down properly, tinted toward the seal navy so it
     reads as a considered neutral rather than grey, and cards go back to being
     raised by CONTRAST instead of by shadow. Shadows are gone almost
     everywhere below; borders do the separating, which is what they are for.

     Mixed off the tokens rather than picked: no new hex enters the console. */
  --bg: color-mix(in srgb, var(--neutral-50) 84%, var(--navy-50));
  --panel: var(--surface-card);
  /* A hairline has to survive on a tinted ground. --border-subtle disappeared
     against it; --border-default is the line that actually draws. */
  --line: var(--border-default);
  --line-soft: var(--border-subtle);
  /* Row hover, selected tiles, attached toolbars: one tint, one recipe. */
  --wash: color-mix(in srgb, var(--action-primary) 5%, var(--panel));
  --wash-strong: color-mix(in srgb, var(--action-primary) 9%, var(--panel));

  --navy: var(--navy-700);
  --navy-deep: var(--navy-800);

  /* Blue is the action colour; navy is an institutional surface. */
  --blue: var(--action-primary);
  --blue-dark: var(--action-primary-hover);
  --blue-light: var(--blue-50);
  /* Mixed down from the action colour by the SAME formula as the three status
     borders below. It was five separate hex literals (#9cc6de on card and
     calendar-cell hover, #bfe0f0 on the blue badge and the info banner) —
     three slightly different blues doing one job, under a header telling the
     next person not to add any. */
  --blue-border: color-mix(in srgb, var(--action-primary) 32%, var(--neutral-0));
  /* Uppercase field labels and table headers. Deliberately darker than
     --ink-soft: at 0.82rem, uppercase, letter-spaced, muted body grey drops
     under contrast. */
  --ink-label: var(--neutral-600);
  /* The text a ghost/secondary button turns on hover. An alias rather than the
     raw token because dark mode needs a different answer and the rule below
     should not have to know that — see css/theme.css. */
  --ghost-hover-ink: var(--action-primary-hover);

  /* Accent only — never a control, never a status. */
  --blossom: var(--accent-blossom);
  --blossom-deep: var(--accent-blossom-strong);
  --blossom-light: var(--surface-accent-tint);

  /* Status. Borders are mixed down from the full-strength colour so a badge
     doesn't outline itself in a shout. */
  --green: var(--status-pass);
  --green-bg: var(--status-pass-soft);
  --green-border: color-mix(in srgb, var(--status-pass) 32%, var(--neutral-0));

  --red: var(--status-critical);
  --red-bg: var(--status-critical-soft);
  --red-border: color-mix(in srgb, var(--status-critical) 32%, var(--neutral-0));

  --amber: var(--status-caution);
  --amber-bg: var(--status-caution-soft);
  --amber-border: color-mix(in srgb, var(--status-caution) 32%, var(--neutral-0));

  --gray-bg: var(--surface-sunken);

  /* --radius-lg (16px), --shadow-sm and --shadow-md all come from the tokens
     with the same names and the same intent, so they are not restated here.
     --radius is this console's "control" size. */
  --radius: var(--radius-control);
}

* { box-sizing: border-box; margin: 0; padding: 0; }

/* Guarantees el.hidden = true always actually hides the element, even if some
   class also sets `display` — see public/css/style.css for the bug this
   prevents (a stuck "Loading…" spinner that never visually goes away). */
[hidden] { display: none !important; }

/* Keyboard-visible focus — see public/css/style.css for why :focus-visible
   specifically. This console is dense with tables and inline buttons; a
   coordinator tabbing through a review queue needs to see where they are. */
a:focus-visible,
button:focus-visible,
.btn:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2.5px solid var(--blue);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

body {
  font-family: var(--font-main);
  color: var(--ink);
  background: var(--bg);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
}

/* A single fixed grain plate over the whole console. Flat neutral fills are
   what make an interface read as unfinished vector art; a hint of tooth stops
   the large empty areas of this page from looking like a blank canvas.
   Inline SVG turbulence — no image request, no dependency, ~250 bytes. It is
   pointer-events:none and sits under every dialog and the sidebar. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.5;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.035'/%3E%3C/svg%3E");
}
@media (prefers-reduced-transparency: reduce) { body::before { display: none; } }

a { color: var(--blue-dark); text-decoration: none; font-weight: 500; }
a:hover { text-decoration: underline; }

/* ── The SideNav ──────────────────────────────────────────────────────────
   A fixed 248px navy column, per the GW OMTC portal kit. It replaces a
   horizontal bar that could not hold this console's thirteen destinations —
   see admin/js/nav.js for the three failed attempts that preceded it.

   The element is still `header.topbar` and each page's markup is unchanged:
   brand, then <nav data-nav>, then the account block, which is already the
   right order top-to-bottom for a column. Renaming it across fourteen pages
   would have bought nothing.

   Below --sidebar-bp the column becomes an off-canvas drawer behind the
   toggle nav.js injects. That breakpoint and WIDE in nav.js must agree, or the
   drawer opens over a layout that already reserved space for it. */
:root {
  --sidebar-w: 248px;
  --sidebar-bp: 1000px;
}

header.topbar {
  position: fixed;
  inset: 0 auto 0 0;
  z-index: 100;
  width: var(--sidebar-w);
  display: flex;
  flex-direction: column;
  padding: var(--space-6) var(--space-3) var(--space-4);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Not a flat navy slab. A long, very shallow fall from the brand at the top
     to the account block at the bottom — enough to give the one permanently
     visible element on every screen some depth, far too little to read as a
     "gradient button". The system's no-gradient rule is about chrome pretending
     to be consumer product; this is an institutional surface with a light
     source. */
  background: linear-gradient(180deg,
              var(--surface-brand) 0%,
              color-mix(in srgb, var(--surface-brand) 80%, var(--navy-950)) 100%);
  color: var(--text-inverse);
  border-right: 1px solid var(--border-inverse);
}

/* Everything that is not the sidebar shifts over to make room.
   `header.topbar ~ main` rather than `body > main` for two reasons: it targets
   only the page-level <main> (the clinical-sheet form nests its own), and it
   outranks the `body > main` mobile padding in css/theme.css, which is shared
   with the student portal and loads AFTER this file. Matching that selector
   here would lose on file order and silently drop the sidebar offset. */
header.topbar ~ main { margin-left: var(--sidebar-w); }

/* ── The cross-app switcher, placed ──────────────────────────────────────────
   The strip is the same on all three apps; where it goes is not. Here the
   sidebar is `position: fixed; inset: 0 auto 0 0`, so a strip in normal flow
   at the top of <body> would simply be covered by it. Fixed, full width, and
   above the sidebar's z-index — then the sidebar starts below it and the page
   content clears it.

   ONE HEIGHT, THREE RULES. --gw-switcher-h is what the strip is and what both
   offsets are made of, so they cannot fall out of step the way a hard-coded
   number in three places would. 56px is the portal's own .app-header
   min-height — this header is meant to be the same object, not a lookalike. */
:root { --gw-switcher-h: 56px; }

/* ── THE SIDEBAR'S ACCOUNT BLOCK IS NOW IN THE HEADER ────────────────────────
   Theme toggle, address and sign-out moved into the shared header, where the
   portal keeps them. Left in the DOM rather than deleted from fifteen pages:
   the header's theme button forwards its click to #theme-toggle, so this stays
   the one place the preference is read and written (js/ui.js), and there is no
   second copy of that logic to drift. display:none rather than a visually-
   hidden class on purpose — hidden-but-focusable would put an invisible
   sign-out button in the keyboard order. */

.gw-switcher-mount {
  position: fixed;
  inset: 0 0 auto 0;
  height: var(--gw-switcher-h);
  /* header.topbar is 100 and the phone drawer's toggle is 110, and both have to
     pass UNDER this strip rather than over it. */
  z-index: 120;
}

header.topbar { top: var(--gw-switcher-h); }
header.topbar ~ main { padding-top: var(--gw-switcher-h); }

/* ── AND THE THREE TOKENS THIS CONSOLE DOES NOT FLIP ─────────────────────────
   The switcher's own stylesheet is written against the design system's semantic
   tokens, which is correct everywhere the system's dark scope is live. It is
   not live here: the tokens file scopes dark to [data-omtc-theme="dark"] and
   this console switches on [data-theme="dark"], so css/theme.css re-declares by
   hand the values it needs — and the three below were not among them, because
   until now nothing in this console read them.

   Left alone, the strip in dark mode drew a #e8ecf0 hairline across the top of
   a near-black bar and painted the current tab in #006fab at 3.3:1 on it. The
   same two faults css/theme.css already fixed for links and field labels.

   Overridden ON THE COMPONENT rather than added to theme.css's :root block:
   these are the switcher's values, and a global --text-link change would reach
   the whole console for the benefit of one strip. Same values theme.css chose
   for the same jobs. */
:root[data-theme="dark"] .gw-switcher {
  --border-subtle: var(--line);
  --text-heading: var(--neutral-0);
  --text-link: var(--blue-300);
}

/* ── Brand ─────────────────────────────────────────────────────────────── */
/* The lockup is the column's masthead, so it gets a rule under it rather than
   just empty space — otherwise "GW OMTC" and the first nav link read as two
   items in the same list. */
header.topbar .brand {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 calc(var(--space-3) * -1) var(--space-5);
  padding: 0 var(--space-5) var(--space-5);
  border-bottom: 1px solid var(--border-inverse);
  text-decoration: none;
}

/* The seal's background is opaque white — a plate keeps it legible instead
   of a stray white rectangle sitting on the navy column. */
header.topbar .brand .logo-plate {
  background: var(--neutral-0);
  border-radius: var(--radius-sm);
  padding: 3px 6px;
  line-height: 0;
  flex-shrink: 0;
}

header.topbar .brand .logo-plate img.logo {
  height: 28px;
  width: auto;
  display: block;
}

header.topbar .brand .brand-text {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
  min-width: 0;
}

header.topbar .brand .brand-text .org {
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-heading);
}

/* The one blossom moment in this chrome. The system allows the accent for
   navigational emphasis on institutional surfaces; it is not a status here
   and nothing else in the column competes with it. */
header.topbar .brand .brand-text .sub {
  font-size: var(--fs-micro);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--blossom-300);
}

/* ── The links ─────────────────────────────────────────────────────────── */
header.topbar nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

header.topbar nav .nav-links {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* A cluster heading, not a link. Thirteen evenly spaced links read as one
   undifferentiated wall; three labelled clusters are scannable. */
header.topbar nav .nav-group {
  margin: var(--space-5) 0 var(--space-2);
  padding: var(--space-4) var(--space-3) 0;
  border-top: 1px solid color-mix(in srgb, var(--neutral-0) 8%, transparent);
  font-size: var(--fs-micro);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--navy-300);
}

header.topbar nav .nav-item {
  position: relative;
  display: block;
  padding: 9px var(--space-3) 9px var(--space-4);
  border-radius: var(--radius-sm);
  color: var(--navy-100);
  font-size: var(--fs-body-sm);
  font-weight: var(--fw-semibold);
  text-decoration: none;
  transition: var(--transition-control);
}

header.topbar nav .nav-item:hover {
  background: color-mix(in srgb, var(--neutral-0) 8%, transparent);
  color: var(--neutral-0);
  text-decoration: none;
}

/* The system's sidebar-active treatment: a blue wash at 18% plus the 3px
   accent rule as an inset on the leading edge. */
header.topbar nav .nav-item.active {
  background: color-mix(in srgb, var(--action-primary) 22%, var(--navy-900));
  color: var(--neutral-0);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--blue-300) 26%, transparent);
}

header.topbar nav .nav-item.active::before {
  content: '';
  position: absolute;
  left: 0;
  top: 6px;
  bottom: 6px;
  width: var(--rule-accent-w);
  border-radius: var(--radius-pill);
  background: var(--blue-300);
}

/* The one link that leaves this console. A rule above it rather than a cluster
   heading: it is not a fourth category of work, it is the way out. */
header.topbar nav .nav-elsewhere {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border-inverse);
}

/* ── The account block ─────────────────────────────────────────────────── */
/* `margin-top: auto` pins it to the bottom of the column on a tall window and
   lets it simply follow the links on a short one, rather than overlapping
   them the way a position:absolute footer would. */
header.topbar .who {
  margin-top: auto;
  margin-left: calc(var(--space-3) * -1);
  margin-right: calc(var(--space-3) * -1);
  margin-bottom: calc(var(--space-4) * -1);
  padding: var(--space-4) var(--space-5) var(--space-5);
  background: color-mix(in srgb, var(--navy-950) 45%, transparent);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  font-size: var(--fs-caption);
  color: var(--navy-200);
  border-top: 1px solid var(--border-inverse);
  min-width: 0;
}

/* The email is the one variable-width thing here, and a long one used to push
   the sign-out button around. It truncates instead. */
header.topbar .who #admin-email {
  flex: 1 1 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* "Sign out" broke across two lines once the email beside it got long. A
   two-word button is not a paragraph. */
header.topbar .who button { white-space: nowrap; flex-shrink: 0; }

header.topbar .who #sign-out {
  background: transparent;
  border-color: var(--border-inverse);
  color: var(--navy-100);
}
header.topbar .who #sign-out:hover {
  background: color-mix(in srgb, var(--neutral-0) 10%, transparent);
  border-color: color-mix(in srgb, var(--neutral-0) 30%, transparent);
  color: var(--neutral-0);
}

/* ...AND HIDDEN, because all three of those now live in the shared portal
   header above this sidebar, where the portal keeps them. This rule comes
   AFTER the block above rather than before it: same specificity, so the later
   one wins, and written the other way round it silently lost and the console
   showed two theme toggles and two sign-outs.

   Left in the DOM rather than deleted from fifteen pages — the header's theme
   button forwards its click to #theme-toggle, so js/ui.js stays the one place
   the preference is read and written. display:none rather than a visually-
   hidden class: hidden-but-focusable would leave an invisible sign-out button
   in the keyboard order. */
header.topbar .who { display: none; }

/* ── The drawer toggle ─────────────────────────────────────────────────── */
/* Hidden at desktop width: the column is always open there, so a control to
   open it would be a lie. nav.js injects the button as the sidebar's previous
   SIBLING — see the comment there for why it cannot live inside the drawer. */
.nav-toggle { display: none; }

.nav-toggle-bars,
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  display: block;
  width: 18px;
  height: 2px;
  border-radius: var(--radius-pill);
  background: currentColor;
}
.nav-toggle-bars {
  position: relative;
}
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  content: '';
  position: absolute;
  left: 0;
}
.nav-toggle-bars::before { top: -6px; }
.nav-toggle-bars::after { top: 6px; }

@media (max-width: 999px) {
  /* Off-canvas. `visibility` rather than `display` so the slide is animatable
     and the links stay out of the tab order while closed — a drawer whose
     contents are still focusable is a keyboard trap into an invisible menu. */
  header.topbar {
    transform: translateX(-100%);
    visibility: hidden;
    box-shadow: var(--shadow-lg);
    transition: transform var(--dur-base) var(--ease-standard),
                visibility var(--dur-base) var(--ease-standard);
  }
  header.topbar.nav-open {
    transform: none;
    visibility: visible;
  }

  /* Headroom for the fixed toggle, which would otherwise sit on top of the
     page's <h1>. */
  header.topbar ~ main { margin-left: 0; padding-top: 4.5rem; }

  /* Pinned to the viewport, above both the drawer and its scrim, so it is the
     way in AND the way out. 44px square: this is the only route into the
     navigation on a phone and it is what a thumb has to hit. */
  .nav-toggle {
    position: fixed;
    /* Below the cross-app strip, which is also fixed to the top of the viewport
       — otherwise the two share the same 44px and the toggle is unreachable. */
    top: calc(var(--gw-switcher-h) + var(--space-2));
    left: var(--space-3);
    z-index: 110;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border-radius: var(--radius-control);
    background: var(--surface-brand);
    border: 1px solid var(--border-inverse);
    color: var(--neutral-0);
  }
  .nav-toggle:hover {
    background: var(--action-secondary-hover);
    border-color: color-mix(in srgb, var(--neutral-0) 30%, transparent);
    color: var(--neutral-0);
  }
  /* While the drawer is open the button sits over it, so it drops its own
     surface rather than stacking a navy box on a navy column. */
  body.nav-drawer-open .nav-toggle {
    left: auto;
    right: var(--space-3);
    background: transparent;
    border-color: transparent;
  }

  /* The scrim. Navy at 62%, the system's overlay token, and it is what the
     click-outside handler in nav.js is closing on. */
  body.nav-drawer-open { overflow: hidden; }
  body.nav-drawer-open::after {
    content: '';
    position: fixed;
    inset: 0;
    z-index: 99;
    background: var(--surface-overlay);
  }
}

/* Main Content Area */
/* 1160px was a reading measure applied to a console that is mostly tables. On
   a 1440px monitor it left 140px of empty gutter on each side while the wide
   tables — students, shifts, doc review — scrolled inside their own box. 1320px
   hands those gutters to the table and is still a sane line length for the
   prose and forms on the settings pages, which is why this is one cap for
   every page rather than a per-page class: almost every page here has BOTH a
   table and a form (rules, emails, sites, shifts), so no `:has()` rule keyed
   on the existing markup separates them honestly. Anything wider than this
   starts stretching a single-column form across the whole monitor. */
/* The 1320px cap measures the space LEFT OF the sidebar, not the window: the
   sidebar offset is a fixed margin-left set by the `header.topbar ~ main` rule
   above, and this box then fills what is left of it.
   NO `margin-right: auto` here. In normal flow it bought nothing — a max-width
   block with a fixed margin-left already sits exactly there — but login.html
   has no topbar, and ITS <main> is a flex item in a `justify-content: center`
   body. An auto margin on a flex item eats the free space before
   justify-content ever sees it, so that one declaration pinned the whole
   sign-in card to the left edge of a dark full-screen page. */
main {
  position: relative;
  z-index: 1;
  max-width: 1320px;
  padding: var(--space-10) var(--gutter-page-lg) var(--space-20);
}
@media (max-width: 1200px) { main { padding-left: var(--gutter-page); padding-right: var(--gutter-page); } }

h1 {
  font: var(--type-h2);
  letter-spacing: var(--ls-heading);
  color: var(--ink-heading);
  margin: 0 0 0.35rem;
}

h2 {
  font: var(--type-h3);
  letter-spacing: var(--ls-heading);
  color: var(--ink-heading);
  margin: var(--space-10) 0 var(--space-4);
}

/* A section heading with a rule running out of it to the page edge. Every page
   here is a stack of unlabelled slabs — a table, then a form, then another
   table — and an h3-sized line of navy on its own was not enough to say "a new
   thing starts here". Scoped to `main >` so it cannot touch the h2s that sit
   inside a card head (the Today queue, the rail cards, the list heads on
   shifts). */
main > h2 {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}
main > h2::after {
  content: '';
  flex: 1 1 auto;
  height: 1px;
  background: linear-gradient(90deg, var(--line) 0%, var(--line) 45%, transparent 100%);
}

p.lede {
  color: var(--ink-soft);
  font-size: var(--fs-body);
  margin-top: 0;
  margin-bottom: 1.5rem;
}

/* Panels & Cards */
/* ── ONE CARD, NOT THREE ───────────────────────────────────────────────────
   A panel, a table and the Today queue were three different cards on the same
   screen: 16px radius with a medium shadow, 10px with a small one, and 16px
   with a small one. Nobody chose that — each was written on its own day. They
   are now one treatment, and --shadow-sm is the one they settle on: a console
   is mostly cards, and a medium shadow under every one of them reads as
   everything floating rather than anything being raised. */
.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: var(--card-pad-lg) var(--card-pad-lg);
  margin-bottom: var(--space-6);
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 1rem;
}

.card {
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  position: relative;
  padding: var(--space-6);
  text-decoration: none;
  color: var(--ink);
  overflow: hidden;
  transition: var(--transition-control), transform var(--dur-fast) var(--ease-standard);
}

/* The accent rule the OMTC kit puts on a card's leading edge, drawn only on
   hover so a grid of six cards is not six coloured bars at rest. Scales from
   the left rather than fading, which is what makes it read as arriving. */
.card::after {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: var(--rule-accent-w);
  background: var(--blue);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-base) var(--ease-out);
}
.card:hover::after { transform: scaleX(1); }

/* Lifts by a transform and a border, not by growing a shadow: this console is
   mostly cards, and a hover that swells the shadow reads as the card floating
   off the page rather than as "you are on this one". */
.card:hover {
  transform: translateY(-2px);
  border-color: var(--blue-border);
  text-decoration: none;
}

.card h3 {
  margin: 0 0 0.4rem;
  font-family: var(--font-display);
  font-size: var(--fs-h4);
  font-weight: var(--fw-semibold);
  color: var(--ink-heading);
}

.card p {
  margin: 0;
  color: var(--ink-soft);
  font-size: var(--fs-body-sm);
  line-height: 1.45;
}

/* Data Tables */
.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--panel);
  margin-bottom: var(--space-6);
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-body-sm);
  background: var(--panel);
}

th, td {
  text-align: left;
  padding: var(--space-3) var(--space-5);
  border-bottom: 1px solid var(--line-soft);
  vertical-align: middle;
}

/* Every numeric column in this console is a count, a ratio or a date, and in
   proportional figures a column of them does not line up — "12 / 24" and
   "7 / 24" sit at different widths and the eye cannot scan down. */
td { font-variant-numeric: tabular-nums; }

tr:last-child td {
  border-bottom: none;
}

/* Column heads are the one place besides the eyebrow where this system allows
   full caps. */
/* A header row, not a toolbar. The sunken fill plus a 2px rule under it made
   every table look like it began with a title bar; a hairline and the label
   colour carry the same "this is a header" without the weight. --ink-label
   rather than --text-muted because these are small, uppercase and
   letter-spaced, which costs legibility body grey does not have to pay. */
th {
  position: relative;
  background: transparent;
  color: var(--ink-label);
  font-weight: var(--fw-bold);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  padding-top: var(--space-4);
  padding-bottom: var(--space-3);
  white-space: nowrap;
  border-bottom: 1px solid var(--border-strong);
}

tbody tr {
  transition: background-color var(--dur-fast) var(--ease-standard);
}

tbody tr:hover {
  background-color: var(--wash);
}

/* Form controls & Buttons */
/* Default is the quiet outlined control this console is full of; `.primary` is
   the Star of Life blue. The gradient fills that used to be on `.primary` are
   gone — this system permits no gradient except the navy hero glow and scrims
   over photography, and a gradient button was reading as consumer chrome in a
   console that is otherwise all record. Hover darkens one ramp step and never
   changes opacity; press translates 1px down and does not scale. */
button, .btn {
  font-family: inherit;
  font: var(--type-button);
  letter-spacing: var(--ls-button);
  cursor: pointer;
  min-height: var(--control-h-md);
  border: 1px solid var(--border-default);
  background: var(--panel);
  color: var(--ink);
  border-radius: var(--radius-control);
  padding: 0.55rem 1.1rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  text-decoration: none;
  transition: var(--transition-control), transform var(--dur-instant) var(--ease-standard);
}

button:hover, .btn:hover {
  border-color: var(--border-brand);
  background: var(--action-ghost-hover);
  color: var(--ghost-hover-ink);
}

button:active, .btn:active { transform: translateY(1px); }

button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
button:disabled:hover {
  border-color: var(--border-default);
  background: var(--panel);
  color: var(--ink);
  transform: none;
}

/* Pulled a few degrees toward the seal navy. The raw action blue is a bright,
   consumer-product blue, and the Today queue stacks four of these buttons in a
   vertical line — at full chroma that column was the loudest thing on a page
   whose actual subject is the student names beside it. */
button.primary, .btn.primary {
  background: color-mix(in srgb, var(--action-primary) 86%, var(--navy-700));
  border-color: color-mix(in srgb, var(--action-primary) 86%, var(--navy-700));
  color: var(--text-on-brand);
}

button.primary:hover, .btn.primary:hover {
  background: var(--action-primary-hover);
  border-color: var(--action-primary-hover);
  color: var(--text-on-brand);
}

button.primary:disabled:hover {
  background: var(--action-primary);
  border-color: var(--action-primary);
  color: var(--text-on-brand);
}

button.danger {
  color: var(--red);
  border-color: var(--red-border);
  background: var(--panel);
}

button.danger:hover {
  background: var(--red-bg);
  border-color: var(--red);
  color: var(--red);
}

button.small {
  min-height: var(--control-h-sm);
  padding: 0.35rem 0.75rem;
  font-size: var(--fs-caption);
  border-radius: var(--radius-sm);
}

input, select, textarea {
  font-family: inherit;
  font-size: var(--fs-body-sm);
  padding: 0.55rem 0.85rem;
  border: 1.5px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--panel);
  color: var(--ink);
  width: 100%;
  outline: none;
  transition: border-color var(--dur-fast) var(--ease-standard),
              box-shadow var(--dur-fast) var(--ease-standard);
}

/* A <select> lays out 2px taller than an <input> with the same font, padding
   and border — the browser reserves room for its own indicator, and no
   line-height declaration takes it back. Every two-column form row in the
   console (site name / site type, rule name / severity) therefore had one
   control sitting 2px lower than its neighbour. Trim it from the padding so it
   still scales with the font size.

   The indicator itself is ours now. Every filter row in this console is mostly
   <select>s, and the platform's own arrow — a different shape, weight and grey
   on every OS — was the loudest thing in the toolbar and the one part of it
   that did not follow the theme. Drawn with a background image so the control
   stays a real <select>: the native popup, keyboard behaviour and mobile
   picker are all untouched. */
select {
  padding-top: calc(0.55rem - 1px);
  padding-bottom: calc(0.55rem - 1px);
  padding-right: var(--space-8);
  -webkit-appearance: none;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none' stroke='%236a7683' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
}
:root[data-theme="dark"] select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none' stroke='%238b96a3' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5'/%3E%3C/svg%3E");
}
select:hover { background-color: var(--panel); }
/* A multi-row <select> is a list box, not a dropdown — no arrow to draw. */
select[multiple], select[size]:not([size="1"]) { background-image: none; padding-right: 0.85rem; }

input:focus, select:focus, textarea:focus {
  border-color: var(--blue);
  box-shadow: var(--shadow-focus);
}

textarea { resize: vertical; min-height: 5em; }

label {
  display: block;
  font-size: var(--fs-caption);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-label);
  margin: var(--space-4) 0 var(--space-2);
}

label .hint {
  font-weight: 400;
  color: var(--ink-soft);
  text-transform: none;
}

.field-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 0.85rem 1.25rem;
  margin-bottom: 0.5rem;
}

.form-actions {
  margin-top: 1.25rem;
  display: flex;
  gap: 0.75rem;
  align-items: center;
  flex-wrap: wrap;
}

/* Status Badges */
/* The radius is in em, and that is the whole fix for the pill that leaked.
   `9999px` is clamped by the browser to half the box HEIGHT, so a badge that
   wrapped to two lines got a 24px corner curve while its text kept the same
   11px of side padding — "CLEARED · MANUAL" on students.html had letters
   sitting outside the rounded background at both ends. 1.15em is just over
   half of a single line's height, so one-liners still render as true pills
   (the clamp does the rest) and anything that wraps becomes a rounded
   rectangle that actually contains its own text. Every variant below inherits
   this, including the long ones — "Match found — needs confirmation" wraps to
   three lines in a narrow column. max-width keeps a badge inside its cell
   instead of pushing a table sideways. */
.badge {
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  padding: 3px var(--space-2);
  /* Was a pill, in full caps, at full status saturation. A roster row carries
     one of these per line and they were shouting over the student's name — the
     status is the adjective, the name is the noun. Squared off, sentence case,
     hairline outline. The wording is untouched.
     A fixed small radius also retires the em-radius workaround this rule used
     to need: a badge that wraps to two lines can no longer grow a 24px corner
     curve around 11px of padding, because the corner no longer scales. */
  border-radius: var(--radius-sm);
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  letter-spacing: 0;
  line-height: 1.35;
  background: var(--gray-bg);
  color: var(--ink-soft);
  border: 1px solid var(--line);
}

.badge.green { background: var(--green-bg); color: var(--green); border: 1px solid var(--green-border); }
.badge.red { background: var(--red-bg); color: var(--red); border: 1px solid var(--red-border); }
.badge.amber { background: var(--amber-bg); color: var(--amber); border: 1px solid var(--amber-border); }
.badge.blue { background: var(--blue-light); color: var(--blue-dark); border: 1px solid var(--blue-border); }

/* Banners */
/* A rule on the leading edge carries the status; the fill only tints. Four
   fully-saturated colour blocks stacked on doc-review read as four alarms. */
.banner {
  border-radius: var(--radius);
  padding: var(--space-3) var(--space-4);
  margin: var(--space-4) 0;
  font-size: var(--fs-body-sm);
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: 10px;
  border-left-width: var(--rule-accent-w) !important;
  color: var(--ink);
}
.banner.info { border-left-color: var(--blue); }
.banner.warn { border-left-color: var(--amber); }
.banner.error { border-left-color: var(--red); }
.banner.success { border-left-color: var(--green); }

.banner.info { background: var(--blue-light); color: var(--blue-dark); border: 1px solid var(--blue-border); }
.banner.warn { background: var(--amber-bg); color: var(--amber); border: 1px solid var(--amber-border); }
.banner.error { background: var(--red-bg); color: var(--red); border: 1px solid var(--red-border); }
.banner.success { background: var(--green-bg); color: var(--green); border: 1px solid var(--green-border); }

.muted { color: var(--ink-soft); }
.small { font-size: var(--fs-body-sm); }
/* --font-mono (IBM Plex Mono) — the design system treats mono as SEMANTIC, not
 * decorative: a value in mono is asserting "this is machine-generated or
 * protocol-bound". This rule used to hardcode a system stack, which quietly
 * opted every such value out of the typeface the tokens exist to supply and
 * contradicted the note at the top of this file. The stack stays as a fallback
 * for the moment before the webfont lands. */
/* An uppercase caption OVER a value — the student drawer and the student
   detail page both use it. It lived in admin/students.html's own <style>, so
   when the Today drawer reused the markup the label rendered as plain body
   text beside correctly-styled siblings.

   Deliberately NOT a <label>: there is no input, and a <label> with no `for`
   and no control inside it is announced to a screen reader as if a field were
   coming. Same look as this console's bare `label` rule, no form semantics. */
.field-label {
  display: block;
  font-size: var(--fs-caption);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-label);
  margin: 0 0 0.35rem;
}

.mono { font-family: var(--font-mono, ui-monospace), "SF Mono", Menlo, Consolas, monospace; font-size: var(--fs-body-sm); }

/* ── Loading ─────────────────────────────────────────────────────────────
 * Copied from public/css/style.css (the student pages) rather than shared:
 * admin pages load THIS stylesheet and not that one, so index.html's
 * `<div class="spinner-text">Loading dashboard…</div>` rendered as plain
 * unstyled text with no spinner at all, and every other admin page settled
 * for a bare `<p class="muted">Loading…</p>`. Colours use this console's
 * variable names (--ink-soft/--line); the geometry is identical on purpose,
 * so the two halves of the app wait the same way. */
.spinner-text {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  color: var(--ink-soft);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-body-sm);
  padding: 44px 0;
}

.spinner-text::before {
  content: '';
  width: 18px;
  height: 18px;
  border: 2.5px solid var(--line);
  border-top-color: var(--blue);
  border-radius: 50%;
  animation: spin 0.75s linear infinite;
}

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

/* Inline code is where the app prints things with no spaces in them — a CSV
   column list, a JSON example, a model id. A single 88-character token is
   ~720px wide and nothing in a .panel contains it, so it pushed past the panel
   and scrolled the PAGE BODY sideways on anything narrower than a laptop. */
code { overflow-wrap: anywhere; }

/* wrap + anywhere because the left side is a student's email and the right is
   a "Confirm match" button. An address has no break opportunity, so without
   these the row's min-content width blew past its cell and scrolled the table
   sideways — putting the confirm button off-screen on the queue whose entire
   job is confirming. */
.candidate-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  overflow-wrap: anywhere;
  gap: 0.75rem;
  padding: 0.45rem 0;
  border-top: 1px dashed var(--line);
}
.candidate-row:first-child { border-top: none; }

.login-wrap {
  max-width: 400px;
  margin: 10vh auto;
  padding: 2.25rem 2rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

.login-wrap h1 { text-align: center; margin-bottom: 0.5rem; }
.login-wrap .brand-lockup {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 1.5rem;
}
.login-wrap .brand-lockup img.logo { height: 64px; width: auto; }

.section-actions {
  display: flex;
  gap: 0.75rem;
  align-items: center;
  flex-wrap: wrap;
  margin: 0.75rem 0 1.25rem;
}

pre.json-out {
  background: var(--navy-deep);
  color: var(--blue-50);
  border-radius: var(--radius);
  padding: 1rem;
  overflow-x: auto;
  font-size: var(--fs-body-sm);
  margin-top: 0.5rem;
}

/* ── THE TOOLBAR ──────────────────────────────────────────────────────────
   This was a white bordered card floating above another white bordered card,
   with a gap between them — two objects where there is one. It is a sunken
   strip now, and where it is followed directly by the table it filters the two
   are welded into a single component: shared corners, one continuous edge, no
   gap. `:has()` keeps that from firing where the bar stands alone (bind-queue
   filters a list of cards, not a table). */
.filter-bar {
  display: flex;
  gap: var(--space-2) var(--space-4);
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: var(--space-6);
  background: var(--gray-bg);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
}
.filter-bar:has(+ .table-wrap) {
  margin-bottom: 0;
  border-bottom: none;
  border-radius: var(--radius-card) var(--radius-card) 0 0;
}
.filter-bar:has(+ .table-wrap) + .table-wrap {
  border-radius: 0 0 var(--radius-card) var(--radius-card);
}

/* Controls inside a toolbar are chrome: shorter than a form field, quieter at
   rest, and they step forward when they hold a value the user set. */
.filter-bar select,
.filter-bar input {
  min-height: var(--control-h-sm);
  padding-top: 5px;
  padding-bottom: 5px;
  font-size: var(--fs-body-sm);
  border-width: 1px;
  border-color: var(--line);
}
.filter-bar select:hover,
.filter-bar input:hover { border-color: var(--border-strong); }

/* Same exception `.sched-controls select` already makes, and for the same
   reason: the global `input, select, textarea { width: 100% }` is right for the
   stacked forms on the settings pages, but inside a wrapping flex row it makes
   every control claim a whole line. Without this the filter bar on students,
   shifts, requests, doc-review and rules rendered as a column of full-width
   selects — five rows of chrome above the table it filters. */
.filter-bar select,
.filter-bar input:not([type="checkbox"]) { width: auto; min-width: 10rem; }
/* An <input> sized by content falls back to size=20 — about 14rem, which cuts
   these placeholders off mid-word. The pages that care already cap them at
   18rem inline; this makes that the floor as well as the ceiling. */
.filter-bar input[type="search"] { min-width: 18rem; }

.filter-bar .date-range { display: inline-flex; align-items: center; gap: 0.5rem; }

.filter-bar label {
  margin: 0;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  color: var(--ink-soft);
  text-transform: none;
  letter-spacing: 0;
}
.filter-bar input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--blue); }

.sim-result {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.25rem;
  margin-top: 1rem;
}
@media (max-width: 680px) { .sim-result { grid-template-columns: 1fr; } }
.sim-result .col {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.15rem;
}
.sim-result h3 { margin: 0 0 0.6rem; font-size: var(--fs-body); font-weight: var(--fw-bold); }
.sim-result ul { margin: 0.5rem 0 0; padding-left: 1.25rem; }

/* List/Calendar view toggle — same pattern as the student portal */
.view-toggle {
  display: inline-flex;
  gap: 4px;
  padding: 3px;
  background: var(--gray-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  margin-bottom: var(--space-4);
}

.view-btn {
  background: none;
  border: none;
  box-shadow: none;
  color: var(--ink-soft);
  padding: 0.45rem 1.1rem;
}

.view-btn:hover { background: color-mix(in srgb, var(--panel) 70%, transparent); }

.view-btn.active {
  background: var(--panel);
  color: var(--ink-heading);
  box-shadow: var(--shadow-xs);
}

.cal-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.75rem;
}

.cal-nav strong {
  font-family: var(--font-display);
  font-size: var(--fs-body-lg);
  color: var(--ink-heading);
}

.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 6px;
  margin-bottom: 1.5rem;
}

.cal-weekday {
  text-align: center;
  font-size: var(--fs-caption);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-soft);
  padding-bottom: 4px;
}

.cal-cell {
  aspect-ratio: 1 / 0.8;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--panel);
  padding: 6px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  font-family: inherit;
  box-shadow: none;
}

.cal-cell.empty { background: transparent; border: none; }

.cal-cell.has-shifts {
  cursor: pointer;
  border-color: var(--blue-border);
}

.cal-cell.has-shifts:hover {
  border-color: var(--blue);
  background: var(--wash);
}

.cal-cell.today {
  border-color: var(--blossom);
  box-shadow: inset 0 0 0 1px var(--blossom);
}
.cal-cell.today .cal-daynum {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: flex-start;
  min-width: 22px;
  height: 22px;
  padding: 0 5px;
  margin: -2px 0 0 -2px;
  border-radius: var(--radius-pill);
  background: var(--blossom-deep);
  color: var(--text-inverse);
}

.cal-daynum { font-weight: var(--fw-bold); font-size: var(--fs-body-sm); color: var(--ink); }
.cal-count { font-size: var(--fs-caption); font-weight: var(--fw-semibold); color: var(--blue-dark); line-height: 1.15; }
.cal-count.cal-count-warn { color: var(--amber); }

/* ══════════════════════════════════════════════════════════════════════
   Schedule (schedule.html) — site colour palette + month/week/site/student
   grid views.
   ══════════════════════════════════════════════════════════════════════ */

/* ── Site colour palette ──────────────────────────────────────────────
 * THE ONE DELIBERATE EXCEPTION to this file's no-hex-literals rule, and it is
 * worth naming as one so it does not read as the thing the header forbids. The
 * OMTC tokens ship six data-visualisation colours (--viz-1..6); a schedule can
 * hold twice that many sites at once, and a categorical scale that repeats
 * itself is worse than no scale — two hospitals in the same colour on the same
 * week grid is a misread, not a cosmetic issue. These are validated below
 * rather than borrowed, per theme, and they are values the design system does
 * not have an answer for. Anything the tokens DO cover belongs in the mapping
 * block at the top of this file.
 *
 * 12 tokens, hashed deterministically from a site's rid (see
 * js/schedule-layout.js's siteColorIndex) so the same site is always the
 * same colour, on every view and every reload. Chosen against the dataviz
 * skill's validator: both rows PASS the OKLCH lightness band and chroma
 * floor in their own theme (the two checks that matter for "legible in both
 * themes"). Full pairwise colour-blind separation across all 12 at once is
 * not reachable — the skill's own reference 8-colour palette documents the
 * same trade-off past its 4th slot — so a couple of pairs (red/orange,
 * green/olive) sit close for a colour-blind viewer. That is why colour is
 * NEVER the only signal here: every dot, chip and block sits next to the
 * site's name or abbreviation (see .site-chip, .sched-legend below). */
:root {
  --site-0-fill: #2a78d6; --site-1-fill: #eb6834; --site-2-fill: #1baf7a; --site-3-fill: #eda100;
  --site-4-fill: #e87ba4; --site-5-fill: #008300; --site-6-fill: #4a3aa7; --site-7-fill: #e34948;
  --site-8-fill: #7c9a1e; --site-9-fill: #1f8fae; --site-10-fill: #9c3fa0; --site-11-fill: #8a3646;
}
:root[data-theme="dark"] {
  --site-0-fill: #3987e5; --site-1-fill: #d95926; --site-2-fill: #199e70; --site-3-fill: #c98500;
  --site-4-fill: #d55181; --site-5-fill: #008300; --site-6-fill: #9085e9; --site-7-fill: #e66767;
  --site-8-fill: #5f9a2e; --site-9-fill: #1f93b8; --site-10-fill: #b559ba; --site-11-fill: #b9505f;
}
.site-color-0 { --site-fill: var(--site-0-fill); } .site-color-1 { --site-fill: var(--site-1-fill); }
.site-color-2 { --site-fill: var(--site-2-fill); } .site-color-3 { --site-fill: var(--site-3-fill); }
.site-color-4 { --site-fill: var(--site-4-fill); } .site-color-5 { --site-fill: var(--site-5-fill); }
.site-color-6 { --site-fill: var(--site-6-fill); } .site-color-7 { --site-fill: var(--site-7-fill); }
.site-color-8 { --site-fill: var(--site-8-fill); } .site-color-9 { --site-fill: var(--site-9-fill); }
.site-color-10 { --site-fill: var(--site-10-fill); } .site-color-11 { --site-fill: var(--site-11-fill); }

/* A small solid square — the legend's swatch, and the leading mark inside a
   chip. Never alone: every place this appears, site-name text is beside it. */
.site-dot {
  display: inline-block;
  flex-shrink: 0;
  width: 10px;
  height: 10px;
  border-radius: 3px;
  background: var(--site-fill, var(--ink-soft));
}

/* One shift, shown as a labelled chip — the month grid's per-day list and
   the by-site/by-student grid's cells all use this. A light tint of the
   site colour (never the raw hue) sits behind ordinary --ink text, so text
   contrast never depends on which of the 12 hues a given site happens to
   hash to; the hue itself carries identity only via the dot and the left
   border accent. */
.site-chip {
  display: flex;
  align-items: center;
  gap: 5px;
  border-radius: var(--radius-sm);
  border-left: var(--rule-accent-w) solid var(--site-fill, var(--ink-soft));
  background: color-mix(in srgb, var(--site-fill, var(--ink-soft)) 16%, var(--panel));
  color: var(--ink);
  padding: 2px 6px;
  font-size: var(--fs-caption);
  line-height: 1.3;
  overflow: hidden;
  white-space: nowrap;
  max-width: 100%;
}
/* text-overflow does not apply to a flex CONTAINER's overflow, and the label
   lives in a child span — so the ellipsis declared on .site-chip never fired
   and long site names were hard-clipped mid-word inside a month cell, with no
   sign anything had been cut. It has to sit on the text element itself, and
   min-width:0 is what lets a flex item shrink below its content width. */
.site-chip > span {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.site-chip.over-capacity {
  border-left-color: var(--red);
  box-shadow: inset 0 0 0 1px var(--red-border);
}
.site-chip .chip-warn { color: var(--red); font-weight: var(--fw-bold); }
.site-chip.cancelled { text-decoration: line-through; opacity: 0.65; }

/* The always-visible legend — every site actually present in the loaded
   window, never a guess at every site that exists. */
.sched-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 18px;
  padding: var(--space-3) var(--space-4);
  background: var(--gray-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  margin-bottom: var(--space-4);
  font-size: var(--fs-caption);
}
/* Filled by JS once the sites load. Until then — and forever, if no site has a
   shift in view — it drew an empty bordered bar across the page above the
   spinner, which reads as a component that failed to render. */
.sched-legend:empty { display: none; }
.sched-legend-item { display: flex; align-items: center; gap: 6px; color: var(--ink-soft); }

.sched-controls {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 1.1rem;
}
.sched-controls > label { margin: 0; display: flex; align-items: center; gap: 0.5rem; font-weight: 600; text-transform: none; }
.sched-controls select { width: auto; min-width: 150px; }

/* ── Month view ──────────────────────────────────────────────────────
 * Reuses .cal-grid/.cal-cell/.cal-weekday/.cal-daynum from the shifts.html
 * calendar above — same skeleton, this page only adds the per-day chip list
 * and the ratio/over-capacity styling shifts.html didn't need. */
.cal-cell.sched-cell { align-items: stretch; cursor: default; height: auto; aspect-ratio: auto; min-height: 92px; }
.cal-chips { display: flex; flex-direction: column; gap: 2px; width: 100%; margin-top: 2px; }
.cal-chips .site-chip { padding: 1px 4px; }
.cal-more { font-size: var(--fs-caption); color: var(--ink-soft); }

/* ── Week view ───────────────────────────────────────────────────────
 * A 56px hour rail + one column per day, shifts positioned by
 * schedule-layout.js's weekBlockForDay/assignLanes. Horizontal AND vertical
 * scroll live inside .week-grid-wrap, never on the page body. */
.week-grid-wrap {
  overflow: auto;
  max-height: 70vh;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  margin-bottom: 1.25rem;
}
.week-grid {
  display: grid;
  grid-template-columns: 52px repeat(var(--week-days, 7), minmax(130px, 1fr));
  min-width: max-content;
}
.week-head {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--gray-bg);
  border-bottom: 1.5px solid var(--line);
  padding: 8px 6px;
  text-align: center;
  font-size: var(--fs-caption);
  font-weight: var(--fw-bold);
  color: var(--ink-soft);
}
.week-head-corner { position: sticky; left: 0; top: 0; z-index: 3; background: var(--gray-bg); }
.week-hour-rail {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--panel);
  border-right: 1px solid var(--line);
}
.week-hour-label {
  height: 48px;
  box-sizing: border-box;
  font-size: var(--fs-caption);
  color: var(--ink-soft);
  text-align: right;
  padding: 0 6px;
  border-bottom: 1px dashed var(--line);
}
.week-day-col {
  position: relative;
  border-left: 1px solid var(--line);
  height: 1152px; /* 24h * 48px */
  background-image: repeating-linear-gradient(to bottom, transparent 0 47px, var(--line) 47px 48px);
  /* Keeps a many-week grid responsive without a virtualization library — a
     column not near the viewport skips layout/paint until it scrolls close. */
  content-visibility: auto;
  contain-intrinsic-size: 1152px;
}
/* The 3px site rule sits on the TOP edge — the system lists "schedule event
   top" as one of the four places a 3px accent rule belongs, and a left rule
   on a block this narrow ate most of a short shift's label anyway. */
.week-block {
  position: absolute;
  border-radius: var(--radius-sm);
  border-top: var(--rule-accent-w) solid var(--site-fill, var(--ink-soft));
  background: color-mix(in srgb, var(--site-fill, var(--ink-soft)) 22%, var(--panel));
  color: var(--ink);
  font-size: var(--fs-caption);
  line-height: 1.25;
  padding: 2px 5px;
  overflow: hidden;
  box-sizing: border-box;
}
.week-block.continues-before { border-top-left-radius: 0; border-top-right-radius: 0; }
.week-block.continues-after { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.week-block.over-capacity { box-shadow: inset 0 0 0 2px var(--red); }
.week-block:focus-visible { outline: 2.5px solid var(--blue); outline-offset: 1px; }

/* ── By-site / by-student grid ──────────────────────────────────────
 * A day per column, sticky first column so a row's label (site or student
 * name) stays visible while the days scroll horizontally underneath it —
 * the whole reason this is a real <table>, not a flex grid: a native
 * scrolling table is the free, no-JS way to get that sticky-header +
 * sticky-first-column behaviour together. */
.lane-grid-wrap {
  overflow: auto;
  max-height: 70vh;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  margin-bottom: 1.25rem;
}
.lane-grid { border-collapse: separate; border-spacing: 0; font-size: var(--fs-caption); width: 100%; }
.lane-grid th, .lane-grid td {
  border-bottom: 1px solid var(--line);
  padding: 6px 8px;
  vertical-align: top;
}
.lane-grid thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--gray-bg);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  white-space: nowrap;
}
.lane-grid th:first-child, .lane-grid td:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--panel);
  border-right: 1.5px solid var(--line);
  min-width: 170px;
  max-width: 230px;
  white-space: normal;
}
.lane-grid thead th:first-child { z-index: 3; background: var(--gray-bg); }
.lane-grid td {
  min-width: 110px;
  /* Off-screen day cells skip render work at a large window — same trick as
     .week-day-col, without pulling in a virtualization library. */
  content-visibility: auto;
  contain-intrinsic-size: 46px;
}
.lane-cell-chips { display: flex; flex-direction: column; gap: 3px; }
.lane-row-double-booked td:first-child { border-left: 3px solid var(--red); }
.lane-row-highlight { animation: lane-row-flash 1.5s ease-out; }
@keyframes lane-row-flash {
  from { background: var(--blue-light); }
  to { background: transparent; }
}

/* ── States ──────────────────────────────────────────────────────────── */
.sched-empty { text-align: center; padding: 3rem 1rem; color: var(--ink-soft); }
.sched-empty strong { color: var(--ink); }

@media (max-width: 720px) {
  .lane-grid th:first-child, .lane-grid td:first-child { min-width: 130px; }
}

/* Print — coordinators print schedules. Chrome, controls and the page's own
   scroll containers don't belong on paper; the grids themselves already lay
   out fine at native size. */
@media print {
  header.topbar, .sched-controls, #theme-toggle, .filter-bar { display: none !important; }
  /* Landscape, because these grids are days-across. Removing the scroll
     container hands the printer the grid's FULL width — at the old 46-day
     default that was ~6000px, and Chrome does not scale to fit: it clips at
     the page edge and silently drops most of the schedule. The window default
     is now a week, so the printed grid fits; keeping overflow visible is what
     makes the whole week appear rather than one scrollport of it. */
  @page { size: landscape; margin: 12mm; }
  .week-grid-wrap, .lane-grid-wrap { overflow: visible !important; max-height: none !important; }
  body { background: var(--neutral-0); }
}

/* ── KPI tile ────────────────────────────────────────────────────────────────
   The count-and-label block above a review queue. A class rather than the four
   inline declarations it replaces in doc-review.html, and the reason is a bug
   rather than tidiness: that copy read `border: 1px solid var(--border)` with
   no fallback, and --border is defined ONLY under :root[data-theme="dark"]
   (../../css/theme.css). In light mode the custom property is missing, the
   declaration is invalid at computed-value time, and the border is dropped
   entirely — so the tiles rendered edgeless on the default theme. Everywhere
   else in this codebase writes the full chain; one inline copy did not, and an
   inline copy is exactly what nothing can check. */
.kpi-tile {
  background: var(--panel, var(--surface-card, #fff));
  border: 1px solid var(--border, var(--line, #dbe6ec));
  border-radius: var(--radius-control, 10px);
  padding: var(--space-3, 12px) var(--space-4, 16px);
  min-width: 140px;
}
.kpi-tile .kpi-value {
  font-size: var(--fs-h2);
  font-weight: var(--fw-bold, 700);
  color: var(--ink-heading);
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--ls-heading);
}
.kpi-row { display: flex; gap: var(--space-3, 12px); flex-wrap: wrap; margin-bottom: var(--space-4); }

/* Start and End are a date input and a time select side by side, standing in
   for the `datetime-local` that used to be there. They share one grid cell, so
   they read as one control.

   Both carry a real minimum width rather than a bare percentage: the date input
   has to fit "mm/dd/yyyy" plus the browser's own calendar button, and the time
   box has to fit "12:30 PM" without clipping to "1:00 P". Where the cell is too
   narrow for both — a phone, or a third column squeezing this one — they wrap
   onto two lines instead of shrinking past those minimums. */
.time-split {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
}

.time-split > input[type="date"] { flex: 1 1 8.5rem; min-width: 8.5rem; }
.time-split > select,
.time-split > .cbx { flex: 1 1 7rem; min-width: 7rem; }

/* The dropdown is pinned to the box's own width, which is narrow enough here to
   break "11:30 AM" across two lines. Let it grow to its longest label. */
.time-split .cbx-list { min-width: max-content; }
.time-split .cbx-option { white-space: nowrap; }
