/* Foundational shared UI: Buttons, Forms, Cards, Badge, Icons, Modal,
   Menu Search Palette, Toast, Touch-Drag pick-up banner.
   Extracted from the former components.css (CSS split, CLAUDE.md §12) —
   that 6064-line file has since been fully split into frontend/css/views/*.css
   and frontend/css/shared/*.css and removed; this was its largest single
   successor. Used across nearly every view. The status-lifecycle badges and
   .ending-banner are smaller cross-view components (planning-calendar.js,
   dashboard-widgets.js, leave-requests.js) bundled here rather than split
   into their own single-purpose files. */
@layer components {

/* ── Buttons ──────────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: 500;
  border: 1px solid transparent;
  transition: background var(--transition), border-color var(--transition), color var(--transition);
  white-space: nowrap;
}
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }

.btn-primary {
  background: var(--primary);
  color: var(--on-primary);
  border-color: var(--primary);
}
.btn-primary:hover:not(:disabled) { background: var(--primary-hover); border-color: var(--primary-hover); }

.btn-secondary {
  background: var(--surface);
  color: var(--text);
  border-color: var(--border);
}
.btn-secondary:hover:not(:disabled, [aria-disabled="true"]) { background: var(--surface-sunk); }

.btn-danger {
  background: var(--danger);
  color: var(--on-danger);
  border-color: var(--danger);
}
.btn-danger:hover:not(:disabled) { background: var(--danger-hover); border-color: var(--danger-hover); }

.btn-ghost {
  background: none;
  color: var(--muted);
  border-color: transparent;
}
.btn-ghost:hover:not(:disabled) { background: var(--surface-sunk); color: var(--text); }

/* ── Forms ────────────────────────────────────────────────────────────────── */
.form-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-bottom: var(--space-4);
}

label {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text);
}

.input {
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  transition: border-color var(--transition), box-shadow var(--transition);
  width: 100%;
}
.input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--p-100);
}
/* Browser autofill (a remembered username picked from the dropdown) paints
   its own light fill with a UA `!important` background that no theme token
   reaches; an inset shadow covers it and the text fill keeps the token
   colour, so autofilled fields follow the light/dark theme like typed ones. */
.input:autofill {
  box-shadow: inset 0 0 0 100vmax var(--surface);
  -webkit-text-fill-color: var(--text);
  caret-color: var(--text);
}
.input:autofill:focus {
  box-shadow: inset 0 0 0 100vmax var(--surface), 0 0 0 3px var(--p-100);
}
.input:disabled, select:disabled, textarea:disabled {
  opacity: var(--opacity-disabled);
  cursor: not-allowed;
  background: var(--surface-sunk);
}

/* Format caption rendered by components/date-field.js next to any native
   date input — the input itself is width:100% (above), so this inline
   span naturally wraps onto its own line beneath it. */
.date-format-hint {
  display: inline-block;
  margin-top: 2px;
}
.input.error { border-color: var(--danger); }

select.input { appearance: none; background-image: url("../../assets/select-arrow.svg"); background-repeat: no-repeat; background-position: right 10px center; padding-right: 2rem; }

/* .checkbox-label had no base rule at all before this pass — every
   consumer either hand-rolled the same flex/gap/cursor styling inline
   (settings.js) or got none at all (the Deactivate dialog's radios in features/users/deactivation.js),
   and a bare <label> is inline by default: without an explicit width, a
   vertical checklist (settings.js's Notification Preferences, in
   particular) rendered as checkboxes wrapping left-to-right like loose
   words instead of stacking one per line. width:100% fixes that as the
   sensible default; a context that genuinely wants checkboxes sitting
   side-by-side in a horizontal filter bar can override both flex and
   width for its own children the same way. */
.checkbox-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  cursor: pointer;
}

/* Plain checkboxes stay checkboxes only for genuine multi-select and the
   compact filter-bar exception — restyled onto the
   primary ramp instead of left as the unstyled browser default. Has no
   visible effect on toggle-switch inputs, which are visually hidden. */
input[type="checkbox"], input[type="radio"] {
  accent-color: var(--p-600);
}
input[type="checkbox"]:disabled, input[type="radio"]:disabled {
  opacity: var(--opacity-disabled);
  cursor: not-allowed;
}

/* Approved checkbox design C: shared square + selectable form rows.
   Opt-in class leaves switches, weekday pills and radios unchanged. */
.checkbox-input {
  appearance: none;
  display: inline-grid;
  place-content: center;
  flex: 0 0 18px;
  width: 18px;
  height: 18px;
  padding: 0;
  margin: 0;
  vertical-align: middle;
  border: 1.5px solid var(--muted);
  border-radius: 5px;
  background: var(--surface);
  color: var(--on-primary);
  cursor: pointer;
}
.checkbox-input::before {
  content: "";
  width: 9px;
  height: 5px;
  border: solid currentColor;
  border-width: 0 0 2px 2px;
  transform: translateY(-1px) rotate(-45deg);
  visibility: hidden;
}
.checkbox-input:checked,
.checkbox-input:indeterminate {
  background: var(--p-600);
  border-color: var(--p-600);
}
.checkbox-input:checked::before { visibility: visible; }
.checkbox-input:indeterminate::before {
  visibility: visible;
  width: 9px;
  height: 2px;
  border: 0;
  background: currentColor;
  transform: none;
}
.checkbox-input:hover:not(:disabled) {
  border-color: var(--p-600);
  box-shadow: 0 0 0 3px var(--p-50);
}
.checkbox-input:focus-visible {
  outline: 2px solid var(--p-500);
  outline-offset: 3px;
}
.checkbox-input--compact { width: 16px; height: 16px; flex-basis: 16px; border-radius: 4px; }
.checkbox-choices { display: grid; gap: var(--space-2); }
.checkbox-choice {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-width: 0;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  margin: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  font-size: var(--text-sm);
  font-weight: 400;
  line-height: 1.45;
  cursor: pointer;
}
.checkbox-choice > span { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.checkbox-choice > .checkbox-input { order: 1; }
.checkbox-choice:has(:checked) { background: var(--p-50); border-color: var(--p-300); }
.checkbox-choice:hover:not(:has(:disabled)) { border-color: var(--p-400); }
.checkbox-choice:has(:disabled) { opacity: var(--opacity-disabled); cursor: not-allowed; }
/* Fade a disabled row once, including disabled fieldset descendants. */
.checkbox-choice .checkbox-input:disabled { opacity: 1; }
.checkbox-choice--ack { align-items: flex-start; padding: var(--space-3); }
.checkbox-choice--ack > .checkbox-input { order: 0; margin-top: 2px; }
@media (forced-colors: active) {
  .checkbox-input { appearance: auto; }
  .checkbox-input::before { content: none; }
  .checkbox-choice:has(:checked) { border-color: Highlight; }
}

/* ── Cards ────────────────────────────────────────────────────────────────── */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-6);
  box-shadow: var(--shadow-sm);
}

/* ── Dashboard card (components/dashboard-widgets.js) ──────────────────────
   The one frame for every dashboard card: title left, optional link right.
   `--flush` cards hold edge-to-edge rows (action queue, tables); their
   header carries the padding instead. */
.dash-card { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; margin-bottom: var(--space-6); }
.dash-card-header { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-2); }
.dash-card-header h2 { display: flex; align-items: center; gap: var(--space-2); margin: 0; font-size: var(--text-base); font-weight: 600; }
.dash-card-header a { font-size: var(--text-sm); }
.dash-card--flush { gap: 0; padding: 0; }
.dash-card--flush > .dash-card-header { padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); }
/* Notices card: date left, notice right, one row each. */
.dash-notice-list { display: flex; flex-direction: column; margin: 0; font-size: var(--text-sm); }
.dash-notice-list > div { display: flex; gap: var(--space-3); padding: var(--space-2) 0; border-top: 1px solid var(--border); }
.dash-notice-list > div:first-child { border-top: 0; padding-top: 0; }
.dash-notice-list dt { flex: 0 0 4.5rem; color: var(--muted); }
.dash-notice-list dd { flex: 1 1 auto; min-width: 0; margin: 0; font-weight: 600; overflow-wrap: anywhere; }

/* Planning Calendar's "staff ending soon" banner (planning-calendar.js) — border-left tone flips
   between danger/warning depending on whether the departure would drop
   expected staff below the busiest day's requirement (§3.13's
   endingIsSevere). */
.ending-banner { padding: var(--space-4); margin-bottom: var(--space-4); border-left: 3px solid transparent; }
.ending-banner.tone-danger  { border-left-color: var(--danger); }
.ending-banner.tone-warning { border-left-color: var(--warning); }
.ending-banner-heading { font-weight: 600; margin-bottom: var(--space-2); }

/* ── Badge ────────────────────────────────────────────────────────────────── */
.badge {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-2);
  border-radius: var(--r-pill);
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.badge-blue   { background: var(--info-bg);    color: var(--info); }
.badge-amber  { background: var(--warning-bg); color: var(--warning); }
.badge-red    { background: var(--danger-bg);  color: var(--danger); }
.badge-green  { background: var(--success-bg); color: var(--success); }
.badge-gray   { background: var(--surface-sunk); color: var(--muted); }
.badge-comp   { border: 1px dashed var(--comp); background: var(--comp-bg); color: var(--comp); }

/* Period status badges (period-navigation.js periodStatusBadgeHtml): the
   three-hue lifecycle family from color-engine.js lifecycleClass, whose
   slot rules in base.css set --lifecycle/--lifecycle-bg. An unknown
   status (lifecycle-none) stays neutral. */
.badge:is(.lifecycle-open, .lifecycle-concept, .lifecycle-published) { background: var(--lifecycle-bg); color: var(--lifecycle); }
.badge.lifecycle-none { background: var(--surface-sunk); color: var(--muted); }
/* A scheduled period (lifecycleBadgeClass): the Open hue, outlined rather
   than filled — the window exists but has not opened. The --lifecycle
   tokens carry their own dark-mode values. */
.badge.lifecycle-outline { background: transparent; box-shadow: inset 0 0 0 1px var(--lifecycle); }

/* Category badges — identity, not severity (see base.css's --cat-* tokens
   and CHANGELOG.md "Anesthesiologist clarity pass"). Never mix with
   .badge-red/amber/green/blue, which stay reserved for status/outcome. */
.badge-cat-rose  { background: var(--cat-rose-bg);  color: var(--cat-rose); }
.badge-cat-denim { background: var(--cat-denim-bg); color: var(--cat-denim); }
.badge-cat-clay  { background: var(--cat-clay-bg);  color: var(--cat-clay); }
.badge-cat-moss  { background: var(--cat-moss-bg);  color: var(--cat-moss); }
.badge-cat-plum  { background: var(--cat-plum-bg);  color: var(--cat-plum); }
.badge-cat-berry { background: var(--cat-berry-bg); color: var(--cat-berry); }

/* ── Icons (fixed vocabulary, components/icons.js) ───────────────────────── */
.icon {
  display: inline-flex;
  flex-shrink: 0;
  vertical-align: -3px;
}
.icon-muted { color: var(--muted); }

/* ── Modal ────────────────────────────────────────────────────────────────── */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,.45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 500;
  padding: var(--space-4);
}

.modal {
  background: var(--surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  width: 100%;
  max-width: 480px;
  max-height: 90vh;
  /* Scroll the body so actions stay visible and corners clip consistently. */
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* Open/close motion — createModal()/confirmModal() toggle .open after the
     overlay's first paint (double rAF, components/modal.js) so the initial
     state above is what actually transitions from, not a flash of the final
     state. close() defers removal by MODAL_CLOSE_MS to let this play out,
     skipping the delay entirely under prefers-reduced-motion below. */
  transform: translateY(10px) scale(.97);
  opacity: 0;
  transition: transform 200ms cubic-bezier(.2,.8,.2,1), opacity 160ms ease;
}
.modal-overlay {
  opacity: 0;
  transition: opacity 180ms ease;
}
.modal-overlay.open { opacity: 1; }
.modal-overlay.open .modal { transform: translateY(0) scale(1); opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .modal, .modal-overlay { transition: none; }
}

/* Size scale — the default (above) covers most modals: a handful of
   fields, one confirmation, one review panel. The wide variant is opt-in
   via createModal({ size: 'lg' }). */
.modal-lg { max-width: 720px; }

.modal-header {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-6);
  border-bottom: 1px solid var(--border);
}

.modal-header h2 { min-width: 0; overflow-wrap: anywhere; font-size: var(--text-base); font-weight: 600; }

.modal-body {
  padding: var(--space-6);
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.modal-footer {
  flex: none;
  display: flex;
  justify-content: flex-end;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-6);
  border-top: 1px solid var(--border);
}

/* Phone (≤600px, the isMobileViewport() breakpoint): every dialog is a bottom
   sheet, the same phone surface as the Roster Editor inspector and the
   Day-Ahead panel (views/roster-grid.css) — full width, anchored to the
   bottom edge (over the phone bottom bar), rounded top corners, sliding up
   from below. The body scrolls between a fixed header and footer, so the
   actions stay in reach however tall the content (2FA QR, wish wizard, user
   editor); the footer carries the home-indicator safe area. A strip of
   backdrop stays visible above the tallest sheet so it still reads as an
   overlay, and a stacked dialog covers the one below it. dvh tracks the
   browser's dynamic toolbar; on-screen keyboards that only resize the
   visual viewport overlay the sheet, and the browser scrolls the focused
   field into view inside the scrolling body. Footer buttons share the row
   and wrap full-width when two don't fit. */
@media (max-width: 600px) {
  .modal-overlay {
    align-items: flex-end;
    padding: var(--space-6) 0 0;
  }

  .modal,
  .modal-lg {
    max-width: none;
    max-height: calc(100dvh - var(--space-6));
    border-radius: var(--radius) var(--radius) 0 0;
    box-shadow: var(--shadow-overlay);
    transform: translateY(100%);
    opacity: 1;
    transition: transform 220ms cubic-bezier(.2,.8,.2,1);
  }
  .modal-overlay.open .modal { transform: translateY(0); }

  .modal-header {
    padding: var(--space-3) var(--space-4);
  }

  .modal-body {
    padding: var(--space-4);
  }

  .modal-footer {
    flex-wrap: wrap;
    padding: var(--space-3) var(--space-4) calc(var(--space-3) + env(safe-area-inset-bottom));
  }

  .modal-footer .btn {
    flex: 1 1 auto;
    min-width: 0;
  }
}
@media (max-width: 600px) and (prefers-reduced-motion: reduce) {
  .modal { transition: none; }
}

/* ── Dense modal form sections ──────────────────────────────────────────────
   A multi-field modal (e.g. admin-locations.js's location-shift rule editor)
   groups its fields into labeled sections instead of one flat .form-group
   stack, so a 10-field form reads as a shape instead of just a longer list. */
.modal-section {
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--border);
}
.modal-section:first-of-type { padding-top: 0; }
.modal-section:last-of-type { border-bottom: none; padding-bottom: 0; }
.modal-section-title {
  font-size: var(--text-eyebrow);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  margin: 0 0 var(--space-3);
}
.modal-field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
}
@media (max-width: 600px) {
  .modal-field-row { grid-template-columns: 1fr; }
}

/* ── Repeatable row cards ────────────────────────────────────────────────────
   Replaces a bare flex row (e.g. specialty-requirements-editor.js) with a
   bordered card per row, so several repeatable rows read as distinct items
   instead of one undifferentiated stack of inline controls. */
.row-card {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  margin-bottom: var(--space-2);
}
.row-card:last-of-type { margin-bottom: 0; }
.row-card .row-card-main { flex: 1; min-width: 0; }
.row-card .row-card-remove {
  background: none;
  border: none;
  color: var(--muted);
  cursor: pointer;
  padding: 4px;
  border-radius: var(--radius-sm);
  display: inline-flex;
}
.row-card .row-card-remove:hover { background: var(--surface-sunk); color: var(--danger); }

/* specialty-requirements-editor.js's count input: .input's own width:100%
   (shared/base-ui.css) becomes this flex item's flex-basis (flex-basis:auto
   resolves from width) since it carries no flex-grow/shrink override of its
   own — inside row-card-main's flex row that stretched it to claim most of
   the row, starving the specialty <select> next to it down to a sliver. A
   fixed flex-basis keeps it a compact, content-appropriate size regardless
   of how wide the surrounding row is. */
.specialty-req-count { flex: 0 0 4.5rem; width: 4.5rem; }

/* ── Weekday-scope picker (components/weekday-picker.js) ─────────────────────
   Real checkbox + label pair per day, restyled as a circular letter pill —
   the input stays in the DOM and keeps its name/value/checked state exactly
   as before, so every consumer's `querySelectorAll('input[name="..."]:checked')`
   collection code is unchanged. Dutch weekday initials collide (Dinsdag /
   Donderdag, Zaterdag / Zondag) so each pill carries a title tooltip with the
   full day name rather than relying on the single letter alone. */
.weekday-picker { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.weekday-picker-input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
.weekday-pill {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--muted);
  font-size: var(--text-sm);
  font-weight: 700;
  cursor: pointer;
  user-select: none;
  transition: background var(--transition), border-color var(--transition), color var(--transition), transform 120ms ease;
}
.weekday-pill:hover { border-color: var(--p-400); color: var(--text); }
.weekday-picker-input:checked + .weekday-pill {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--on-primary);
}
.weekday-picker-input:checked + .weekday-pill:hover { transform: translateY(-1px); }
.weekday-picker-input:focus-visible + .weekday-pill { outline: 2px solid var(--primary); outline-offset: 2px; }

/* ── Toggle switches (components/enable-toggle.js, components/toggle.js) ────
   Two color variants sharing one track/thumb geometry:
   a real, visually-hidden checkbox paired with sibling elements styled off
   `:checked`, same technique as the weekday picker above, so `#<id>.checked`
   keeps working unchanged for both.
   - `.enable-toggle` (semantic/status): green/red, reserved for the one
     thing it already means app-wide — active vs. inactive. Doesn't double
     up green/red for a toggle whose "on" state isn't a status
     (CLAUDE.md §13.1 on not doubling up what a hue means).
   - `.toggle` (neutral): primary-ramp when checked, plain neutral track
     when unchecked, no baked-in on/off text — the field's own <label>
     carries the meaning. Use this for every other boolean setting
     (is_hard, is_oncall, resident_eligible, use_compensation_day, …). */
.toggle, .enable-toggle { display: inline-flex; align-items: center; gap: var(--space-2); cursor: pointer; user-select: none; }
.toggle-input, .enable-toggle-input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
.toggle-track, .enable-toggle-track {
  position: relative;
  flex-shrink: 0;
  width: 40px;
  height: 22px;
  border-radius: var(--r-pill);
  transition: background var(--transition), border-color var(--transition);
}
.toggle-thumb, .enable-toggle-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  transition: transform var(--transition), background var(--transition);
}
.toggle-input:checked + .toggle-track .toggle-thumb,
.enable-toggle-input:checked + .enable-toggle-track .enable-toggle-thumb {
  transform: translateX(18px);
}
.toggle-input:focus-visible + .toggle-track,
.enable-toggle-input:focus-visible + .enable-toggle-track { outline: 2px solid var(--p-500); outline-offset: 2px; }

/* Neutral variant colors */
.toggle-track { background: var(--surface-sunk); border: 1px solid var(--border-strong); }
.toggle-thumb { background: var(--muted); }
.toggle-input:checked + .toggle-track { background: var(--p-50); border-color: var(--p-600); }
.toggle-input:checked + .toggle-track .toggle-thumb { background: var(--p-600); }
.toggle-label { font-size: var(--text-sm); color: var(--text); }

/* Semantic (status) variant colors — green/red on/off, unchanged meaning */
.enable-toggle-track { background: var(--danger-bg); border: 1px solid var(--danger); }
.enable-toggle-thumb { background: var(--danger); }
.enable-toggle-input:checked + .enable-toggle-track {
  background: var(--success-bg);
  border-color: var(--success);
}
.enable-toggle-input:checked + .enable-toggle-track .enable-toggle-thumb {
  background: var(--success);
}
.enable-toggle-text { font-size: var(--text-sm); font-weight: 600; }
.enable-toggle-text-on, .enable-toggle-text-off { display: none; }
.enable-toggle-input:checked ~ .enable-toggle-text .enable-toggle-text-on { display: inline; color: var(--success); }
.enable-toggle-input:not(:checked) ~ .enable-toggle-text .enable-toggle-text-off { display: inline; color: var(--danger); }

/* Disabled state — shared token, applies regardless of which value the
   control is currently showing ("control-disabled", as opposed to an
   inactive record). :has() here only moves the cursor onto the label
   wrapping the visually-hidden input; the hidden input already blocks the
   click regardless of what cursor the label shows. */
.toggle-input:disabled + .toggle-track,
.enable-toggle-input:disabled + .enable-toggle-track {
  opacity: var(--opacity-disabled);
  cursor: not-allowed;
}
.toggle:has(.toggle-input:disabled), .enable-toggle:has(.enable-toggle-input:disabled) { cursor: not-allowed; }

/* ── Menu search palette (components/menu-search-palette.js) ────────────────
   A command-palette dialog reads better anchored near the top of the
   viewport than vertically centered like a normal form dialog. */
.modal-menu-search {
  max-width: 560px;
  align-self: flex-start;
  margin-top: 10vh;
}
/* Phone: the one exception to the bottom sheet above. The palette is typed
   into, so it drops from the top edge, where the on-screen keyboard leaves
   its input and first results visible. */
@media (max-width: 600px) {
  .modal-overlay:has(.modal-menu-search) { padding: 0 0 var(--space-6); }
  .modal-menu-search {
    margin-top: 0;
    border-radius: 0 0 var(--radius) var(--radius);
    transform: translateY(-100%);
  }
}

.menu-search-input {
  width: 100%;
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-base);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
}
.menu-search-input:focus { outline: none; border-color: var(--primary); }

.menu-search-listbox {
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
  max-height: 320px;
  overflow-y: auto;
}

.menu-search-option {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.menu-search-option svg { flex-shrink: 0; color: var(--muted); }
.menu-search-option-label { font-size: var(--text-sm); font-weight: 500; }
.menu-search-option-group { margin-left: auto; font-size: var(--text-eyebrow); }

/* --p-50/--p-600/--p-800 already flip under [data-theme="dark"] in base.css
   (the same tokens .nav-link.active already relies on) — no separate dark
   override needed here. */
.menu-search-option.active { background: var(--p-50); color: var(--p-800); }
.menu-search-option.active svg { color: var(--p-600); }

.menu-search-empty { padding: var(--space-3); font-size: var(--text-sm); }
.menu-search-hint { margin-top: var(--space-3); font-size: var(--text-xs); }

/* ── Toast ────────────────────────────────────────────────────────────────── */
#toast-container {
  position: fixed;
  bottom: var(--space-6);
  right: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  z-index: 1000;
}

.toast {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  font-size: var(--text-sm);
  font-weight: 500;
  animation: slideIn 200ms ease;
  max-width: 360px;
}

@keyframes slideIn {
  from { transform: translateX(120%); opacity: 0; }
  to   { transform: translateX(0);   opacity: 1; }
}

.toast-success { background: var(--success); color: var(--on-success); }
.toast-error   { background: var(--danger); color: var(--on-danger); }
.toast-warning { background: var(--warning); color: var(--on-warning); }
.toast-info    { background: var(--primary); color: var(--on-primary); }
.toast-action { flex: none; padding: 2px var(--space-2); border: 1px solid currentColor; border-radius: var(--radius-sm); background: transparent; color: inherit; font: inherit; font-weight: 700; cursor: pointer; }
.toast-action:hover { text-decoration: underline; }
.toast-dismiss { border-color: transparent; }

/* ── Touch drag pick-up banner (touch-drag.js) ───────────────────────────── */
/* Below #toast-container's z-index — a completed reassign always cancels
   pick-up mode first (see performDrop/handleDrop), so the two are never
   meant to be visible for the same interaction; this ordering just protects
   against the rare case of an unrelated toast firing while something is
   still picked up. */
.touch-drag-banner {
  position: fixed;
  left: var(--space-4);
  right: var(--space-4);
  bottom: max(var(--space-4), env(safe-area-inset-bottom));
  z-index: 900;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--space-3) var(--space-4);
}
/* Above the phone bottom bar (layout.css) for the roles that have it. */
@media (max-width: 768px) {
  body.has-bottom-nav #toast-container,
  body.has-bottom-nav .touch-drag-banner { bottom: calc(var(--bottom-nav-offset) + var(--space-3)); }
}
.touch-drag-banner-text {
  font-size: var(--text-sm);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.touch-drag-banner-cancel { flex: none; }

.btn-sm { padding: 2px 8px; font-size: var(--text-sm); }

/* ── Dark theme: select arrow (data-URI colour baked in) ───────────────────── */
[data-theme="dark"] select.input {
  background-image: url("../../assets/select-arrow-dark.svg");
}

/* Inline advisory warnings inside forms (e.g. the wish-submission
   consecutive-off-days heads-up) — same amber treatment as .conflict-soft. */
.inline-warning {
  background: var(--warning-bg);
  border-left: 3px solid var(--warning);
  color: var(--warning);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  font-size: 0.85rem;
  line-height: 1.4;
}

.inline-warning a {
  color: inherit;
  font-weight: 600;
  text-decoration: underline;
}

/* A warning that arose while a form or panel is open, as a state of that
   surface (UX_SPEC.md "Choosing a surface"): a title, paragraphs or a list and
   an optional action, stacked; the surface relabels its button ("… anyway"). */
.inline-warning--stack { display: grid; justify-items: start; gap: var(--space-1); }
.inline-warning--stack[hidden] { display: none; }
.inline-warning--stack > p,
.inline-warning--stack > ul { margin: 0; }
.inline-warning--stack > ul { padding-left: var(--space-4); }


@media (pointer: coarse), (max-width: 600px) {
  #main-content .btn,
  #main-content .input,
  #main-content .filter-chip,
  #main-content .settings-tab { min-height: 44px; }
  #main-content .btn-sm { min-width: 44px; }

}


/* ── Segmented control ─────────────────────────────────────────────────────
   One pill group of mutually exclusive options: buttons or links (the chosen
   one carries aria-pressed="true", aria-checked="true", aria-selected="true"
   for a tablist, or aria-current), or
   labels around native radios, which keep the browser's own keyboard
   behaviour. `.segmented-fill` stretches the options over the full width. */
.segmented {
  display: inline-grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-sunk);
}
.segmented-fill { display: grid; width: 100%; }
/* A page-level view switch in a header toolbar (components/view-switch.js):
   never shrinks, compact tabs except under a coarse pointer (below). */
.view-switch { flex: none; }
.segmented > .btn, .segmented > a {
  min-height: 36px;
  border-color: transparent;
  background: transparent;
  color: var(--text-2);
  justify-content: center;
  text-align: center;
  text-decoration: none;
}
.view-switch > .btn { min-height: 30px; }
.segmented > label {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-height: 36px;
  padding: 0 var(--space-3);
  border: 1px solid transparent;
  border-radius: calc(var(--radius-sm) - 2px);
  color: var(--text-2);
  font-size: var(--text-sm);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
}
.segmented > label > input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.segmented > label:has(input:focus-visible) { outline: 2px solid var(--primary); outline-offset: 1px; }
.segmented > [aria-pressed="true"], .segmented > [aria-checked="true"], .segmented > [aria-selected="true"], .segmented > [aria-current], .segmented > label:has(input:checked) {
  border-color: var(--border-strong);
  background: var(--surface);
  color: var(--text);
  font-weight: 700;
}
@media (pointer: coarse) {
  .segmented > .btn, .segmented > a, .segmented > label { min-height: 44px; }
}

/* ── Banner action ──────────────────────────────────────────────────────────
   The one solid button of a shell banner: the action-needed notice (its tone
   recolours it, shared/misc.css) and the impersonation banner's Stop (red). */
.banner-action {
  flex-shrink: 0;
  padding: 6px var(--space-4);
  border: none;
  border-radius: var(--radius-sm);
  background: var(--danger);
  color: #fff;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
  transition: filter var(--transition), transform var(--transition);
}
.banner-action:hover { filter: brightness(1.08); }
.banner-action:active { filter: brightness(0.94); transform: scale(0.98); }

/* ── Page tabs ──────────────────────────────────────────────────────────────
   A row of links that switch the context a view shows (My Stats, Shift
   Distribution, Day-Ahead sites), or of role="tab" buttons that filter it
   in place (People's role tabs). The current one carries
   aria-current="page" or aria-selected="true"; a long row scrolls instead
   of wrapping. */
.page-tabs { display: flex; gap: var(--space-2); overflow-x: auto; margin-bottom: var(--space-4); }
.page-tabs > a, .page-tabs > button { flex: 0 0 auto; }
.page-tabs [aria-current="page"], .page-tabs [aria-selected="true"] { box-shadow: inset 0 -2px var(--primary); background: var(--surface-sunk); }

/* ── Fact list ──────────────────────────────────────────────────────────────
   A <dl> of label/value pairs for one person's figures: label left and
   muted, value right with aligned digits. Used by the Today balances and
   the personal working-hours report. */
.fact-list {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-1) var(--space-4);
  margin: 0;
}
.fact-list dt { color: var(--muted); }
.fact-list dd { margin: 0; font-weight: 600; text-align: right; font-variant-numeric: tabular-nums; }
} /* @layer components */

@layer components {
/* Specialty badge (see specialty-labels.js) — deliberately monochrome, not
   a location-hue or duty-text channel (CLAUDE.md §13.1): a small identity
   marker on top of the existing chip, same slot as .chip-sick above. Built
   on --surface-sunk/--muted, which already carry their own dark
   values (base.css), so no separate [data-theme="dark"] override is needed. */
.chip-specialty {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  margin-left: 2px;
  border-radius: 50%;
  background: var(--surface-sunk);
  color: var(--muted);
  border: 1px solid var(--border);
  font-size: 0.62rem;
  font-weight: 700;
  line-height: 1;
}

.chip-specialty-more {
  border-radius: var(--radius-sm);
}

/* A specialty the seat is short of, on a candidate who holds it (the Roster
   Editor inspector, specialtyBadgesHtml's isNeeded); --success carries its
   own dark value. */
.chip-specialty.chip-specialty-needed {
  background: var(--surface);
  color: var(--success);
  border-color: currentColor;
}

/* Applied when badge-disambiguator.js had to extend a specialty's badge
   past 2 characters to resolve a collision (rare — most names disambiguate
   at 1-2 letters) — the fixed circle can't hold 3-4 letters legibly, so it
   widens into the same rounded-rect shape .chip-specialty-more already uses. */
.chip-specialty-wide {
  min-width: 20px;
  border-radius: var(--radius-sm);
}

/* Resident identity marker (see resident-badge.js) — same .chip-specialty
   pill shape, tinted with the dedicated cat-berry semantic token
   (color-engine.js's SEMANTIC.residentAssignment) rather than staying
   monochrome, since a resident marker needs its own color to stay
   recognizable at avatar-circle size (.curr-sched-avatar-resident below)
   and to read as one consistent identity next to the same-colored role
   badge in Admin Users (utils.js's roleBadgeClass). Border matches the
   pill's own background tint (not its text color) — the same soft,
   near-invisible-border look .chip-specialty's neutral pill already has;
   an outline matching the text instead would read as a harder, more
   assertive ring than every sibling badge on this chip. */
.chip-resident {
  background: var(--cat-berry-bg);
  color: var(--cat-berry);
  border-color: var(--cat-berry-bg);
}

}

@layer components {
/* ── Shift chips ─────────────────────────────────────────────────────────── */
.shift-chip {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 3px 6px;
  border-radius: var(--radius-sm);
  border-left: 3px solid var(--muted);
  background: var(--surface);
  margin-bottom: 3px;
  font-size: 0.72rem;
  font-weight: 500;
  line-height: 1.2;
  min-height: 24px;
  position: relative;
  box-shadow: 0 1px 2px rgba(0,0,0,.06);
}

.shift-chip:last-child {
  margin-bottom: 0;
}

.chip-dot {
  flex-shrink: 0;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--muted);
}

/* Location identity (see color-engine.js) — border-left + dot carry the
   location's hue, read from the slot class's --loc (base.css "Palette
   slots"). The :is() gate keeps these at (0,2,0), so the chip-unfilled and
   chip-sick-open overrides below still win by source order. */
.shift-chip:is(.loc-1, .loc-2, .loc-3, .loc-4, .loc-5, .loc-6, .loc-7, .loc-8) { border-left-color: var(--loc); }
.shift-chip:is(.loc-1, .loc-2, .loc-3, .loc-4, .loc-5, .loc-6, .loc-7, .loc-8) .chip-dot { background: var(--loc); }

/* Timing (shift_types.is_oncall) — a hollow ring instead of a solid dot,
   same hue, so it reads as "the same location, off-hours" rather than a
   different location. A pure dot-level cue, deliberately not a
   border-style change. */
.shift-chip.chip-oncall .chip-dot { background: transparent; border: 1.5px solid var(--muted); }
.shift-chip.chip-oncall:is(.loc-1, .loc-2, .loc-3, .loc-4, .loc-5, .loc-6, .loc-7, .loc-8) .chip-dot { border-color: var(--loc); }

.chip-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text);
}

/* Unfilled slot — co-occurs with a .loc-N class on the same chip
   (roster-grid.js), so ".shift-chip.loc-N"'s own border-left-color (0,2,0)
   needs beating. Compounding onto .shift-chip matches that specificity
   (0,2,0); declared after the .loc-N block above, so it wins the tie by
   source order without !important. The dot rule compounds the same way:
   ".shift-chip.loc-N .chip-dot" is (0,3,0), so a bare ".chip-unfilled
   .chip-dot" (0,2,0) would lose and the dot would keep the location hue. */
.shift-chip.chip-unfilled {
  background: var(--danger-bg);
  border-left-color: var(--danger);
}

.shift-chip.chip-unfilled .chip-dot {
  background: var(--danger);
}

.chip-unfilled .chip-name {
  color: var(--danger);
  font-style: italic;
}

/* Open because of sickness — a colleague's sick slot as clinical and viewer
   roles receive it, without the occupant (features/roster/chips.js
   isOpenSickSlot). Amber, not red: the position was filled and the
   rostermaker is arranging cover, unlike a never-filled .chip-unfilled.
   Same specificity pattern as .shift-chip.chip-unfilled above, including
   the dot; the on-call ring compounds one class further to beat
   ".shift-chip.chip-oncall.loc-N .chip-dot" (0,4,0) by source order. */
.shift-chip.chip-sick-open { background: var(--warning-bg); border-left-color: var(--warning); }
.shift-chip.chip-sick-open .chip-dot { background: var(--warning); }
.shift-chip.chip-sick-open.chip-oncall .chip-dot { background: transparent; border-color: var(--warning); }
.chip-sick-open .chip-name { color: var(--warning); font-style: italic; }

/* A planned compensation day off (features/roster/chips.js
   compensationDayMarkHtml, the signal catalog's compensationDay): the COMP
   hue (--comp/--comp-bg, dark included in base.css), always dashed, so it
   never reads as a location hue or a problem. Each host sizes it (People
   cell, calendar day, Today's strip); the legend sample keeps this size. */
.comp-day-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 4px;
  border: 1px dashed var(--comp);
  border-radius: 3px;
  background: var(--comp-bg);
  color: var(--comp);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  line-height: 1.5;
  white-space: nowrap;
}

}

@layer components {
.admin-user-role {
  min-width: 2.1rem;
  padding-inline: 5px;
  text-align: center;
}
}
