/* ============================================================
   Seagoat — Admin panel
   Built entirely on styles.css tokens & component classes.
   Dense, data-rich operations console. Light theme default.
   ============================================================ */

/* derive warning/danger in the same oklch lightness/chroma band as --pos.
   Every themed value here is a light-dark() pair on the same terms
   tokens.css sets out: the light half is the original literal, unchanged,
   so the 2026-08-16 a11y audit's measured ratios still hold. */
:root {
  --warn: oklch(0.74 0.14 75);
  --warn-tint: light-dark(oklch(0.95 0.05 80), oklch(0.33 0.075 80));
  /* --warn itself is under 3:1 for white text/icons at any weight (a11y
     audit, 2026-08-16) — this is the --warn-family equivalent of
     --brand-700, for the handful of solid-fill badges that put a white
     glyph directly on a warn background (dispatch's own stop/pin kind
     markers). A solid fill with a white glyph reads the same on either
     theme, so this one doesn't flip. */
  --warn-700: oklch(0.45 0.14 75);
  --danger: oklch(0.62 0.16 22);
  --danger-tint: light-dark(oklch(0.95 0.03 20), oklch(0.33 0.075 22));
  --pos-tint: light-dark(oklch(0.95 0.04 155), oklch(0.33 0.075 155));
  /* Cyan was the one status family with no tint token at all — .status.s-cyan
     carried both halves as literals. Tokenised here so it flips with the
     other three rather than being the one badge left light-on-light. */
  --cyan-tint: light-dark(oklch(0.95 0.04 195), oklch(0.33 0.075 195));

  /* The ink that goes ON those tints. The light halves are the exact
     darkened tones the a11y audit landed on (and which .status.s-live /
     .delta.up / .alert-*-ic were each repeating as a literal); the dark
     halves lift to ~0.86 lightness at the same hue, measured 7.3-8.1:1
     against their own tint. Two sites keep an inline light-dark() pair
     instead of a token — .alert-danger/.alert-ok's body text and
     .btn-danger-soft's label sit a step darker again (0.42-0.45) than the
     badge tone, and folding them in here would have quietly changed the
     light theme to make the dark one tidier. */
  --pos-ink:    light-dark(oklch(0.45 0.12 155), oklch(0.86 0.13 155));
  --warn-ink:   light-dark(oklch(0.5 0.13 65), oklch(0.86 0.12 75));
  --danger-ink: light-dark(oklch(0.5 0.16 22), oklch(0.86 0.12 22));
  --cyan-ink:   light-dark(oklch(0.45 0.1 195), oklch(0.86 0.09 195));

  --sidebar-w: 232px;
  --topbar-h: 52px;
  /* resting border for text fields — darker than --line-2 so the field edge
     itself clears WCAG 2.2 SC 1.4.11 (3:1 for a UI component boundary).
     Measured: #8a939f = 3.11:1 on white; the dark half measures 3.5:1 on
     --bg and 4.0:1 on --bg-soft, the two surfaces a field ever sits on.
     Do not lighten the light half past oklch(0.66), or darken the dark
     half past oklch(0.56). */
  --field-line: light-dark(oklch(0.66 0.02 255), oklch(0.56 0.02 257));

  /* Translucent, blurred chrome that floats over content: the topbar and
     the map overlays. Not --bg with an alpha — a custom property can't
     take one — so the dark half restates --dark-2's own coordinates. */
  --veil: light-dark(oklch(1 0 0 / 0.85), oklch(0.24 0.04 257 / 0.82));
  --veil-strong: light-dark(oklch(1 0 0 / 0.92), oklch(0.24 0.04 257 / 0.92));

  /* The <select> chevron. light-dark() substitutes <color> values only,
     and this one's stroke is baked into a data: URI, so it's the single
     token in the whole palette that needs a real per-theme override
     rather than a pair — hence the two rules under this block. */
  --chevron-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  --chevron-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2398a2b3' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  --select-chevron: var(--chevron-light);
}
:root[data-theme="dark"] { --select-chevron: var(--chevron-dark); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --select-chevron: var(--chevron-dark); }
}

html, body { height: 100%; }
body {
  background: var(--bg-soft);
}
/* The admin shell owns its own scrolling (.content), so the document itself
   must not scroll too — otherwise a long page like /settings gets a second,
   outer scrollbar. Scoped with :has() rather than set on body outright, so
   the standalone pages that share this stylesheet (login, public order,
   invoice, receipt) still scroll normally. */
html:has(.admin), body:has(.admin) { overflow: hidden; }

/* ---------- App shell ---------- */
/* overflow:hidden lives here, not on body: the admin shell owns its own
   scrolling (.content), but standalone pages (login, public order,
   invoice, receipt) share this stylesheet and must scroll normally. */
.admin {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  height: 100vh;
  width: 100%;
  overflow: hidden;
}
.admin.nav-collapsed { grid-template-columns: 76px 1fr; }

/* ---------- View transitions ---------- */
/* actions.js wraps every swap in document.startViewTransition (see its
   own comment on render()). Unscoped, that's the bare browser default:
   the *entire* page gets snapshotted and cross-faded as one flat image
   on every single navigation — sidebar and topbar included, even when
   neither changed, and #flash included, even though it has its own
   is-entering/is-leaving animation (this file's "Page-level banners"
   section) that a root-level crossfade just steps on: the toast gets
   frozen into the "old"/"new" screenshots and whatever it looked like
   at that instant is what fades, not what its own keyframes were doing.
   Between two meaningfully different layouts — the order page's own
   Overview <-> Details tabs, say — a flat opacity cross-fade between two
   screenshots doesn't even read as motion, just a blur.

   idiomorph has already morphed #content in smoothly by the time any of
   this would play, so the fix isn't a *better* crossfade, it's turning
   this one off everywhere except the one thing that should actually
   animate: #flash gets its own named group, carved out of the root
   snapshot entirely, and every group's own default crossfade is turned
   off — is-entering/is-leaving are the only animation left, running on
   the live element instead of a frozen screenshot of it. */
::view-transition-group(*) { animation-duration: 0s; }
::view-transition-old(*),
::view-transition-new(*) { animation: none; }

@media (prefers-reduced-motion: reduce) {
  /* The rule above already leaves nothing animating at the page-
     transition level, but the API doesn't respect this media query on
     its own — stated explicitly in case a future screen adds its own
     view-transition-name with a real animation attached. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

/* ---------- Focus visibility (keyboard) ---------- */
/* Every selector below is a real, keyboard-reachable <a>/<button>
   (confirmed against the templates that use them) that already had a
   :hover state but no :focus-visible equivalent — a mouse user got a
   signal, a keyboard user got only the browser's own default outline
   (input/.skip-link/.toggle already do this correctly; this is that
   same fix for everything else, a11y audit 2026-08-16).
   Centralized here in one pass rather than distributed into each
   component's own section below, since the fix — and the outline — is
   identical everywhere. The literal --brand-700 these all used became
   --focus-ring when the dark theme landed: --brand-700 is close to the
   darkest thing in the dark palette, which would have made the focus
   ring the one indicator a keyboard user couldn't see. */
.btn:focus-visible,
.nav-item:focus-visible,
.order-tabs .tab:focus-visible,
.settings-nav button:focus-visible,
.settings-nav a:focus-visible,
.card-link:focus-visible,
.filter-chip:focus-visible,
.pager button:focus-visible,
.pager a:focus-visible,
.row-act:focus-visible,
.icon-btn:focus-visible,
.topbar-collapse:focus-visible,
.x-btn:focus-visible,
.side-user:focus-visible,
.seg button:focus-visible,
.courier-card:focus-visible,
.map-zoom button:focus-visible,
.crumb-link:focus-visible,
.dispatch-pin-link:focus-visible,
.dsc-label:focus-visible,
.dispatch-route-head .dr-name:focus-visible,
.thread-item:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* ============================================================
   SIDEBAR
   ============================================================ */
.sidebar {
  background: var(--bg);
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  min-height: 0;
  z-index: 40;
}
.sidebar-head {
  height: var(--topbar-h);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 18px;
  border-bottom: 1px solid var(--line);
  flex: none;
}
/* A flex row, not inline spans: the .mark is display:grid and would
   otherwise break onto its own line above the name — which a 64px head
   happened to fit and a 56px one clips. */
.sidebar .brand { display: flex; align-items: center; font-size: 20px; gap: 11px; }
.sidebar .brand .mark {
  width: 34px; height: 34px; border-radius: 10px;
  background: var(--brand); color: #fff;
  display: grid; place-items: center;
}
.sidebar .brand .mark svg { width: 24px; height: 24px; }
.nav-collapsed .sidebar .brand span:last-child,
.nav-collapsed .sidebar-section-label,
.nav-collapsed .nav-item span,
.nav-collapsed .nav-item .nav-badge,
.nav-collapsed .side-user .su-meta,
.nav-collapsed .side-foot-text { display: none; }
.nav-collapsed .sidebar-head { justify-content: center; padding: 0; }
.nav-collapsed .nav-item { justify-content: center; padding: 11px; }
.nav-collapsed .side-user { justify-content: center; }

.sidebar-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 10px 8px 8px;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.sidebar-scroll::-webkit-scrollbar { width: 8px; }
.sidebar-scroll::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 8px; }

.sidebar-section-label {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding: 16px 12px 7px;
}
.sidebar-section-label:first-child { padding-top: 4px; }

.nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 10px;
  border-radius: var(--r-sm);
  font-size: 15.5px;
  font-weight: 600;
  color: var(--ink-2);
  cursor: pointer;
  border: none;
  background: transparent;
  width: 100%;
  text-align: left;
  transition: background .15s ease, color .15s ease;
  position: relative;
}
.nav-item .nav-ic { width: 19px; height: 19px; flex: none; color: var(--ink-3); transition: color .15s ease; }
.nav-item:hover { background: var(--bg-soft-2); color: var(--ink); }
.nav-item:hover .nav-ic { color: var(--ink-2); }
.nav-item.active { background: var(--brand-tint); color: var(--brand-ink); }
.nav-item.active .nav-ic { color: var(--brand-link); }
.nav-item .nav-badge {
  margin-left: auto;
  font-size: 12px;
  font-weight: 700;
  font-family: var(--font-display);
  padding: 2px 8px;
  border-radius: 999px;
  /* --brand under white text measures ~3.6:1 at this size (a11y audit,
     2026-08-16) — --brand-700 clears it the same way .pager's active
     page number does below. */
  background: var(--brand-700);
  color: #fff;
  line-height: 1.5;
}
.nav-item .nav-badge.muted { background: var(--bg-soft-2); color: var(--ink-3); }
.nav-item.active .nav-badge.muted { background: var(--brand-tint-2); color: var(--brand-ink); }

.sidebar-foot {
  flex: none;
  border-top: 1px solid var(--line);
  padding: 12px;
}
.side-user {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 7px 8px;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background .15s ease;
}
.side-user:hover { background: var(--bg-soft-2); }
.side-user .ava {
  width: 36px; height: 36px; border-radius: 50%; flex: none;
  /* was a --brand-300→--brand-700 gradient: contrast for the white
     initials literally varied across the glyph, down to 2.29:1 at the
     --brand-300 end (a11y audit, 2026-08-16). Flat --brand-600 matches
     .cell-user .ava-sm, already the correct pattern elsewhere. */
  background: var(--brand-600);
  color: #fff; display: grid; place-items: center;
  font-weight: 700; font-family: var(--font-display); font-size: 15px;
}
.side-user .su-meta { min-width: 0; flex: 1; }
.side-user .su-name { font-size: 15px; font-weight: 700; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.side-user .su-role { font-size: 13px; color: var(--ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.side-user .su-chev { color: var(--ink-3); width: 16px; height: 16px; flex: none; }

/* ============================================================
   MAIN COLUMN
   ============================================================ */
.main {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}
.topbar {
  height: var(--topbar-h);
  flex: none;
  background: var(--veil);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
  backdrop-filter: blur(14px) saturate(160%);
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 0 20px;
  z-index: 30;
}
.topbar-collapse {
  width: 36px; height: 36px; border-radius: var(--r-xs);
  display: grid; place-items: center; flex: none;
  /* --line-2 → --field-line: same sole-boundary fix (a11y audit,
     2026-08-16). */
  border: 1px solid var(--field-line); background: var(--bg);
  color: var(--ink-2); box-shadow: var(--sh-sm);
}
.topbar-collapse:hover { border-color: var(--ink-3); color: var(--ink); }
.topbar-collapse svg { width: 18px; height: 18px; }
/* One header line: crumb › title · subtitle. The subtitle is the only
   part allowed to truncate at desktop width; the crumb never truncates
   — below 900px it is hidden outright (with the subtitle, see the
   media block below), because a short title behind a wide crumb
   ("Workspace › Dispatch") otherwise rendered "D…" on a phone
   (2026-09-30) — and the title truncates only below 900px, where a
   long one ("RT-20 · North Zone · …") would otherwise push the topbar
   buttons off a phone — see the Routes rework block at the end. */
.page-title { min-width: 0; flex: 1; display: flex; align-items: baseline; gap: 8px; white-space: nowrap; overflow: hidden; }
.page-title h1 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 19px;
  letter-spacing: -0.02em;
  color: var(--ink);
  line-height: 1.1;
  flex: none;
}
.page-title .crumb { font-size: 14px; color: var(--ink-3); font-weight: 600; flex: none; }
.page-title .crumb-sep { font-size: 14px; color: var(--ink-3); flex: none; }
.page-title .page-sub { font-size: 14px; color: var(--ink-3); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.page-title .page-sub:empty { display: none; }
.page-title .page-sub::before { content: "·"; margin-right: 8px; }

.topbar-search {
  margin-left: 8px;
  position: relative;
  display: flex;
  align-items: center;
  width: 300px;
  max-width: 34vw;
}
.topbar-search svg { position: absolute; left: 13px; width: 17px; height: 17px; color: var(--ink-3); pointer-events: none; }
.topbar-search input {
  width: 100%;
  height: 40px;
  padding: 0 14px 0 38px;
  border-radius: 999px;
  border: 1px solid var(--field-line);
  background: var(--bg-soft);
  font-family: var(--font-body);
  font-size: 15.5px;
  color: var(--ink);
  outline: none;
  transition: border-color .18s ease, background .18s ease, box-shadow .18s ease;
}
.topbar-search input::placeholder { color: var(--ink-3); }
.topbar-search input:focus {
  border-color: var(--brand); background: var(--bg); box-shadow: 0 0 0 4px var(--brand-tint-2);
  outline: 2px solid var(--focus-ring); outline-offset: 2px;
}
.topbar-search .kbd {
  position: absolute; right: 12px; font-size: 12px; font-weight: 700; color: var(--ink-3);
  border: 1px solid var(--line-2); border-radius: 6px; padding: 2px 6px; background: var(--bg);
}

.topbar-actions { margin-left: auto; display: flex; align-items: center; gap: 10px; flex: none; }
/* The + New menu: a .pop anchored to its own button rather than the
   portal's 62px topbar offset. */
.quick-new { position: relative; }
.quick-new-btn { display: inline-flex; align-items: center; gap: 6px; }
.quick-new-btn svg { width: 16px; height: 16px; }
.quick-new-pop { top: 44px; right: 0; width: 220px; padding: 6px; }
.icon-btn {
  width: 36px; height: 36px; border-radius: 999px;
  display: grid; place-items: center; position: relative;
  /* --line-2 → --field-line: same sole-boundary fix (a11y audit,
     2026-08-16). */
  border: 1px solid var(--field-line); background: var(--bg);
  color: var(--ink-2); box-shadow: var(--sh-sm);
  transition: border-color .15s ease, color .15s ease, transform .15s ease;
}
.icon-btn:hover { border-color: var(--ink-3); color: var(--ink); transform: translateY(-1px); }
.icon-btn svg { width: 19px; height: 19px; }
.icon-btn .dot {
  position: absolute; top: 8px; right: 9px; width: 8px; height: 8px; border-radius: 50%;
  background: var(--danger); border: 2px solid var(--bg);
}

/* ---------- Content scroll ---------- */
.content {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: auto;
  padding: 12px;
}
.content::-webkit-scrollbar { width: 11px; }
.content::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 8px; border: 3px solid var(--bg-soft); }
.content::-webkit-scrollbar-track { background: transparent; }

.page { max-width: 1680px; margin-inline: auto; display: flex; flex-direction: column; gap: 8px; }

/* ---------- Page header (within content) ---------- */
/* A page's own actions (Edit, Remove, Back to order…). Titles and
   subtitles live in the topbar's .page-title now — see layout.html. */
.page-bar {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
}
.page-bar.page-actions { justify-content: flex-end; }
.page-bar .spacer { flex: 1; }

/* date-range / segmented control */
.seg {
  display: inline-flex; align-items: center; gap: 2px;
  background: var(--bg-soft-2); border: 1px solid var(--line); border-radius: 999px; padding: 3px;
}
.seg button {
  border: none; background: transparent; padding: 5px 12px; border-radius: 999px;
  font-family: var(--font-body); font-weight: 600; font-size: 14.5px; color: var(--ink-2);
  transition: all .18s ease;
}
.seg button.active { background: var(--bg); color: var(--ink); box-shadow: var(--sh-sm); }
.seg button:hover:not(.active) { color: var(--ink); }

.btn.btn-sm { padding: 5px 10px; font-size: 14.5px; }
.btn-soft {
  background: var(--brand-tint); color: var(--brand-ink); border-color: transparent;
}
.btn-soft:hover { background: var(--brand-tint-2); transform: translateY(-1px); }
/* destructive actions (Remove / Delete / Deactivate) */
.btn-danger-soft {
  background: var(--danger-tint);
  /* a step darker than --danger-ink (the badge tone) — kept as its own
     inline pair rather than folded into that token, which would have
     changed the light theme to tidy up the dark one. */
  color: light-dark(oklch(0.45 0.16 22), oklch(0.88 0.10 22));
  border-color: transparent;
}
/* hover darkens past --danger itself: white on --danger is only 3.9:1, below AA
   for the label. On dark it goes the other way — a light red fill with dark
   lettering — because darkening a fill that already sits on a dark card just
   makes the button disappear on hover. */
.btn-danger-soft:hover {
  background: light-dark(oklch(0.48 0.16 22), oklch(0.75 0.16 22));
  color: light-dark(#fff, oklch(0.20 0.05 22));
  transform: translateY(-1px);
}

/* Busy state actions.js toggles on a trigger (or its data-indicator) while
   a fragment request is in flight — the spinner itself is killed by the
   prefers-reduced-motion rule in tokens.css, same as everything else that
   animates here. */
[data-indicator-active] { pointer-events: none; opacity: .72; }
[data-indicator-active]::after {
  content: ""; display: inline-block; width: 13px; height: 13px; margin-left: 8px; vertical-align: -2.5px;
  border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%;
  animation: action-spin .6s linear infinite;
}
@keyframes action-spin { to { transform: rotate(360deg); } }

/* ============================================================
   CARD PRIMITIVE
   ============================================================ */
.card {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--sh-sm);
}
.card-head {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 10px;
  border-bottom: 1px solid var(--line);
}
.card-head h3 { font-family: var(--font-display); font-weight: 600; font-size: 16.5px; letter-spacing: -0.02em; color: var(--ink); }
.card-head .ch-sub { font-size: 13.5px; color: var(--ink-3); margin-top: 1px; }
.card-head .spacer { flex: 1; }
/* Tightened 2026-08-16 — a card audit found the 18px/16px-18px original
   padding, plus 22-24px gaps between stacked field/card blocks, adding up
   to a lot of vertical space per card (most visibly Settings' 14 stacked
   cards and Order create's single big one). See CLAUDE.md-adjacent
   templates for the matching field-spacing pass (16/18/22/24px inline
   gaps collapsed to a 12/16px scale) and Settings' card grid below. */
.card-body { padding: 8px 10px; }
/* .card.pad — a bare card with no separate .card-head/.card-body split,
   padded straight on the outer element (same 14px/16px .card-body
   already uses). Used across the customer portal (Payments' summary
   tiles, Profile's identity card, Support's channel cards, Order
   detail's Collection/Delivery cards) — .card itself carries zero
   padding by design (.card-head/.card-body own it), so every one of
   those was rendering with its content flush against the card's own
   border until this rule existed; only each card's own ad-hoc
   margin-top/margin-bottom on individual children was doing anything,
   which never touched the padding around the whole block. */
.card.pad { padding: 14px 16px; }
.card-link {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 14.5px; font-weight: 600; color: var(--brand-link);
}
/* Order detail's "at a glance" strip on the details tab — a compact
   preview of each other tab (Collection/Delivery/Payments/Items/Photos/
   Logs/Notify), each one a small bordered tile rather than the KPI
   card's heavier numeric-stat treatment (.kpi-card), which doesn't fit
   short summary text. auto-fit so it reflows instead of overflowing on
   narrower widths, same pattern .grid-2/.grid-3 already use. */
.overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); gap: 16px; }
.overview-tile {
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 10px 12px;
  display: flex; flex-direction: column; gap: 8px;
}
.overview-tile .field-section-label { font-size: 13px; }
.overview-tile-body { font-size: 16.5px; color: var(--ink); line-height: 1.5; }
.overview-tile .card-link { font-size: 15px; margin-top: 2px; }

/* Order identity bar — replaced the "Order summary" card that used to
   wrap the editable title/status form above the tabs. Read-only, one
   line, so the tabs and the Overview's two stop panels start ~140px
   higher on every tab. The editing moved to the Details tab; see
   orders/detail.html's own comment. */

/* The journey header (2026-09-27): the order as its two ends joined by
   a line. Three columns — collection, the line, delivery — with the
   line on the postcode's row, so the drawing and the two places read as
   one sentence. The nodes take the map pins' own progress colours
   (--pin-*), and a half of the line is solid once the crew has been to
   the stop it leads to, dotted ahead of it — the stop maps' rule, so the
   header and the maps are one drawing. Delivery mirrors collection
   (right-aligned) so the eye travels C → D. Below 820px the ends stack
   and the line turns vertical, like the run sheet's spine. */
.journey { margin-bottom: 16px; overflow: hidden; }
.jr {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(120px, 0.9fr) minmax(0, 1fr);
  grid-template-areas: "c line d" "c mid d";
  grid-template-rows: auto 1fr;
  column-gap: 20px; padding: 16px 18px 18px;
}
.jr-end.jr-collection { grid-area: c; }
.jr-end.jr-delivery { grid-area: d; text-align: right; justify-items: end; }
.jr-end { display: grid; align-content: start; gap: 3px; min-width: 0; }
.jr-kind { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--ink-2); }
.jr-delivery .jr-kind { flex-direction: row-reverse; }
.jr-place {
  font-family: var(--font-display); font-size: 26px; font-weight: 600;
  letter-spacing: -0.01em; line-height: 1.15; color: var(--ink); margin-top: 6px;
  overflow-wrap: anywhere;
}
.jr-addr { font-size: 14px; color: var(--ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.jr-when { font-size: 14.5px; font-weight: 600; color: var(--ink); margin-top: 8px; }
.jr-state { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 13.5px; color: var(--ink-2); }
.jr-delivery .jr-state { justify-content: flex-end; }
.jr-answer { color: var(--pos-ink); font-weight: 600; }
.jr-answer::before { content: "·"; color: var(--ink-3); font-weight: 400; margin-right: 6px; }
.jr-answer.jr-declined { color: var(--danger-ink); }
.jr-facts { display: flex; flex-wrap: wrap; column-gap: 12px; font-size: 13.5px; color: var(--ink-2); }
.jr-delivery .jr-facts { justify-content: flex-end; }
.jr-rated { display: inline-flex; align-items: center; gap: 5px; }
.jr-rated svg { width: 14px; height: 14px; }
.jr-note { font-size: 13px; color: var(--ink-3); }
.jr-own-route { margin-top: 4px; }
.jr-shared .jr-own-route { display: none; }

.jr-route {
  display: inline-flex; align-items: center; gap: 7px; max-width: 100%;
  font-size: 13.5px; font-weight: 600; color: var(--brand-link); text-decoration: none;
}
.jr-route:hover { text-decoration: underline; }
.jr-route .rt-swatch { width: 4px; height: 15px; border-radius: 2px; background: var(--line-2); flex: none; }

.jr-line { grid-area: line; display: flex; align-items: center; margin-top: 44px; }
.jr-mid { grid-area: mid; text-align: center; padding-top: 8px; font-size: 13.5px; }
.jr-seg { flex: 1; border-top: 2px dotted var(--ink-3); }
.jr-seg.jr-travelled { border-top-style: solid; border-top-color: var(--ink-2); }
.jr-node, .jr-dot { flex: none; border-radius: 50%; border: 2px solid var(--pin-ahead); background: var(--bg); }
.jr-node { width: 14px; height: 14px; }
.jr-dot { width: 9px; height: 9px; border-width: 1.5px; }
.jr-done { background: var(--pin-done); border-color: var(--pin-done); }
.jr-active { background: var(--pin-active); border-color: var(--pin-active); }
.jr-failed { background: var(--pin-failed); border-color: var(--pin-failed); }
.jr-skipped { background: var(--pin-skipped); border-color: var(--pin-skipped); }

/* The next step: one sentence and what to do about it, on a tint of its
   tone. Absent when nothing needs doing — the header shows no filler. */
.jr-next {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 12px 18px; border-top: 1px solid var(--line);
}
.jr-next > svg { width: 18px; height: 18px; flex: none; }
.jr-next p { margin: 0; flex: 1 1 160px; min-width: 0; font-weight: 600; color: var(--ink); }
.jr-next-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.jr-next-danger { background: var(--danger-tint); }
.jr-next-danger > svg { color: var(--danger-ink); }
.jr-next-warn { background: var(--warn-tint); }
.jr-next-warn > svg { color: var(--warn-ink); }
.jr-next-info { background: var(--brand-tint); }
.jr-next-info > svg { color: var(--brand-ink); }

@media (max-width: 820px) {
  .jr { grid-template-columns: minmax(0, 1fr); grid-template-areas: "c" "line" "mid" "d"; grid-template-rows: none; row-gap: 0; }
  .jr-end.jr-delivery { text-align: left; justify-items: start; }
  .jr-delivery .jr-kind { flex-direction: row; }
  .jr-delivery .jr-state, .jr-delivery .jr-facts { justify-content: flex-start; }
  /* The line turns vertical at the badges' left edge, like the run
     sheet's spine: node, travelled half, ahead half, node. */
  .jr-line { flex-direction: column; align-items: flex-start; margin: 10px 0 0 4px; height: 64px; }
  .jr-seg { flex: 1; border-top: 0; border-left: 2px dotted var(--ink-3); margin-left: 6px; }
  .jr-seg.jr-travelled { border-left-style: solid; border-left-color: var(--ink-2); }
  .jr-mid { text-align: left; padding: 0 0 10px 26px; margin-top: -40px; min-height: 30px; }
  .jr-next { align-items: flex-start; }
  .jr-next-actions { flex-basis: 100%; padding-left: 28px; }
}

/* Overview: Collection and Delivery at half width each, above the
   remaining tiles. Collapses to one column on narrow viewports the same
   auto-fit way .overview-grid does, rather than at a hand-picked
   breakpoint. */
.overview-split {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
  gap: 16px; margin-bottom: 16px;
}
/* The panel is a <form>. Its rows keep the label-left/value-right shape
   the read-only version had; the value is now a control. align-items is
   center rather than baseline because an .input's baseline sits well
   below a 12.5px uppercase label's.

   .stop-panel is also worn by the stop tab's read-only "Driver progress"
   block (0050, orders/detail.html), which wants the same row shape with
   no controls in it — that block sits in the card, not in .stop-form, so
   it would otherwise match none of these selectors. */
.stop-panel { display: flex; flex-direction: column; gap: 14px; }
.stop-panel .sp-meta { display: flex; flex-direction: column; gap: 10px; }
.stop-panel .spm { display: flex; align-items: center; gap: 12px; }
.stop-panel .spm-k {
  flex: none; width: 84px; font-size: 13.5px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em; color: var(--ink-3);
}
.stop-panel .spm-v { flex: 1; min-width: 0; font-size: 16.5px; color: var(--ink); }
.stop-panel .spm-v .input { width: 100%; }
/* Route: shown, not edited — see the template's own comment for why. */
.stop-panel .sp-ro { font-size: 15.5px; color: var(--ink-2); }
.stop-panel .sp-foot { margin-top: auto; display: flex; align-items: center; gap: 12px; }
.stop-panel .sp-foot .spacer { flex: 1; }

/* Order detail used to carry its own one-step text bump under
   .order-page-lg (2026-08-15). The compact-console pass (2026-09-07, on
   direct request: less padding, slightly larger text everywhere) took
   that comment's own advice and promoted the readable sizes into the
   base rules above — table text 14px, labels 13.5px, hints 13px, tabs
   14.5px — so the console has one scale. The class stays on the page's
   root for anything page-specific later; it sets nothing today. */
.card-link svg { width: 15px; height: 15px; transition: transform .18s ease; }
.card-link:hover svg { transform: translateX(2px); }

/* ============================================================
   KPI ROW
   ============================================================ */
.kpi-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  gap: 8px;
}
.kpi-card {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 10px 12px;
  box-shadow: var(--sh-sm);
  position: relative;
  overflow: hidden;
  min-width: 0;
  transition: transform .18s ease, box-shadow .22s ease, border-color .2s ease;
}
.kpi-card:hover { transform: translateY(-2px); box-shadow: var(--sh-md); border-color: var(--line-2); }
.kpi-top { display: flex; align-items: center; gap: 10px; }
.kpi-ic {
  width: 38px; height: 38px; border-radius: 11px; flex: none;
  display: grid; place-items: center;
}
.kpi-ic svg { width: 19px; height: 19px; }
.kpi-label { font-size: 13px; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase; color: var(--ink-3); }
.kpi-val {
  font-family: var(--font-display); font-weight: 600; font-size: 30px;
  letter-spacing: -0.03em; color: var(--ink); margin-top: 12px; line-height: 1;
  min-width: 0; overflow-wrap: anywhere;
}
.kpi-val .unit { font-size: 17px; color: var(--ink-3); font-weight: 500; margin-left: 2px; }
.kpi-foot { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
.delta {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: 13.5px; font-weight: 700; font-family: var(--font-display);
  padding: 2px 8px; border-radius: 999px;
}
.delta svg { width: 13px; height: 13px; }
/* --pos/--danger measure under 3.4:1 as text on their own tints (a11y
   audit, 2026-08-16) — same darkened tones .status.s-live/s-delayed
   already use safely, reused here rather than inventing a third value. */
.delta.up { color: var(--pos-ink); background: var(--pos-tint); }
.delta.down { color: var(--danger-ink); background: var(--danger-tint); }
.delta.flat { color: var(--ink-3); background: var(--bg-soft-2); }
.kpi-foot .kpi-note { font-size: 13px; color: var(--ink-3); }

/* sparkline */
.spark { display: block; width: 100%; height: 30px; margin-top: 12px; }
.spark path.line { fill: none; stroke: var(--brand); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.spark path.area { fill: url(#sparkfill); stroke: none; }

/* ============================================================
   DASHBOARD GRID
   ============================================================ */
.dash-grid {
  display: grid;
  grid-template-columns: 1fr 360px;
  gap: 18px;
  align-items: start;
}
.dash-col { display: flex; flex-direction: column; gap: 18px; min-width: 0; }

/* ---------- Live map panel ---------- */
.map-panel { padding: 0; overflow: hidden; }
.map-canvas {
  position: relative;
  height: 380px;
  background:
    radial-gradient(circle at 28% 32%, light-dark(oklch(0.96 0.025 var(--brand-h)), oklch(0.30 0.05 var(--brand-h))), transparent 55%),
    radial-gradient(circle at 78% 68%, light-dark(oklch(0.96 0.03 195), oklch(0.30 0.05 195)), transparent 50%),
    var(--bg);
  overflow: hidden;
}
.map-canvas .map-grid-svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.map-grid-svg line { stroke: var(--line); stroke-width: 1; }
.map-canvas .route-bg { stroke: var(--brand-tint-2); stroke-width: 8; fill: none; stroke-linecap: round; }
.map-canvas .route { stroke: var(--brand); stroke-width: 3; fill: none; stroke-linecap: round; stroke-dasharray: 7 8; }
.map-canvas .route.cyan { stroke: var(--accent); }
.map-canvas .route-bg.cyan { stroke: light-dark(oklch(0.92 0.05 195), oklch(0.38 0.08 195)); }
.map-canvas .stop { fill: var(--bg); stroke: var(--brand); stroke-width: 3; }
.map-truck {
  position: absolute; width: 32px; height: 32px; border-radius: 10px;
  background: var(--brand); display: grid; place-items: center;
  box-shadow: 0 6px 16px oklch(0.55 0.14 var(--brand-h) / 0.5);
  transform: translate(-50%, -50%); cursor: pointer; z-index: 3;
  transition: transform .2s ease;
  border: 2px solid var(--bg);
}
.map-truck:hover { transform: translate(-50%, -50%) scale(1.12); z-index: 5; }
.map-truck svg { width: 17px; height: 17px; color: #fff; }
/* .cyan's own background is darkened from --accent (2.19:1 for the white
   icon on top, a11y audit 2026-08-16) — scoped to this one background,
   not the --accent token itself, since --accent's other consumers are
   strokes/fills with no text-contrast requirement of their own. */
.map-truck.cyan { background: oklch(0.5 0.1 195); box-shadow: 0 6px 16px oklch(0.7 0.12 195 / 0.5); }
.map-truck.warn { background: var(--warn-700); box-shadow: 0 6px 16px oklch(0.74 0.14 75 / 0.5); }
.map-tag {
  position: absolute; padding: 6px 10px; border-radius: 9px; background: var(--bg);
  font-size: 12.5px; font-weight: 700; color: var(--ink); box-shadow: var(--sh-md);
  border: 1px solid var(--line); white-space: nowrap; transform: translate(-50%, -130%); z-index: 4;
  pointer-events: none;
}
.map-tag .mt-sub { font-weight: 600; color: var(--ink-3); font-size: 11.5px; }
.map-overlay {
  position: absolute; left: 16px; top: 16px; z-index: 6;
  display: flex; gap: 8px; flex-wrap: wrap;
}
.map-chip {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--veil-strong); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  border: 1px solid var(--line); border-radius: 999px; padding: 6px 12px;
  font-size: 13.5px; font-weight: 700; color: var(--ink); box-shadow: var(--sh-sm);
}
.map-chip .swatch { width: 9px; height: 9px; border-radius: 50%; }
.map-legend {
  position: absolute; right: 16px; bottom: 16px; z-index: 6;
  background: var(--veil-strong); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 10px 13px;
  display: grid; gap: 6px; box-shadow: var(--sh-md);
}
.map-legend .lg-row { display: flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 600; color: var(--ink-2); }
.map-legend .lg-row .swatch { width: 8px; height: 8px; border-radius: 50%; }
.map-zoom {
  position: absolute; right: 16px; top: 16px; z-index: 6;
  display: grid; gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: var(--r-xs); overflow: hidden;
  box-shadow: var(--sh-sm);
}
.map-zoom button { width: 34px; height: 34px; border: none; background: var(--bg); color: var(--ink-2); display: grid; place-items: center; }
.map-zoom button:hover { background: var(--bg-soft-2); color: var(--ink); }
.map-zoom svg { width: 16px; height: 16px; }

/* ============================================================
   LIVE FEED
   ============================================================ */
.feed { display: flex; flex-direction: column; }
.feed-list { display: flex; flex-direction: column; max-height: 420px; overflow-y: auto; }
.feed-list::-webkit-scrollbar { width: 7px; }
.feed-list::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 8px; }
.feed-item {
  display: flex; gap: 12px; padding: 13px 18px;
  border-bottom: 1px solid var(--line);
  position: relative;
}
.feed-item:last-child { border-bottom: none; }
.feed-dot {
  width: 30px; height: 30px; border-radius: 9px; flex: none; margin-top: 1px;
  display: grid; place-items: center;
}
.feed-dot svg { width: 16px; height: 16px; }
.feed-main { min-width: 0; flex: 1; }
.feed-title { font-size: 14.5px; font-weight: 600; color: var(--ink); line-height: 1.4; }
.feed-title b { font-weight: 700; }
.feed-meta { font-size: 13px; color: var(--ink-3); margin-top: 2px; display: flex; gap: 9px; flex-wrap: wrap; }
.feed-time { font-size: 12.5px; color: var(--ink-3); font-weight: 600; white-space: nowrap; }

/* ============================================================
   STATUS / BADGES
   ============================================================ */
.status {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 13.5px; font-weight: 700; letter-spacing: 0.01em;
  padding: 3px 9px 3px 7px; border-radius: 999px;
  white-space: nowrap;
}
.status .sdot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.status.s-live { background: var(--pos-tint); color: var(--pos-ink); }
.status.s-live .sdot { background: var(--pos); }
.status.s-transit { background: var(--brand-tint); color: var(--brand-ink); }
.status.s-transit .sdot { background: var(--brand); }
.status.s-pending { background: var(--warn-tint); color: var(--warn-ink); }
.status.s-pending .sdot { background: var(--warn); }
.status.s-delayed { background: var(--danger-tint); color: var(--danger-ink); }
.status.s-delayed .sdot { background: var(--danger); }
.status.s-done { background: var(--bg-soft-2); color: var(--ink-2); }
.status.s-done .sdot { background: var(--ink-3); }
.status.s-cyan { background: var(--cyan-tint); color: var(--cyan-ink); }
.status.s-cyan .sdot { background: var(--accent); }

/* live pulse dot (reusing system pattern) */
.livedot { width: 8px; height: 8px; border-radius: 50%; background: var(--pos); box-shadow: 0 0 0 0 oklch(0.66 0.14 155 / 0.6); animation: pulse 2s infinite; flex: none; }

/* ============================================================
   TABLES
   ============================================================ */
.table-card { padding: 0; overflow: hidden; }
/* position: relative is load-bearing, not decoration: .visually-hidden is
   position:absolute, and a hidden label inside a wide table (the Routes
   list's "confirmed"/"declined" words, the run sheet's per-row button
   suffixes) would otherwise take the page as its containing block and sit
   at its static position past the right edge — outside the wrapper's
   clip, so the document grew sideways on a phone (390px → 843px on a
   route page, 2026-09-22) while everything visible looked fine. */
.table-wrap { position: relative; overflow-x: auto; }
/* The route page's two history tables (courier notifications, SMS) sit
   above the stops since 2026-09-22, so a long log must not push the run
   sheet a screen down: cap them like .feed-list and let them scroll
   (the wrapper is already a keyboard-focusable region). */
.route-history { max-height: 420px; overflow-y: auto; }
/* …and automatically, so a wide table can't be clipped just because a
   template forgot the explicit .table-wrap. */
.card-body:has(> table.tbl),
.table-card:has(table.tbl) { overflow-x: auto; }
table.tbl { width: 100%; border-collapse: collapse; font-size: 15px; }
.tbl thead th {
  text-align: left; font-weight: 700; font-size: 13px; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--ink-3); padding: 6px 10px; border-bottom: 1px solid var(--line);
  background: var(--bg-soft); white-space: nowrap; position: sticky; top: 0; z-index: 1;
}
.tbl thead th.sortable { cursor: pointer; }
.tbl thead th.sortable:hover { color: var(--ink-2); }
.tbl thead th .th-in { display: inline-flex; align-items: center; gap: 5px; }
.tbl thead th .th-in svg { width: 13px; height: 13px; opacity: 0.6; }
.tbl tbody td { padding: 6px 10px; border-bottom: 1px solid var(--line); color: var(--ink-2); vertical-align: middle; }
.tbl tbody tr { transition: background .12s ease; cursor: pointer; }
/* tables whose rows carry explicit action buttons instead of a row link */
.tbl.no-rowlink tbody tr { cursor: default; }
.tbl tbody tr:hover { background: var(--bg-soft); }
.tbl tbody tr:last-child td { border-bottom: none; }
.tbl tbody tr.selected { background: var(--brand-tint); }
.tbl .td-strong { font-weight: 700; color: var(--ink); font-family: var(--font-display); }
.tbl .td-mono { font-variant-numeric: tabular-nums; font-family: var(--font-display); font-weight: 600; color: var(--ink); }
.tbl .td-muted { color: var(--ink-3); }
.cell-id { font-family: var(--font-display); font-weight: 700; color: var(--brand-link); font-size: 14px; }

.cell-user { display: flex; align-items: center; gap: 10px; min-width: 0; }
.cell-user .ava-sm {
  width: 30px; height: 30px; border-radius: 50%; flex: none;
  display: grid; place-items: center; font-weight: 700; font-family: var(--font-display);
  font-size: 13px; background: var(--brand-600); color: #fff;
}
.cell-user .cu-name { font-weight: 700; color: var(--ink); white-space: nowrap; }
.cell-user .cu-sub { font-size: 13.5px; color: var(--ink-3); }

.checkbox {
  width: 17px; height: 17px; border-radius: 5px; border: 1.5px solid var(--line-2);
  background: var(--bg); display: grid; place-items: center; cursor: pointer; flex: none;
  transition: all .15s ease;
}
.checkbox.on { background: var(--brand); border-color: var(--brand); }
.checkbox svg { width: 12px; height: 12px; color: #fff; opacity: 0; }
.checkbox.on svg { opacity: 1; }

.row-act {
  width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center;
  color: var(--ink-3); border: 1px solid transparent; background: transparent;
}
.row-act:hover { background: var(--bg-soft-2); color: var(--ink); border-color: var(--line); }
.row-act svg { width: 16px; height: 16px; }

/* table toolbar */
.tbl-toolbar {
  display: flex; align-items: center; gap: 8px; padding: 7px 10px;
  border-bottom: 1px solid var(--line); flex-wrap: wrap;
}
.filter-chip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 13px; border-radius: 999px; font-size: 14px; font-weight: 600;
  /* --line-2 → --field-line: same sole-boundary fix (a11y audit,
     2026-08-16). */
  border: 1px solid var(--field-line); background: var(--bg); color: var(--ink-2);
  transition: all .15s ease;
}
.filter-chip svg { width: 14px; height: 14px; color: var(--ink-3); }
.filter-chip:hover { border-color: var(--ink-3); color: var(--ink); }
.filter-chip.on { background: var(--brand-tint); border-color: transparent; color: var(--brand-ink); }
.filter-chip.on svg { color: var(--brand-link); }
/* opacity:.7 dragged the already-checked --ink-2/--brand-700 text down to
   ~3.15-3.83:1 (a11y audit, 2026-08-16) — --ink-3 at full opacity reads
   the same "muted" but is pre-checked at 5.1-5.5:1. */
.filter-chip .fc-count { font-family: var(--font-display); font-weight: 700; color: var(--ink-3); }
.filter-chip.on .fc-count { color: var(--brand-ink); }

.mini-search { position: relative; display: flex; align-items: center; }
.mini-search svg { position: absolute; left: 11px; width: 15px; height: 15px; color: var(--ink-3); }
.mini-search input {
  height: 34px; width: 220px; padding: 0 12px 0 33px; border-radius: 999px;
  border: 1px solid var(--field-line); background: var(--bg-soft); font-family: var(--font-body);
  font-size: 14.5px; color: var(--ink); outline: none;
}
.mini-search input:focus {
  border-color: var(--brand); background: var(--bg); box-shadow: 0 0 0 3px var(--brand-tint-2);
  outline: 2px solid var(--focus-ring); outline-offset: 2px;
}

/* table footer / pagination */
.tbl-foot {
  display: flex; align-items: center; gap: 12px; padding: 13px 16px; flex-wrap: wrap;
  border-top: 1px solid var(--line);
}
.tbl-foot .tf-info { font-size: 14px; color: var(--ink-3); }
.pager { margin-left: auto; display: flex; align-items: center; gap: 4px; }
.pager button, .pager a {
  min-width: 32px; height: 32px; padding: 0 8px; border-radius: 8px;
  /* --line-2 → --field-line: same sole-boundary contrast fix as
     tokens.css's .btn-ghost/.pill (a11y audit, 2026-08-16). */
  border: 1px solid var(--field-line); background: var(--bg); color: var(--ink-2);
  font-family: var(--font-display); font-weight: 600; font-size: 14px;
  display: grid; place-items: center;
}
.pager button:hover:not(:disabled), .pager a:hover { border-color: var(--ink-3); color: var(--ink); }
/* --brand under white text measures ~3.6:1 at this weight/size (a11y
   audit, 2026-08-16) — --brand-700 clears 4.5:1. */
.pager button.active, .pager a.active { background: var(--brand-700); border-color: var(--brand-700); color: #fff; }
.pager button:disabled { opacity: 0.4; cursor: not-allowed; }
.pager button svg, .pager a svg { width: 15px; height: 15px; }

/* ============================================================
   PROGRESS / BARS / CHARTS
   ============================================================ */
.track { height: 7px; border-radius: 999px; background: var(--bg-soft-2); overflow: hidden; }
.track .fill { height: 100%; border-radius: 999px; background: var(--brand); }
.track .fill.cyan { background: var(--accent); }
.track .fill.pos { background: var(--pos); }
.track .fill.warn { background: var(--warn); }

/* bar chart */
.bars { display: flex; align-items: flex-end; gap: 7px; height: 180px; padding-top: 10px; overflow-x: auto; }
.bar-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 0; height: 100%; justify-content: flex-end; }
.bar-stack { width: 100%; max-width: 30px; display: flex; flex-direction: column; justify-content: flex-end; gap: 2px; height: 100%; }
/* min-height 0 on purpose: a zero-volume day must draw nothing, not a
   3px sliver that reads as real volume. */
.bar {
  width: 100%; border-radius: 5px 5px 3px 3px; min-height: 0;
  transition: opacity .15s ease; position: relative;
}
.bar.b-brand { background: var(--brand); }
.bar.b-tint { background: var(--brand-tint-2); border-radius: 3px; }
.bar.b-accent { background: var(--accent); }
.bar-col:hover .bar { opacity: 0.82; }
.bar-label { font-size: 12px; font-weight: 600; color: var(--ink-3); white-space: nowrap; }

/* line chart svg */
.linechart { width: 100%; display: block; }
.linechart .grid-l { stroke: var(--line); stroke-width: 1; }
.linechart .axis-lbl { fill: var(--ink-3); font-size: 12px; font-weight: 600; font-family: var(--font-body); }
.linechart .ln { fill: none; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
.linechart .ln.brand { stroke: var(--brand); }
.linechart .ln.accent { stroke: var(--accent); stroke-dasharray: 5 5; }
.linechart .ln-area { stroke: none; }
.linechart .dot-pt { fill: var(--bg); stroke: var(--brand); stroke-width: 2.5; }

/* donut */
.donut-wrap { display: flex; align-items: center; gap: 22px; }
.donut { position: relative; width: 150px; height: 150px; flex: none; }
.donut svg { transform: rotate(-90deg); }
.donut .donut-center {
  position: absolute; inset: 0; display: grid; place-items: center; text-align: center;
}
.donut .dc-val { font-family: var(--font-display); font-weight: 700; font-size: 26px; color: var(--ink); letter-spacing: -0.02em; }
.donut .dc-lbl { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-3); }
.legend-list { display: flex; flex-direction: column; gap: 11px; flex: 1; }
.legend-row { display: flex; align-items: center; gap: 10px; font-size: 14.5px; }
.legend-row .swatch { width: 11px; height: 11px; border-radius: 4px; flex: none; }
.legend-row .lr-name { color: var(--ink-2); font-weight: 600; }
.legend-row .lr-val { margin-left: auto; font-family: var(--font-display); font-weight: 700; color: var(--ink); }
.legend-row .lr-pct { font-size: 13px; color: var(--ink-3); width: 42px; text-align: right; }

/* ============================================================
   COURIER / CUSTOMER CARDS
   ============================================================ */
.tile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); gap: 16px; }
.courier-card {
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--r);
  padding: 18px; box-shadow: var(--sh-sm); cursor: pointer;
  transition: transform .18s ease, box-shadow .22s ease, border-color .2s ease;
}
.courier-card:hover { transform: translateY(-3px); box-shadow: var(--sh-lg); border-color: var(--line-2); }
.cc-top { display: flex; align-items: center; gap: 13px; }
.cc-ava {
  width: 50px; height: 50px; border-radius: 14px; flex: none; position: relative;
  display: grid; place-items: center; font-weight: 700; font-family: var(--font-display);
  font-size: 18px; color: #fff;
}
.cc-ava .pres {
  position: absolute; bottom: -2px; right: -2px; width: 15px; height: 15px; border-radius: 50%;
  border: 2.5px solid var(--bg);
}
.pres.on { background: var(--pos); }
.pres.busy { background: var(--warn); }
.pres.off { background: var(--ink-3); }
.cc-name { font-family: var(--font-display); font-weight: 600; font-size: 17px; color: var(--ink); }
.cc-id { font-size: 13.5px; color: var(--ink-3); font-weight: 600; }
.cc-rating { margin-left: auto; display: inline-flex; align-items: center; gap: 4px; font-family: var(--font-display); font-weight: 700; font-size: 15px; color: var(--ink); }
/* was oklch(0.78 0.14 75), 2.04:1 (a11y audit, 2026-08-16) — the rating
   number itself carries the real information, but the star should still
   clear the 3:1 graphical-object floor. */
.cc-rating svg { width: 15px; height: 15px; color: light-dark(oklch(0.6 0.13 75), oklch(0.82 0.13 80)); }
.cc-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px 10px; margin-top: 16px; padding-top: 15px; border-top: 1px solid var(--line); }
.cc-stat { text-align: center; min-width: 0; }
/* value and label always stack, whichever wrapper they sit in — they used
   to run together ("1routes") when they shared a line. */
.cc-stats .v { display: block; font-family: var(--font-display); font-weight: 700; font-size: 19px; color: var(--ink); line-height: 1.2; }
.cc-stats .l { display: block; font-size: 12px; font-weight: 600; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.03em; margin-top: 2px; }
.cc-foot { margin-top: 15px; display: flex; align-items: center; gap: 10px; }
.cc-load { flex: 1; }
.cc-load .cl-top { display: flex; justify-content: space-between; font-size: 12.5px; font-weight: 600; color: var(--ink-3); margin-bottom: 5px; }

/* ============================================================
   MODAL / DRAWER
   ============================================================ */
.scrim {
  position: fixed; inset: 0; z-index: 90;
  /* deeper on dark: the same 42% navy over an already-dark page barely
     separates the drawer from what's behind it. */
  background: light-dark(oklch(0.22 0.04 257 / 0.42), oklch(0.10 0.03 257 / 0.62));
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
  display: flex; justify-content: flex-end;
  animation: scrimIn .2s ease;
}
.scrim.center { align-items: center; justify-content: center; }
@keyframes scrimIn { from { opacity: 0; } to { opacity: 1; } }
.drawer {
  width: 520px; max-width: 94vw; height: 100%; background: var(--bg);
  border-left: 1px solid var(--line); box-shadow: var(--sh-lg);
  display: flex; flex-direction: column; min-height: 0;
  animation: drawerIn .28s cubic-bezier(.2,.7,.3,1);
  transition: width .28s cubic-bezier(.2,.7,.3,1);
}
/* expanded variant: forms that need room (e.g. order edit, two-up cards) */
.drawer.wide { width: 960px; }
@keyframes drawerIn { from { transform: translateX(40px); opacity: 0.4; } to { transform: none; opacity: 1; } }
.modal-box {
  width: 460px; max-width: 94vw; background: var(--bg); border-radius: var(--r-lg);
  box-shadow: var(--sh-lg); border: 1px solid var(--line); overflow: hidden;
  animation: modalIn .26s cubic-bezier(.2,.7,.3,1);
}
@keyframes modalIn { from { transform: translateY(14px) scale(.98); opacity: 0; } to { transform: none; opacity: 1; } }
.drawer-head, .modal-head {
  display: flex; align-items: center; gap: 14px; padding: 12px 14px;
  border-bottom: 1px solid var(--line); flex: none;
}
.drawer-head h3, .modal-head h3 { font-family: var(--font-display); font-weight: 600; font-size: 19px; letter-spacing: -0.02em; color: var(--ink); }
.drawer-head .dh-sub { font-size: 14px; color: var(--ink-3); margin-top: 2px; }
.modal-body { padding: 14px; }
.drawer-body { flex: 1; min-height: 0; overflow-y: auto; padding: 22px; display: flex; flex-direction: column; gap: 20px; }
.drawer-foot, .modal-foot {
  display: flex; align-items: center; gap: 10px; padding: 10px 14px;
  border-top: 1px solid var(--line); flex: none;
}
.x-btn {
  width: 34px; height: 34px; border-radius: 999px; display: grid; place-items: center;
  /* --line-2 → --field-line: same sole-boundary fix (a11y audit,
     2026-08-16). */
  border: 1px solid var(--field-line); background: var(--bg); color: var(--ink-2); flex: none;
}
.x-btn:hover { border-color: var(--ink-3); color: var(--ink); }
.x-btn svg { width: 17px; height: 17px; }

/* full-page order view: legacy-style action rail + main column */
.order-layout { display: grid; grid-template-columns: 300px minmax(0, 1fr); gap: 18px; align-items: start; }
.order-rail { display: flex; flex-direction: column; gap: 18px; position: sticky; top: 12px; }
.order-main { display: flex; flex-direction: column; gap: 18px; min-width: 0; }
@media (max-width: 1100px) {
  .order-layout { grid-template-columns: 1fr; }
  .order-rail { position: static; }
  .order-ops { grid-template-columns: 1fr; }
}

/* ============================================================
   ORDER DETAIL — TAB BAR
   Server-owned via ?tab=, same as every other filter/sort control in
   this app — plain <a> links, no JS, each tab a full page render.
   ============================================================ */
.order-tabs {
  display: flex; align-items: center; gap: 2px;
  border-bottom: 1px solid var(--line);
  overflow-x: auto;
  /* overflow-y would otherwise compute to auto too, and the active tab's
     underline makes content 1-2px taller than the strip — which rendered
     a stray vertical scrollbar inside the tab bar on every tabbed page. */
  overflow-y: hidden;
}
.order-tabs .tab {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 12px; margin-bottom: -1px;
  font-family: var(--font-body); font-weight: 600; font-size: 15.5px; color: var(--ink-3);
  border-bottom: 2px solid transparent;
  white-space: nowrap;
  transition: color .15s ease, border-color .15s ease;
}
.order-tabs .tab:hover { color: var(--ink); }
.order-tabs .tab.active { color: var(--brand-ink); border-bottom-color: var(--brand); }
.order-tabs .tab .tab-count {
  font-family: var(--font-display); font-weight: 700; font-size: 12px;
  min-width: 18px; text-align: center; padding: 1px 6px; border-radius: 999px;
  background: var(--bg-soft-2); color: var(--ink-3);
}
.order-tabs .tab.active .tab-count { background: var(--brand-tint); color: var(--brand-ink); }

/* detail rows in drawer */
.detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr)); gap: 14px 18px; }
.detail-grid > * { min-width: 0; }
.detail-item .di-label { font-size: 12.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-3); }
.detail-item .di-val { font-size: 15.5px; font-weight: 600; color: var(--ink); margin-top: 4px; }
.detail-item.full { grid-column: 1 / -1; }

.timeline { display: flex; flex-direction: column; gap: 0; }
.tl-item { display: flex; gap: 14px; position: relative; padding-bottom: 20px; }
.tl-item:last-child { padding-bottom: 0; }
.tl-rail { display: flex; flex-direction: column; align-items: center; flex: none; }
.tl-node { width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; flex: none; z-index: 1; }
.tl-node svg { width: 14px; height: 14px; }
/* --pos under the white check icon measured 2.92:1 (a11y audit,
   2026-08-16) — same darkened tone .status.s-live already uses. */
.tl-node.done { background: oklch(0.45 0.12 155); color: #fff; }
.tl-node.current { background: var(--brand); color: #fff; box-shadow: 0 0 0 4px var(--brand-tint-2); }
.tl-node.future { background: var(--bg-soft-2); color: var(--ink-3); border: 1px solid var(--line-2); }
.tl-line { width: 2px; flex: 1; background: var(--line-2); margin: 3px 0; min-height: 18px; }
.tl-item:last-child .tl-line { display: none; }
.tl-body { padding-top: 2px; }
.tl-title { font-size: 15px; font-weight: 700; color: var(--ink); }
.tl-meta { font-size: 13.5px; color: var(--ink-3); margin-top: 1px; }

/* ============================================================
   FORMS (settings)
   ============================================================ */
.field { display: flex; flex-direction: column; gap: 4px; }
.field label { font-size: 14.5px; font-weight: 700; color: var(--ink); }
.hint,
.field .hint { font-size: 14px; color: var(--ink-3); }
/* Groups a run of fields within one card-body — same small-caps
   treatment as .sidebar-section-label (11px/700/uppercase/muted), sized
   for in-form use instead of the sidebar's own padding. First one in a
   card sits flush against the top hint/heading; every other one gets
   its run separated by a .divider (see orders/detail.html's Order
   details tab, the first place this earned its keep — one flat form
   mixing source-tracking/order-identity/customer-assignment/notes with
   nothing telling them apart). */
.field-section-label {
  font-size: 12px; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--ink-3); margin-bottom: 14px;
}
.input, .select, .textarea {
  width: 100%; height: 34px; padding: 0 10px; border-radius: var(--r-xs);
  border: 1px solid var(--field-line); background: var(--bg); font-family: var(--font-body);
  font-size: 15.5px; color: var(--ink); outline: none;
  transition: border-color .18s ease, box-shadow .18s ease;
}
.textarea { height: auto; padding: 11px 14px; resize: vertical; min-height: 90px; line-height: 1.5; }
/* the tinted halo is decoration; the outline is the actual focus indicator */
.input:focus, .select:focus, .textarea:focus {
  border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-tint-2);
  outline: 2px solid var(--focus-ring); outline-offset: 2px;
}
.select { appearance: none; background-image: var(--select-chevron); background-repeat: no-repeat; background-position: right 13px center; padding-right: 38px; }

/* A real checkbox under a styled track, switched by :has() — no JS to
   flip an .on class (never written; grep confirms .toggle rendered
   nowhere yet). Upgraded 2026-08-16, prompted by a Tailwind UI settings
   reference the user shared: its own toggle is the same native-checkbox
   idea, just Tailwind's arbitrary-variant spelling of :has(). This one's
   better fit for this app specifically, not just copied — a real
   checkbox means a boolean setting toggle posts as normal form data
   like every other input here, no data-action wiring required, and it
   degrades to a plain (if unstyled by :has()) checkbox on a browser
   without :has() support rather than becoming inert.
   Markup: <label class="toggle"><input type="checkbox" name="…"
   {{if .X}}checked{{end}}><span class="track"></span></label> */
.toggle { display: inline-flex; position: relative; width: 44px; height: 26px; flex: none; cursor: pointer; }
.toggle input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
.toggle .track {
  display: block; width: 100%; height: 100%; border-radius: 999px; background: var(--line-2);
  transition: background .2s ease;
}
.toggle .track::after {
  content: ""; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%;
  background: light-dark(#fff, oklch(0.93 0.012 255)); box-shadow: var(--sh-sm); transition: transform .2s ease;
}
.toggle:has(input:checked) .track { background: var(--brand); }
.toggle:has(input:checked) .track::after { transform: translateX(18px); }
.toggle:has(input:focus-visible) .track { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

.settings-grid { display: grid; grid-template-columns: 230px 1fr; gap: 28px; align-items: start; }
.settings-nav { display: flex; flex-direction: column; gap: 2px; position: sticky; top: 0; }
/* button, for the design's own client-driven original; a (block, no
   underline) for this app's server-rendered ?tab= links — same visual
   treatment either way, since real navigation here is a link, not a
   client state change. */
.settings-nav button, .settings-nav a {
  text-align: left; padding: 10px 14px; border-radius: var(--r-sm); border: none; background: transparent;
  font-family: var(--font-body); font-weight: 600; font-size: 15px; color: var(--ink-2); cursor: pointer;
  transition: all .15s ease; display: block; text-decoration: none;
}
.settings-nav button:hover, .settings-nav a:hover { background: var(--bg-soft-2); color: var(--ink); }
.settings-nav button.active, .settings-nav a.active { background: var(--brand-tint); color: var(--brand-ink); }

.setting-row {
  display: flex; align-items: center; gap: 18px; padding: 18px 0; border-bottom: 1px solid var(--line);
}
.setting-row:last-child { border-bottom: none; }
.setting-row .sr-main { flex: 1; min-width: 0; }
.setting-row .sr-title { font-size: 15.5px; font-weight: 700; color: var(--ink); }
.setting-row .sr-desc { font-size: 14px; color: var(--ink-2); margin-top: 3px; max-width: 480px; }

/* ============================================================
   MISC HELPERS
   ============================================================ */
.row { display: flex; align-items: center; gap: 10px; }
.col { display: flex; flex-direction: column; }
/* auto-fit + min() so a narrow parent gets one column instead of starving
   the second one; min-width:0 stops a child with its own overflow:hidden
   from zeroing its minimum size. */
.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); gap: 10px; }
.grid-2 > * { min-width: 0; }
.grid-3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); gap: 10px; }
.grid-3 > * { min-width: 0; }
/* Settings' per-tab card sequences (General/Catalog/Routing/Plan) — added
   2026-08-16, named .settings-cards (not .settings-grid — that name was
   already taken, see the design-system's own 230px-sidebar-nav rule
   above, dead until a settings-nav redesign actually uses it; this used
   to just silently override it). Each tab used to stack 2-4 cards full-
   height down the page, every one capped at max-width:480/640px and
   separated by margin-top — correct widths, wrong direction: on any
   screen wider than one narrow card, that's pure unused space beside
   each one instead of a second column. A wider min basis than .grid-2's
   own 280px, since these cards carry tables with 2-3 columns and delete
   buttons that wrap badly narrower than this. align-items:start so a
   tall card (a table with several rows) never stretches its shorter
   row-mate to match. */
.settings-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(380px, 100%), 1fr)); gap: 16px; align-items: start; }
.settings-cards > * { min-width: 0; }
.wrap { flex-wrap: wrap; }
.muted { color: var(--ink-3); }
.tnum { font-variant-numeric: tabular-nums; }
.divider { height: 1px; background: var(--line); }
.empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 60px 20px; text-align: center; color: var(--ink-3); gap: 12px;
}
.empty .em-ic { width: 54px; height: 54px; border-radius: 16px; background: var(--brand-tint); color: var(--brand-link); display: grid; place-items: center; }
.empty .em-ic svg { width: 26px; height: 26px; }

.alloc-bar { display: flex; height: 12px; border-radius: 999px; overflow: hidden; gap: 2px; }
.alloc-bar span { display: block; height: 100%; }

.heat { display: grid; grid-template-columns: repeat(24, 1fr); gap: 3px; }
.heat .cell { aspect-ratio: 1; border-radius: 3px; background: var(--brand-tint); }

/* screen-reader-only: in the accessibility tree, out of the layout */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* skip-to-content: hidden until focused, then a real, visible target */
.skip-link {
  position: absolute;
  left: -9999px; top: 0;
  z-index: 100;
  padding: 10px 16px;
  border-radius: var(--r-sm);
  background: var(--bg);
  color: var(--brand-ink);
  font-size: 15px; font-weight: 700;
  box-shadow: var(--sh-md);
}
.skip-link:focus {
  left: 8px; top: 8px;
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* ---------- Page-level banners ---------- */
.alert {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  width: 100%;
  padding: 12px 14px;
  border-radius: var(--r-sm);
  font-size: 15px;
  font-weight: 600;
  border-left: 3px solid var(--line-2);
  background: var(--bg-soft-2);
  color: var(--ink-2);
}
/* Both body-text tones sit a step darker than the badge inks
   (--danger-ink/--pos-ink), so they stay inline pairs rather than being
   folded into those tokens — see the :root block's own note. */
.alert-danger {
  background: var(--danger-tint);
  border-left-color: var(--danger);
  color: light-dark(oklch(0.45 0.16 22), oklch(0.90 0.09 22));
}
.alert-ok {
  background: var(--pos-tint);
  border-left-color: var(--pos);
  color: light-dark(oklch(0.42 0.12 155), oklch(0.90 0.11 155));
}
.alert-ic { flex: none; width: 20px; height: 20px; margin-top: 1px; }
/* --danger/--pos measure 3.34:1 / 2.55:1 as icon color against their own
   tint (a11y audit, 2026-08-16) — under 3:1 SC 1.4.11 needs for a
   graphical object; same darkened tones as everywhere else in this file. */
.alert-danger .alert-ic { color: var(--danger-ink); }
.alert-ok .alert-ic { color: var(--pos-ink); }
.alert-text { flex: 1; line-height: 1.4; }
.alert-close {
  flex: none;
  width: 22px; height: 22px;
  display: grid; place-items: center;
  border: none; border-radius: var(--r-xs);
  background: none; color: inherit; opacity: 0.6; cursor: pointer;
  transition: opacity 0.15s ease, background 0.15s ease;
}
.alert-close:hover, .alert-close:focus-visible { opacity: 1; background: light-dark(oklch(0 0 0 / 0.06), oklch(1 0 0 / 0.12)); }
.alert-close svg { width: 14px; height: 14px; }

/* #flash rides detached from the page: a floating toast anchored to the
   viewport corner rather than in-flow content, so it never shifts layout
   or gets scrolled away with the page underneath it. Sits below the
   topbar and above everything else the app draws (.sidebar is 40, the
   modal overlay is 90, .skip-link:focus is the previous highest at 100)
   so it's always the topmost thing on screen while visible. The enter/
   leave animation itself is driven by the is-entering/is-leaving classes
   actions.js toggles — see layout.html's own comment on "fragment" for
   why a bare animation on #flash isn't enough once idiomorph is morphing
   this node in place across swaps instead of recreating it. Also named
   its own view-transition group (see this file's own "View transitions"
   section) so it never gets swept into a page-level crossfade mid-swap
   and fights with is-entering/is-leaving over what #flash looks like. */
#flash {
  view-transition-name: flash-toast;
  position: fixed;
  top: calc(var(--topbar-h) + 16px);
  right: 16px;
  z-index: 110;
  width: min(380px, calc(100vw - 32px));
  box-shadow: var(--sh-lg);
}
#flash[hidden] { display: none; }
#flash.is-entering { animation: toast-in 0.28s cubic-bezier(0.16, 1, 0.3, 1); }
#flash.is-leaving { animation: toast-out 0.18s ease-in forwards; }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(-8px) scale(0.98); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes toast-out {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to { opacity: 0; transform: translateY(-6px) scale(0.98); }
}
@media (prefers-reduced-motion: reduce) {
  /* Keep a quick opacity cue instead of killing the animation outright —
     actions.js's dismiss logic waits on "animationend" to actually hide
     the node, so the animation has to still fire, just without the
     motion a reduced-motion user asked not to see. */
  @keyframes toast-in { from { opacity: 0; } to { opacity: 1; } }
  @keyframes toast-out { from { opacity: 1; } to { opacity: 0; } }
}
@media (max-width: 640px) {
  #flash { left: 16px; right: 16px; top: calc(var(--topbar-h) + 12px); width: auto; }
}

/* responsive: collapse sidebar automatically on small screens */
@media (max-width: 1180px) {
  /* .kpi-row needs no override: its auto-fit track list already drops a
     column per breakpoint on its own, down to one on a phone. */
  .dash-grid { grid-template-columns: 1fr; }
}
/* Below 900px the sidebar is a 76px icon rail by DEFAULT, and the topbar
   button opens it as an overlay drawer. Note the class name reads
   backwards down here: the button toggles "nav-collapsed", so at this
   width `.admin.nav-collapsed` means OPENED. Kept that way on purpose so
   the existing markup and its onclick need no change. */
@media (max-width: 900px) {
  .page-title .page-sub { display: none; }
  .page-title .crumb, .page-title .crumb-sep { display: none; }
  .quick-new-btn span { display: none; }
  .admin { grid-template-columns: 76px 1fr; }
  .admin:not(.nav-collapsed) .sidebar .brand span:last-child,
  .admin:not(.nav-collapsed) .sidebar-section-label,
  .admin:not(.nav-collapsed) .nav-item span,
  .admin:not(.nav-collapsed) .nav-item .nav-badge,
  .admin:not(.nav-collapsed) .side-user .su-meta { display: none; }
  .admin:not(.nav-collapsed) .nav-item { justify-content: center; }
  .admin:not(.nav-collapsed) .sidebar-head { justify-content: center; padding: 0; }

  /* Opened, the drawer is position:fixed and therefore out of grid flow, so
     .main becomes the FIRST in-flow item. Without this the content lands in
     the 76px rail column and the whole page squeezes to 76px wide. */
  .admin.nav-collapsed { grid-template-columns: 1fr; }

  /* opened: overlay drawer above the content, every label back */
  .admin.nav-collapsed .sidebar {
    position: fixed;
    inset-block: 0;
    left: 0;
    width: var(--sidebar-w);
    z-index: 80;
    box-shadow: var(--sh-lg);
  }
  .admin.nav-collapsed .sidebar .brand span:last-child,
  .admin.nav-collapsed .sidebar-section-label,
  .admin.nav-collapsed .nav-item span,
  .admin.nav-collapsed .nav-item .nav-badge,
  .admin.nav-collapsed .side-user .su-meta { display: revert; }
  .admin.nav-collapsed .sidebar-head { justify-content: flex-start; padding: 0 18px; }
  .admin.nav-collapsed .nav-item { justify-content: flex-start; padding: 9px 12px; }
  .admin.nav-collapsed .side-user { justify-content: flex-start; }

  .topbar-search { display: none; }
  .settings-grid { grid-template-columns: 1fr; }
  .settings-nav { position: static; flex-direction: row; flex-wrap: wrap; }
}

/* ============================================================
   DISPATCH MAP — real geocoded stops (internal/geocode) on a live
   Leaflet + OpenStreetMap tile basemap, not the decorative
   .map-canvas/.map-truck schematic above (that block was copied in
   with the rest of admin.css at scaffold time but never wired to real
   data — see CLAUDE.md). Reuses .map-panel's card chrome for the same
   rounded/clipped container; everything below is new.
   ============================================================ */
.dispatch-map { position: relative; height: 460px; background: var(--bg-soft); }
/* Leaflet paints its own popup `background: white; color: #333`. On the
   Dispatch map the popup body is OURS (.dispatch-pin-label/-addr below use
   --ink/--ink-3), so under a dark theme that white bubble would get
   near-white lettering on it — the popup has to take the app's surface
   tokens rather than Leaflet's defaults.

   Deliberately NOT scoped to .dispatch-map: the portal's own order map
   (portal/order_detail.html) binds a plain-text popup that would otherwise
   inherit Leaflet's #333 and leave the two maps looking like different
   apps in dark mode. Matching .leaflet-container's specificity is what
   makes it stick — both templates inject leaflet.css into the body, i.e.
   AFTER this file, so a bare .leaflet-popup-content-wrapper would tie on
   specificity and lose on source order.

   The basemap TILES stay exactly as they are, on purpose: they're real
   cartography, not app chrome, and there's no keyless dark tile source to
   switch to (see CLAUDE.md's Maps decision). Leaflet's own zoom/attribution
   controls stay light for the same reason — they sit on the light tiles,
   not on the app's surface. */
.leaflet-container .leaflet-popup-content-wrapper,
.leaflet-container .leaflet-popup-tip {
  background: var(--bg);
  color: var(--ink);
}
.leaflet-container .leaflet-popup-content-wrapper { border-radius: var(--r-sm); box-shadow: var(--sh-md); }
.leaflet-container a.leaflet-popup-close-button { color: var(--ink-3); }
.dispatch-map .leaflet-popup-content { font-family: var(--font-body); font-size: 13.5px; margin: 10px 12px; }
.dispatch-pin-label { font-weight: 700; color: var(--ink); margin-bottom: 2px; }
.dispatch-pin-addr { color: var(--ink-3); margin-bottom: 6px; }
.dispatch-pin-link { color: var(--brand-link); font-weight: 600; text-decoration: none; }
.dispatch-pin-link:hover { text-decoration: underline; }

.dispatch-pin-meta { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.dispatch-pin-time { color: var(--ink-2); font-variant-numeric: tabular-nums; }

/* Pin and line colours (static/stopmap.js). Fixed values rather than
   light-dark() pairs, on purpose: every pin and line sits on the
   OpenStreetMap tiles, which stay light in both themes (see the Theming
   decision — real cartography, no keyless dark tile source), so what a
   white number needs behind it is the same whatever the page around the
   map is doing. Each fill is one this design system already prints a
   white glyph on: --pin-done is .tl-node.done's green, --pin-active
   --brand-700 (.status.s-transit's fill), --pin-failed the light --danger-ink,
   --pin-skipped the light --ink-3, --pin-unassigned --warn-700
   (the light --warn-ink's family) — all ≥ 4.5:1 under #fff. */
/* Stop kind: a pickup is blue, a drop-off is red, everywhere a leg is
   marked (the stop card's C/D badge, the pin's corner tag, the legend,
   the run sheet, the Order page's two Overview cards) — decided
   2026-09-25, see CLAUDE.md's Theming bullet. Two tokens of their own,
   not --brand-700 and --danger: the brand is a design choice that a
   rebrand may move, and red already means "failed"/"declined" in the
   status vocabulary, whereas "blue = pickup" is a rule this console
   states to its users. Fixed values for the same tile reason as the
   pins, and each ≥ 6:1 under the white glyph it carries (blue 6.18:1,
   red 6.12:1, both measured). */
:root {
  --collect: oklch(0.50 0.19 258);
  --deliver: oklch(0.52 0.20 25);
  --pin-done: oklch(0.45 0.12 155);
  --pin-active: var(--brand-700);
  --pin-failed: oklch(0.5 0.16 22);
  --pin-skipped: oklch(0.52 0.02 255);
  --pin-pending: #ffffff;
  --pin-unassigned: var(--warn-700);
  --pin-ahead: oklch(0.42 0.04 255);
}

/* Marker divIcon: a numbered circle coloured by the stop's progress,
   with a small Collection/Delivery tag on its corner — no external
   marker image to vendor (Leaflet's own default relies on shipped PNGs
   this project deliberately doesn't carry). The number is the run
   sheet's own "#" column, so a pin and a row can be matched by eye. An
   unassigned stop (Dispatch's pool) has no place in a run and keeps
   the plain lettered dot. */
.dispatch-pin-dot {
  position: relative; box-sizing: border-box;
  width: 26px; height: 26px; border-radius: 50%;
  display: grid; place-items: center;
  font-family: var(--font-display); font-variant-numeric: tabular-nums;
  color: #fff; font-size: 12px; font-weight: 800; line-height: 1;
  border: 2px solid #fff;
  box-shadow: 0 1px 2px oklch(0 0 0 / 0.28), 0 2px 6px oklch(0 0 0 / 0.18);
}
.dispatch-pin-dot.unassigned { background: var(--pin-unassigned); }
.dispatch-pin-dot.p-done { background: var(--pin-done); }
.dispatch-pin-dot.p-active { background: var(--pin-active); }
.dispatch-pin-dot.p-failed { background: var(--pin-failed); }
.dispatch-pin-dot.p-skipped { background: var(--pin-skipped); }
/* Not started: hollow, so the run reads as "filled in as it goes". The
   ring is the brand fill the active pin uses, the number the light
   theme's --ink — both fixed, for the tile reason above. */
.dispatch-pin-dot.p-pending { background: var(--pin-pending); color: oklch(0.22 0.03 255); border-color: var(--brand-700); }
/* The stop being worked right now: a ring pulsing out from the pin.
   tokens.css's prefers-reduced-motion rule stops the animation on its
   first frame, which leaves the brand fill to mark it on its own. */
.dispatch-pin-dot.p-active::after {
  content: ""; position: absolute; inset: -4px; border-radius: 50%;
  border: 2px solid var(--brand); pointer-events: none;
  animation: stop-pulse 1.8s ease-out infinite;
}
@keyframes stop-pulse { 0% { transform: scale(.85); opacity: .9; } 100% { transform: scale(1.9); opacity: 0; } }
.dispatch-pin-dot .pd-k {
  position: absolute; right: -7px; bottom: -7px;
  width: 14px; height: 14px; border-radius: 4px; box-sizing: border-box;
  display: grid; place-items: center;
  font-family: var(--font-body); font-size: 8.5px; font-weight: 800; color: #fff;
  border: 1.5px solid #fff;
}
.dispatch-pin-dot .pk-collect { background: var(--collect); }
.dispatch-pin-dot .pk-delivery { background: var(--deliver); }

/* The run-order line, one SVG path per segment in Leaflet's overlay
   pane. Leaflet writes its `color` option as a `stroke` attribute; a
   stylesheet rule beats a presentation attribute, so the colour and
   dash live here and follow the pin tokens above. The drop-shadow is a
   thin light casing so the line stays legible over street labels. */
.leaflet-overlay-pane .stop-line {
  stroke-linecap: round; stroke-linejoin: round;
  filter: drop-shadow(0 0 1.5px #fff) drop-shadow(0 0 1.5px #fff);
}
.leaflet-overlay-pane .stop-line.travelled { stroke: var(--pin-done); }
.leaflet-overlay-pane .stop-line.ahead { stroke: var(--pin-ahead); stroke-dasharray: 1 10; }
/* The hover label on a Dispatch line (which route it is) — themed like
   the popup, for the same reason and at the same specificity. */
.leaflet-container .leaflet-tooltip.stop-line-tip {
  background: var(--bg); color: var(--ink); border-color: var(--line);
  font-family: var(--font-body); font-size: 12.5px; font-weight: 700; box-shadow: var(--sh-md);
}
.leaflet-container .leaflet-tooltip-top.stop-line-tip::before { border-top-color: var(--bg); }

/* Placed via L.control({position:"bottomleft"}) — Leaflet's own control
   corner already handles positioning/stacking, so only appearance is set
   here (not position/z-index, which would fight Leaflet's control CSS). */
.dispatch-map-legend {
  background: var(--veil-strong); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 8px 12px;
  display: flex; flex-wrap: wrap; gap: 5px 14px; box-shadow: var(--sh-md); margin: 12px;
}
.dispatch-map-legend .lg-row { display: flex; align-items: center; gap: 7px; font-size: 12.5px; font-weight: 600; color: var(--ink-2); white-space: nowrap; }
.dispatch-map-legend .swatch { width: 11px; height: 11px; border-radius: 50%; flex: none; box-sizing: border-box; }
.dispatch-map-legend .swatch.p-done { background: var(--pin-done); }
.dispatch-map-legend .swatch.p-active { background: var(--pin-active); }
.dispatch-map-legend .swatch.p-failed { background: var(--pin-failed); }
.dispatch-map-legend .swatch.p-skipped { background: var(--pin-skipped); }
.dispatch-map-legend .swatch.p-pending { background: var(--pin-pending); border: 2px solid var(--brand-700); }
.dispatch-map-legend .swatch.p-unassigned { background: var(--pin-unassigned); }
.dispatch-map-legend .swatch.kind-collect { background: var(--collect); }
.dispatch-map-legend .swatch.kind-delivery { background: var(--deliver); }
.dispatch-map-legend .lg-line { width: 18px; height: 0; flex: none; border-top: 3px solid var(--pin-done); border-radius: 2px; }
.dispatch-map-legend .lg-line.ahead { border-top-style: dotted; border-top-color: var(--pin-ahead); }

/* Route detail's "4 of 10 done" beside the map's title — the run
   sheet's own Done tally, drawn as a bar so progress is readable
   before the pins are. The host passes Done/Total/DonePct (see the
   partial's header); Dispatch has several routes on one map and no
   single run to measure, so it gets none. */
.map-meter { display: flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 600; color: var(--ink-2); white-space: nowrap; }
.map-meter .mm-bar { width: 96px; height: 6px; border-radius: 999px; background: var(--bg-soft-2); overflow: hidden; }
.map-meter .mm-bar > span { display: block; height: 100%; background: var(--pin-done); border-radius: inherit; }

/* ============================================================
   STOP CARDS AND DROP ZONES — the shared stop card
   (partials/stop_card.html) as the Routes board and Dispatch's rail
   render it, the Board's own columns, and the drop targets Dispatch's
   drag posts to (the existing MoveOperation endpoint, POST
   /operations/{id}/route — the same one the Board's select-and-Move
   cards use; no client-side state layer). Dispatch's own page layout
   is the DISPATCH PAGE block at the end of this file (2026-09-30).
   ============================================================ */
/* .dispatch-col/.dispatch-col-body are the Board's columns (and the
   map card's own class list); Dispatch's rail no longer uses them —
   see the DISPATCH PAGE block at the end of this file. */
.dispatch-col { display: flex; flex-direction: column; overflow: hidden; }
.dispatch-col-body { max-height: 620px; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 10px; }

/* Drop targets: Dispatch's pool list (drop here = unassign) and each
   route's whole <details>, head included (drop here = assign). */
.dispatch-dropzone { border-radius: var(--r-sm); transition: background-color .12s ease, outline-color .12s ease; outline: 2px dashed transparent; outline-offset: -2px; }
.dispatch-dropzone.drag-over { background: var(--brand-tint); outline-color: var(--brand-300); }
/* While a card is in the air the cards themselves are not targets, so
   a drag over a sibling card lights the zone beneath it rather than
   flickering between the two. */
.dispatch-board.is-dragging .dispatch-stop-card { pointer-events: none; }

.dispatch-stop-card {
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 10px 11px;
  background: var(--bg); display: flex; flex-direction: column; gap: 5px;
}
/* The grab cursor only where a drag actually works: Dispatch marks its
   cards .is-draggable; the Route board renders the same card without it
   (select-and-Move only), and a grab cursor over a card that cannot be
   dragged is a promise the page does not keep. */
.dispatch-stop-card.is-draggable { cursor: grab; }
.dispatch-stop-card.is-draggable:active { cursor: grabbing; }
.dispatch-stop-card.dragging { opacity: .4; }
.dsc-top { display: flex; align-items: center; gap: 8px; }
.dsc-kind {
  flex: none; width: 20px; height: 20px; border-radius: 6px; display: grid; place-items: center;
  font-size: 11.5px; font-weight: 800; color: #fff;
}
/* The stop-kind colours (see :root above): blue pickup, red drop-off.
   Before 2026-09-25 these were --warn-700/--brand-700, the darkened
   tones the a11y audit landed on; the new fills keep that ≥ 4.5:1
   floor under the white glyph. The same badge sits inline in the run
   sheet's Order cell and the Order page's Overview card heads, so a
   leg is the same colour wherever it is named. */
.dsc-kind.dsc-collect { background: var(--collect); }
.dsc-kind.dsc-delivery { background: var(--deliver); }
.card-head .dsc-kind, .tbl .dsc-kind { display: inline-grid; vertical-align: middle; }
.tbl .dsc-kind { width: 18px; height: 18px; font-size: 10.5px; margin-right: 6px; }
/* The badge and the tracking id are one token: "ORD-1006" must not break
   after the hyphen to make room for it in a narrow Order column. */
.tbl .dsc-kind + .td-strong { white-space: nowrap; }
.dsc-label { font-weight: 700; font-size: 14px; color: var(--ink); text-decoration: none; }
.dsc-label:hover { text-decoration: underline; }
.dsc-addr { font-size: 13px; color: var(--ink-3); }

.dispatch-route-card { border: 1px solid var(--line); border-radius: var(--r-sm); overflow: hidden; }
.dispatch-route-head { padding: 10px 12px; background: var(--bg-soft); border-bottom: 1px solid var(--line); }
.dispatch-route-head .dr-name { font-family: var(--font-display); font-weight: 600; font-size: 14.5px; color: var(--ink); text-decoration: none; }
.dispatch-route-head .dr-name:hover { text-decoration: underline; }
.dispatch-route-head .dr-driver { font-size: 12.5px; color: var(--ink-3); margin-top: 1px; }
.dispatch-route-stops { padding: 10px; display: flex; flex-direction: column; gap: 8px; min-height: 54px; }
.dispatch-route-empty { text-align: center; padding: 12px 4px; font-size: 13px; color: var(--ink-3); border: 1px dashed var(--line-2); border-radius: var(--r-xs); }

/* Dispatch: the tap/keyboard move control on each stop card. Dragging is a
   mouse-only shortcut — these are the controls that work everywhere else. */
.dsc-move { display: flex; gap: 6px; margin-top: 8px; }
.dsc-move .select { flex: 1; min-width: 0; font-size: 13px; padding: 6px 26px 6px 9px; }
.dsc-move .btn { flex: none; }

/* A breadcrumb that actually goes somewhere. Only rendered as a link on
   screens below a list (see views.PageData.CrumbHref); the list pages keep
   the plain text, because they are the top of their own section. */
.crumb-link { display: inline-block; text-decoration: none; }
.crumb-link:hover { color: var(--brand-ink); text-decoration: underline; }

/* ============================================================
   TOPBAR POPOVERS (bell, user menu) — first used by the customer
   portal's topbar (customer_layout.html); lives here rather than a
   portal-only stylesheet since it's a general dropdown-panel pattern,
   the same "one shared design system, not staff-specific" reasoning
   tokens.css's own header comment already states. Positioned by a
   plain absolute top/right offset from the topbar (no JS positioning
   library) — toggled by the small inline script in customer_layout.html,
   which also handles outside-click-to-close.
   ============================================================ */
.pop {
  position: absolute; top: 62px; z-index: 80;
  width: 340px; max-width: calc(100vw - 32px);
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-lg, 16px);
  box-shadow: var(--sh-lg); overflow: hidden;
  opacity: 0; transform: translateY(-8px) scale(0.98); pointer-events: none;
  transition: opacity .16s ease, transform .16s ease;
}
.pop.open { opacity: 1; transform: none; pointer-events: auto; }
.pop-head { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--line); }
.pop-head h4 { font-size: 15.5px; font-weight: 600; }
.pop-list { max-height: 360px; overflow-y: auto; }
.pop-empty { padding: 28px 16px; text-align: center; color: var(--ink-3); font-size: 14.5px; }
.pop-foot { padding: 8px; }
.pop-item {
  display: flex; flex-direction: column; gap: 2px;
  padding: 12px 16px; border-bottom: 1px solid var(--line);
  color: inherit; text-decoration: none; transition: background .12s ease;
}
.pop-item:last-child { border-bottom: none; }
.pop-item:hover { background: var(--bg-soft-2); }
.pop-item .pi-subject { font-size: 14.5px; font-weight: 600; color: var(--ink); }
.pop-item .pi-time { font-size: 13px; color: var(--ink-3); }
.pop-account { padding: 14px 16px; border-bottom: 1px solid var(--line); }
.pop-account .pa-name { font-size: 15px; font-weight: 700; color: var(--ink); }
.pop-account .pa-email { font-size: 13.5px; color: var(--ink-3); margin-top: 2px; }
.pop-link {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 10px 12px; border-radius: var(--r-xs, 8px); border: none; background: none;
  font-family: var(--font-body); font-size: 15px; font-weight: 600; color: var(--ink-2);
  text-align: left; cursor: pointer; text-decoration: none;
}
.pop-link:hover { background: var(--bg-soft-2); color: var(--ink); }
.pop-link svg { width: 17px; height: 17px; flex: none; color: var(--ink-3); }

/* ============================================================
   PORTAL BRAND SHELL — customer_layout.html wraps its whole shell in
   .portal, which everything below is scoped under; staff's plain-white
   .sidebar (this file's own definition, above) is completely untouched
   for any page that isn't inside .portal. This reproduces the imported
   Client Panel design's own white-on-blue sidebar (design/
   seagoat-client-panel/panel.css's .side) — the single most visually
   distinctive piece of that design, and the first thing a side-by-side
   screenshot comparison against it flagged as missing (2026-08-17).
   ============================================================ */
.portal .sidebar {
  background: linear-gradient(185deg, var(--brand-600), var(--brand-700));
  border-right: 1px solid var(--brand-700);
}
.portal .sidebar-head { border-bottom: 1px solid oklch(1 0 0 / 0.14); }
.portal .sidebar .brand { color: #fff; }
.portal .sidebar .brand .mark { background: oklch(1 0 0 / 0.16); box-shadow: inset 0 0 0 1px oklch(1 0 0 / 0.2); }
.portal .sidebar-section-label { color: oklch(1 0 0 / 0.55); }
.portal .nav-item { color: oklch(1 0 0 / 0.82); }
.portal .nav-item .nav-ic { color: currentColor; }
.portal .nav-item:hover { background: oklch(1 0 0 / 0.1); color: #fff; }
.portal .nav-item:hover .nav-ic { color: #fff; }
.portal .nav-item.active { background: oklch(1 0 0 / 0.17); color: #fff; box-shadow: inset 0 0 0 1px oklch(1 0 0 / 0.14); }
.portal .nav-item.active .nav-ic { color: #fff; }
.portal .sidebar-foot { border-top: 1px solid oklch(1 0 0 / 0.14); }
.portal .side-user:hover { background: oklch(1 0 0 / 0.1); }
.portal .side-user .su-name { color: #fff; }
.portal .side-user .su-role { color: oklch(1 0 0 / 0.6); }
.portal .side-user .su-chev { color: oklch(1 0 0 / 0.6); }

/* Sidebar CTA (bottom, above sidebar-foot) — "Request a quote" shortcut,
   the design's own side-cta. */
.portal .side-cta {
  display: flex; align-items: center; gap: 12px;
  margin: 10px 14px 14px; padding: 12px 14px; border-radius: var(--r-sm);
  background: oklch(1 0 0 / 0.1); border: 1px solid oklch(1 0 0 / 0.16);
  color: #fff; text-decoration: none;
  transition: background .15s ease, transform .15s ease;
}
.portal .side-cta:hover { background: oklch(1 0 0 / 0.16); transform: translateY(-1px); }
.portal .side-cta .sc-ic {
  width: 34px; height: 34px; border-radius: 10px; flex: none;
  /* --brand-600, not --brand-link: this chip keeps a literal white fill on
     the portal's brand-gradient sidebar (which stays brand-colored in both
     themes, see this section's own note), so its glyph needs the on-white
     tone in both — --brand-link's dark half is a pale blue that would all
     but vanish here. */
  background: #fff; color: var(--brand-600); display: grid; place-items: center;
}
.portal .side-cta .sc-ic svg { width: 18px; height: 18px; }
.portal .side-cta .sc-t { font-family: var(--font-display); font-weight: 600; font-size: 15px; line-height: 1.15; }
.portal .side-cta .sc-s { font-size: 12.5px; color: oklch(1 0 0 / 0.72); margin-top: 1px; }

/* Topbar user avatar — real initials, not a generic icon (the other
   thing the screenshot comparison flagged: portalUserBtn was rendering
   blank). Small enough to sit inline in .topbar-actions next to
   .icon-btn's own 40px circle. */
.topbar-avatar {
  width: 34px; height: 34px; border-radius: 50%; flex: none;
  background: linear-gradient(135deg, var(--brand-300), var(--brand-700));
  color: #fff; display: grid; place-items: center;
  font-family: var(--font-display); font-weight: 700; font-size: 14px;
  border: none; cursor: pointer;
}

/* ============================================================
   DASHBOARD HERO + ACTIVE DELIVERY — PortalDashboard's own
   greeting/CTA row and its "Active delivery" panel. Generic enough
   (eyebrow/hello pattern, two-column dash-grid) that it isn't
   .portal-scoped the way the sidebar/topbar identity is; nothing here
   collides with an existing staff class.
   ============================================================ */
.dash-hello { display: flex; align-items: flex-end; gap: 18px; margin-bottom: 20px; flex-wrap: wrap; }
.dash-hello h2 { font-size: clamp(22px, 3vw, 28px); font-weight: 600; letter-spacing: -0.03em; }
.dash-hello p { color: var(--ink-2); margin-top: 6px; font-size: 15.5px; }
.dash-hello .dh-cta { margin-left: auto; display: flex; gap: 10px; }

.active-delivery { padding: 20px 24px; }
.active-delivery .ad-top { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.active-delivery .ad-ic { width: 46px; height: 46px; border-radius: 13px; flex: none; }
.active-delivery .ad-id { font-size: 13px; font-weight: 700; color: var(--ink-3); letter-spacing: 0.02em; }
.active-delivery .ad-title { font-weight: 600; font-size: 17px; color: var(--ink); margin-top: 2px; }
.active-delivery .ad-route { font-size: 14px; color: var(--ink-2); margin-top: 6px; }

/* ============================================================
   QUOTE WIZARD (PortalQuote) — a client-side stepper (plain JS, one
   page, scoped to itself the same way order_detail.html's map script
   is) over a single real POST at the end. Reproduces the imported
   design's own wizard classes (design/seagoat-client-panel/panel.css)
   directly — this is the piece flagged as flat-out missing in the
   2026-08-17 screenshot comparison, not just under-styled. See
   handlers.CreatePortalQuote's own doc comment for what "estimated
   quote" actually means here (a real Enquiry order + real operations
   rows, an honestly-labeled estimate, never a bill).
   ============================================================ */
.wiz-head { text-align: center; max-width: 620px; margin: 4px auto 26px; }
.wiz-head h2 { font-size: clamp(24px, 3vw, 32px); font-weight: 600; letter-spacing: -0.03em; color: var(--ink); }
.wiz-head p { color: var(--ink-2); margin-top: 10px; font-size: 16.5px; }

.stepper { display: flex; align-items: flex-start; gap: 0; margin: 0 auto 24px; max-width: 760px; }
.stepper .step-cell { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 8px; position: relative; }
.stepper .sc-line { position: absolute; top: 19px; height: 3px; background: var(--line-2); left: -50%; right: 50%; z-index: 0; border-radius: 2px; }
.stepper .step-cell:first-child .sc-line { display: none; }
.stepper .step-cell.done .sc-line, .stepper .step-cell.active .sc-line { background: var(--brand); }
.stepper .sc-num {
  position: relative; z-index: 1;
  width: 38px; height: 38px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--bg-soft-2); color: var(--ink-3);
  font-family: var(--font-display); font-weight: 700; font-size: 16px;
  transition: all .2s ease;
}
.stepper .sc-num svg { width: 16px; height: 16px; }
.stepper .step-cell.active .sc-num { background: var(--brand); color: #fff; box-shadow: var(--sh-brand, 0 8px 20px oklch(0.55 0.14 250 / 0.3)); }
.stepper .step-cell.done .sc-num { background: var(--brand-tint); color: var(--brand-ink); }
.stepper .sc-label { font-size: 13.5px; font-weight: 700; color: var(--ink-3); text-align: center; }
.stepper .step-cell.active .sc-label { color: var(--brand-ink); }
.stepper .step-cell.done .sc-label { color: var(--ink-2); }

.wiz-step-head { display: flex; align-items: center; gap: 14px; padding: 20px 24px; border-bottom: 1px solid var(--line); }
.wiz-step-head .wsh-ic { width: 42px; height: 42px; border-radius: 12px; flex: none; display: grid; place-items: center; background: var(--brand); color: #fff; }
.wiz-step-head .wsh-ic svg { width: 21px; height: 21px; }
.wiz-step-head h3 { font-size: 19px; font-weight: 600; letter-spacing: -0.02em; color: var(--ink); }
.wiz-step-head p { font-size: 14px; color: var(--ink-2); margin-top: 2px; }
.wiz-body { padding: 24px; }
.wiz-foot { display: flex; align-items: center; gap: 12px; padding: 16px 24px; border-top: 1px solid var(--line); background: var(--bg-soft); }
.wiz-foot .wf-step { font-size: 14px; color: var(--ink-3); font-weight: 600; }
.wiz-foot .wf-act { margin-left: auto; display: flex; gap: 10px; }

.qty-row { display: flex; align-items: center; gap: 14px; padding: 13px 0; border-bottom: 1px solid var(--line); }
.qty-row:last-child { border-bottom: none; }
.qty-row .qr-ic { width: 38px; height: 38px; border-radius: 11px; flex: none; display: grid; place-items: center; background: var(--brand-tint); color: var(--brand-link); }
.qty-row .qr-main { flex: 1; }
.qty-row .qr-t { font-size: 15px; font-weight: 600; color: var(--ink); }
.qty-row .qr-s { font-size: 13px; color: var(--ink-3); }
.stepper-num { display: flex; align-items: center; border: 1px solid var(--line-2); border-radius: 999px; overflow: hidden; }
.stepper-num button { width: 32px; height: 32px; border: none; background: var(--bg); color: var(--ink-2); font-size: 18px; display: grid; place-items: center; cursor: pointer; }
.stepper-num button:hover { background: var(--brand-tint); color: var(--brand-link); }
.stepper-num .sn-val { min-width: 34px; text-align: center; font-family: var(--font-display); font-weight: 600; font-size: 15px; color: var(--ink); }

.choice-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.choice {
  border: 1.5px solid var(--line-2); border-radius: var(--r-sm); padding: 14px 16px; cursor: pointer;
  transition: border-color .15s ease, background .15s ease, box-shadow .15s ease; position: relative;
}
.choice:hover { border-color: var(--brand-300); }
.choice.sel { border-color: var(--brand); background: var(--brand-tint); box-shadow: 0 0 0 3px var(--brand-tint-2); }
.choice .ch-check { position: absolute; top: 12px; right: 12px; width: 18px; height: 18px; border-radius: 50%; border: 2px solid var(--line-2); display: grid; place-items: center; }
.choice.sel .ch-check { background: var(--brand); border-color: var(--brand); }
.choice .ch-check svg { display: none; width: 10px; height: 10px; color: #fff; }
.choice.sel .ch-check svg { display: block; }
/* padding-right keeps the title clear of the absolutely-positioned
   .ch-check above it: 12px offset + 18px badge, less the card's own
   16px padding, plus a little air. */
.choice .ch-t { font-family: var(--font-display); font-weight: 600; font-size: 15.5px; color: var(--ink); padding-right: 20px; }
.choice .ch-d { font-size: 13px; color: var(--ink-2); margin-top: 3px; line-height: 1.4; }

.review-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.rev-block { border: 1px solid var(--line); border-radius: var(--r-sm); padding: 14px 16px; }
.rev-block .rb-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.rev-block .rb-head h4 { font-size: 13px; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase; color: var(--ink-3); }
.rev-block .rb-edit { margin-left: auto; font-size: 13px; font-weight: 700; color: var(--brand-link); cursor: pointer; background: none; border: none; }
.rev-block .rb-row { display: flex; justify-content: space-between; gap: 12px; font-size: 14.5px; padding: 3px 0; }
.rev-block .rb-row .k { color: var(--ink-3); }
.rev-block .rb-row .v { color: var(--ink); font-weight: 600; text-align: right; }

.quote-box {
  margin-top: 16px; border-radius: var(--r); overflow: hidden;
  background: linear-gradient(135deg, var(--brand), var(--brand-700)); color: #fff;
  padding: 22px 26px; display: flex; align-items: center; gap: 20px; flex-wrap: wrap;
}
.quote-box .qb-main { flex: 1; min-width: 200px; }
.quote-box .qb-label { font-size: 13px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: oklch(1 0 0 / 0.78); }
.quote-box .qb-val { font-family: var(--font-display); font-weight: 700; font-size: 38px; letter-spacing: -0.03em; margin-top: 4px; }
.quote-box .qb-sub { font-size: 13.5px; color: oklch(1 0 0 / 0.82); margin-top: 4px; }

/* ============================================================
   QUOTE WIZARD — the pieces the 2026-08-28 real-pricing rebuild added.
   Everything above this block predates it and is untouched.

   Three things here are load-bearing rather than decorative:
   the coverage note (a customer has to be able to tell "we run here on
   Thursdays" from "we don't come here at all" at a glance, so the two
   states differ in colour AND wording, never colour alone); the item
   list's fixed max-height (it renders up to 60 search hits, and a list
   that pushes the Continue button off-screen is a list nobody finishes);
   and the price table, which is a real invoice-shaped breakdown, not a
   summary — every line the server charged for has a row.
   ============================================================ */

/* Coverage feedback under a postcode field. */
.cov-note { margin-top: 7px; font-size: 13.5px; line-height: 1.45; padding: 7px 10px; border-radius: var(--r-sm); }
.cov-note.ok { background: var(--pos-tint); color: var(--pos-ink); }
.cov-note.bad { background: var(--danger-tint); color: var(--danger-ink); }
.cov-note strong { font-weight: 700; }

/* Catalogue search + category tabs. */
.cat-search { position: relative; display: flex; align-items: center; }
.cat-search svg { position: absolute; left: 13px; color: var(--ink-3); pointer-events: none; }
.cat-search .input { padding-left: 38px; }
/* The quote wizard's "What's Moving" groups: six rows plus a
   remainder, one open at a time. Replaced a row of 16 category pills
   (.cat-tabs/.cat-tab/.ct-n, deleted rather than left unused) — see
   domain.ItemGroup for why that taxonomy was the wrong question to ask
   a customer. */
.group-list { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
.group { border: 1px solid var(--line); border-radius: var(--r-sm); overflow: hidden; background: var(--bg); }
.group.open { border-color: var(--brand-300); }
.group.has-qty { border-color: var(--brand); }
.group-head {
  width: 100%; display: flex; align-items: center; gap: 13px; padding: 13px 15px;
  background: none; border: none; text-align: left; font-family: inherit; cursor: pointer;
}
.group-head:hover { background: var(--bg-soft); }
.group-head .gh-ic {
  flex: none; width: 36px; height: 36px; border-radius: var(--r-sm);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--brand-tint); color: var(--brand-ink);
}
.group-head .gh-main { flex: 1; min-width: 0; }
.group-head .gh-t { display: block; font-size: 15.5px; font-weight: 600; color: var(--ink); }
.group-head .gh-s { display: block; font-size: 13px; color: var(--ink-3); margin-top: 1px; }
.group-head .gh-sel {
  flex: none; font-size: 12.5px; font-weight: 700; color: var(--brand-ink);
  background: var(--brand-tint-2); border-radius: 999px; padding: 3px 10px;
}
.group-head .gh-n { flex: none; font-size: 13px; font-weight: 600; color: var(--ink-3); }
.group-head .gh-chev { flex: none; color: var(--ink-3); transition: transform .18s ease; }
.group.open .gh-chev { transform: rotate(180deg); }
/* The body reuses .item-list, so it needs no border of its own — the
   group already draws one. */
.group-body.item-list { border: none; border-top: 1px solid var(--line); border-radius: 0; }

/* The item rows themselves. */
.item-list { border: 1px solid var(--line); border-radius: var(--r-sm); max-height: 340px; overflow-y: auto; }
.item-row { display: flex; align-items: center; gap: 14px; padding: 11px 16px; border-bottom: 1px solid var(--line); }
.item-row:last-child { border-bottom: none; }
.item-row.has-qty { background: var(--brand-tint); }
.item-row .ir-main { flex: 1; min-width: 0; }
.item-row .ir-t { font-size: 15px; font-weight: 600; color: var(--ink); }
.item-row .ir-s { font-size: 13px; color: var(--ink-3); margin-top: 1px; }
.item-row .ir-your { color: var(--brand-ink); font-weight: 600; }
.item-empty { padding: 18px 4px; font-size: 14.5px; color: var(--ink-3); text-align: center; }

/* Running tally under the picker. */
.basket-bar {
  margin-top: 14px; display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  padding: 14px 18px; border-radius: var(--r-sm); border: 1px solid var(--line); background: var(--bg-soft);
}
.basket-bar.filled { border-color: var(--brand-300); background: var(--brand-tint); }
.basket-bar .bb-main { flex: 1; min-width: 180px; }
.basket-bar .bb-count { font-family: var(--font-display); font-weight: 600; font-size: 15.5px; color: var(--ink); }
.basket-bar .bb-hint { font-size: 13.5px; color: var(--ink-2); margin-top: 2px; }
.basket-bar .bb-total { font-family: var(--font-display); font-weight: 700; font-size: 22px; letter-spacing: -0.02em; color: var(--ink); }

/* Floor picker — seven options, so a wider grid than .choice-grid's three. */
.level-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
.level-grid .choice { padding: 12px 13px; }
.lift-toggle {
  margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; align-items: center;
  padding: 12px 14px; border: 1px solid var(--line-2); border-radius: var(--r-sm); cursor: pointer;
}
.lift-toggle:hover { border-color: var(--brand-300); }
.lift-toggle input { width: 17px; height: 17px; accent-color: var(--brand); }
.lift-toggle span { font-size: 15px; font-weight: 600; color: var(--ink); }
.lift-toggle .lt-note { grid-column: 2; font-size: 13.5px; font-weight: 400; color: var(--ink-3); }

/* The review step's itemised price. */
.rev-loading { padding: 32px 4px; text-align: center; color: var(--ink-3); font-size: 15px; }
.rev-problem {
  display: flex; align-items: flex-start; gap: 10px; padding: 14px 16px;
  border-radius: var(--r-sm); background: var(--danger-tint); color: var(--danger-ink); font-size: 15px;
}
.rev-problem svg { flex: none; margin-top: 1px; }
.price-table { margin-top: 16px; border: 1px solid var(--line); border-radius: var(--r-sm); overflow: hidden; }
.price-table .pt-head { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-bottom: 1px solid var(--line); background: var(--bg-soft); }
.price-table .pt-head h4 { font-size: 13px; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase; color: var(--ink-3); }
.price-table .pt-head .rb-edit { margin-left: auto; }
.pt-row, .pt-sub {
  display: grid; grid-template-columns: 34px 1fr auto auto; gap: 10px;
  align-items: baseline; padding: 9px 16px; border-bottom: 1px solid var(--line); font-size: 14.5px;
}
.pt-row:last-child, .pt-sub:last-child { border-bottom: none; }
.pt-qty { font-weight: 700; color: var(--ink-3); }
.pt-name { color: var(--ink); }
.pt-unit { font-size: 13px; color: var(--ink-3); text-align: right; }
.pt-amt { font-weight: 700; color: var(--ink); text-align: right; min-width: 78px; }
.pt-amt.pt-free { font-weight: 600; color: var(--ink-3); }
.pt-sub { background: var(--bg-soft); grid-template-columns: 34px 1fr auto; font-weight: 600; }
.pt-sub .pt-name { font-weight: 600; }
.pt-discount .pt-name, .pt-discount .pt-amt { color: var(--pos-ink); }

@media (max-width: 720px) {
  .pt-row, .pt-sub { grid-template-columns: 30px 1fr auto; }
  .pt-unit { display: none; }
}

.wiz-success { text-align: center; padding: 44px 28px; }
.wiz-success .ws-ic { width: 68px; height: 68px; border-radius: 22px; margin: 0 auto 20px; background: var(--pos-tint); color: light-dark(oklch(0.5 0.13 155), oklch(0.86 0.13 155)); display: grid; place-items: center; }
.wiz-success .ws-ic svg { width: 32px; height: 32px; }
.wiz-success h2 { font-size: 24px; font-weight: 600; letter-spacing: -0.03em; }
.wiz-success p { color: var(--ink-2); margin-top: 10px; font-size: 16px; max-width: 440px; margin-inline: auto; }

/* ============================================================
   PORTAL TOAST — one floating, JS-triggered notification, used for
   client-side-only feedback the server never renders a page for (the
   quote wizard's "please fill in every required field"). The design's
   own `window.toast()` had exactly this; this build's first pass
   validated silently (a red field outline, no message) — found missing
   only once a real click-through with a field left empty read as "the
   button doesn't work," not a validation message that was just easy to
   miss. window.portalToast(msg), defined once in customer_layout.html,
   is the one entry point every portal page's own inline script can call.
   ============================================================ */
.portal-toast {
  position: fixed; bottom: 22px; left: 50%; transform: translate(-50%, 20px);
  /* an INVERTED surface, which is what --ink/--bg already are relative to
     each other — so it flips for free instead of needing a token pair
     (a literal #fff label would land on a near-white --ink fill on dark). */
  z-index: 300; background: var(--ink); color: var(--bg);
  padding: 13px 20px; border-radius: 999px; font-size: 15px; font-weight: 600;
  box-shadow: var(--sh-lg); display: flex; align-items: center; gap: 10px;
  opacity: 0; pointer-events: none; transition: opacity .2s ease, transform .2s ease;
  max-width: calc(100vw - 32px);
}
.portal-toast.on { opacity: 1; transform: translate(-50%, 0); }
.portal-toast svg { width: 17px; height: 17px; color: var(--danger); flex: none; }

/* ============================================================
   FIELD VALIDATION STATE — the quote wizard's own client-side
   validateStep() toggles .err on inputs and .invalid on their .field
   wrapper (mirroring the design's own wField/wSelect helpers), but
   until now nothing rendered either state: no red border, no inline
   message — only .portal-toast said anything was wrong at all. Added
   once a real click-through with an empty required field read as "the
   button doesn't work" rather than "you missed something."
   ============================================================ */
.input.err, .textarea.err, .select.err {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px var(--danger-tint);
}
.field-err { font-size: 13px; color: var(--danger); font-weight: 600; margin-top: 2px; display: none; }
.field.invalid .field-err { display: block; }

/* ============================================================
   THEME TOGGLE — the reader-facing half of the light/dark switch
   (internal/web/static/theme.js). Cycles System → Light → Dark, and the
   button shows the state it is IN rather than the one it would move to:
   in "system" neither a sun nor a moon would be true, since the device
   is what's deciding, so that state gets a monitor of its own.

   Hidden until theme.js has stamped data-theme-state on it. With no JS
   the control can do nothing, and a dead button is worse than no button
   — the palette still follows the device in that case, from
   tokens.css's own `color-scheme: light dark`, so nothing is lost but
   the ability to override it. Shape/hover/focus all come from .icon-btn,
   which this is always rendered with (see views.ThemeToggle).
   ============================================================ */
.theme-toggle { display: none; }
.theme-toggle[data-theme-state] { display: grid; }
.theme-toggle [data-theme-icon] { display: none; }
.theme-toggle[data-theme-state="system"] [data-theme-icon="system"],
.theme-toggle[data-theme-state="light"] [data-theme-icon="light"],
.theme-toggle[data-theme-state="dark"] [data-theme-icon="dark"] { display: block; }

/* The standalone pages (both sign-in screens) have no topbar for the
   button to sit in, so it pins to the corner instead. Below .scrim (90)
   and #flash (110) — nothing on those pages draws either, but the class
   shares a stylesheet with pages that do. */
.theme-toggle-float { position: fixed; top: 16px; right: 16px; z-index: 50; }

/* ============================================================
   ORDER DETAIL — STOP CARD (2026-08-29)

   admin.seagoat's OrderViewForm is dense and legible because every
   field wears its meaning: an antd `prefix` icon on each input, a
   coloured button carrying the route/direction/driver, coloured tags
   for the states that decide whether a stop can run. The first port of
   that form to this console arrived as a plain stack of labelled
   inputs, which is the same data and a worse instrument — a dispatcher
   on the phone reads a stop by scanning, not by reading labels.

   This is that density rebuilt in this app's own system: no new hues,
   no new type, only tokens already defined above. Everything is scoped
   under .stop-form / .stop-rail so no other screen sharing .field,
   .input or .status is touched — the same containment rule
   .order-page-lg set for itself.
   ============================================================ */

/* --- the readiness rail ------------------------------------------
   The band under the stop's card head, answering "where does this
   stop stand" before any field is read. Upstream's OriginButtons row
   is the same idea; the chips here read real columns (route,
   is_confirmed, is_locked, completed_at, geocoding) and nothing
   decorative earns a slot. */
.stop-rail {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  /* .card-head's own 12px/16px, so the rail reads as a second band of
     the head rather than an inset block. */
  padding: 12px 16px; border-bottom: 1px solid var(--line); background: var(--bg-soft);
}
.srail {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 13.5px; font-weight: 700; letter-spacing: 0.01em;
  padding: 6px 12px; border-radius: 999px; white-space: nowrap;
  background: var(--bg-soft-2); color: var(--ink-2);
  border: 1px solid transparent;
}
.srail svg { width: 14px; height: 14px; flex: none; }
.srail.t-pos    { background: var(--pos-tint);    color: var(--pos-ink); }
.srail.t-warn   { background: var(--warn-tint);   color: var(--warn-ink); }
.srail.t-danger { background: var(--danger-tint); color: var(--danger-ink); }
.srail.t-brand  { background: var(--brand-tint);  color: var(--brand-ink); }
/* Not-yet states are drawn as an outline, not a fill: an empty slot
   should read as absence, and four filled chips of equal weight would
   make "no driver yet" look like a fact rather than a gap. */
.srail.t-none { background: transparent; border-color: var(--line-2); color: var(--ink-3); font-weight: 600; }
/* matches .status's own step-up on this page — see .order-page-lg above */
.order-page-lg .srail { font-size: 14.5px; }

/* The route chip carries the direction's own colour as a leading bar —
   the one place a tenant's palette appears on this page, exactly as
   the direction badge does upstream. --line-2 is the fallback when a
   direction has no colour set, or set one this app won't vouch for
   (see safeHexColor). */
a.srail { text-decoration: none; }
a.srail:hover { border-color: var(--brand-300); }
.srail .rt-swatch { width: 4px; height: 15px; border-radius: 2px; background: var(--line-2); flex: none; }
.srail .rt-sep { color: var(--ink-3); font-weight: 600; }
.srail .rt-driver { font-weight: 600; color: var(--ink-2); }

/* --- icon-prefixed inputs ----------------------------------------
   antd's `prefix` slot, rebuilt on the existing .input. The glyph is
   presentational only — every field keeps its real <label>, because an
   icon is a landmark for someone scanning and never an accessible
   name. */
.ic-field { position: relative; display: block; }
.ic-field > svg {
  position: absolute; left: 13px; top: 50%; transform: translateY(-50%);
  width: 16px; height: 16px; color: var(--ink-3); pointer-events: none; z-index: 1;
}
.ic-field > .input { padding-left: 38px; }
.ic-field:focus-within > svg { color: var(--brand); }

/* --- section heads -----------------------------------------------
   The existing .field-section-label with its own glyph, so the four
   groups this form is built from (where / access / who / when) are
   findable at a glance rather than by reading small caps. */
.stop-form .sect, .stop-panel .sect {
  display: flex; align-items: center; gap: 8px; margin-bottom: 14px;
  font-size: 12px; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: var(--ink-3);
}
/* Not decoration: admin.css scopes every svg sizing rule to a parent and
   has no global one, so an unmatched <svg viewBox="0 0 24 24"> with no
   width/height renders at the replaced-element default of 300x150px. */
.stop-form .sect svg, .stop-panel .sect svg { width: 14px; height: 14px; }
.order-page-lg .stop-form .sect, .order-page-lg .stop-panel .sect { font-size: 13.5px; }

/* --- segmented control -------------------------------------------
   For the closed sets small enough to show whole: lift (3), crew (5),
   notice (5). A <select> hides four of five options behind a click,
   and these are the values a dispatcher changes most often while
   talking. Longer or wordier vocabularies stay selects — the floor
   picker below is the one exception, and it earns it. */
.seg { display: inline-flex; flex-wrap: wrap; gap: 4px; padding: 4px; border-radius: var(--r-xs); background: var(--bg-soft-2); }
/* The floor strip is the widest of these (eight values); its segments
   sit tighter, and Access gives it the room rather than splitting the
   row evenly with the three-value lift control beside it. */
.seg.seg-tight span { padding: 7px 10px; }
.access-row { display: flex; flex-wrap: wrap; gap: 18px; align-items: flex-start; }
.access-row > .field:first-child { flex: 1 1 460px; }
.access-row > .field + .field { flex: 0 1 auto; }
.seg label { position: relative; display: inline-flex; }
.seg input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.seg span {
  display: inline-flex; align-items: center; gap: 6px; padding: 7px 13px; border-radius: 8px;
  font-size: 14.5px; font-weight: 600; color: var(--ink-2); white-space: nowrap;
  transition: background .15s ease, color .15s ease;
}
.seg span svg { width: 14px; height: 14px; }
.seg label:hover span { background: var(--bg); color: var(--ink); }
.seg label:has(input:checked) span { background: var(--bg); color: var(--ink); font-weight: 700; box-shadow: var(--sh-sm); }
.seg label:has(input:focus-visible) span { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* --- the customer's own words ------------------------------------
   Read-only by design (UpdateStopDetails never writes client_notes),
   so it is set as a quotation rather than a disabled input: a greyed
   field invites a click that will never work. */
.note-quote {
  display: flex; gap: 10px; padding: 12px 14px; border-radius: var(--r-xs);
  background: var(--warn-tint); color: var(--warn-ink); font-size: 15px; line-height: 1.5;
  white-space: pre-wrap;
}
.note-quote svg { width: 16px; height: 16px; flex: none; margin-top: 2px; }

/* --- the two flags -----------------------------------------------
   .setting-row's shape, given the glyph its state deserves: the icon
   swaps with the toggle, so "locked" is readable from the icon alone
   at a glance across the card. */
.stop-form .flag-row { display: flex; align-items: center; gap: 14px; padding: 14px 0; border-bottom: 1px solid var(--line); }
.stop-form .flag-row:last-of-type { border-bottom: none; }
.stop-form .flag-ic {
  width: 36px; height: 36px; border-radius: 10px; flex: none; display: grid; place-items: center;
  background: var(--bg-soft-2); color: var(--ink-3); transition: background .15s ease, color .15s ease;
}
.stop-form .flag-ic svg { width: 17px; height: 17px; }
.stop-form .flag-row:has(input:checked) .flag-ic { background: var(--brand-tint); color: var(--brand-ink); }
.stop-form .flag-main { flex: 1; min-width: 0; }
.stop-form .flag-main label { font-size: 15px; font-weight: 700; color: var(--ink); }

/* --- footer -------------------------------------------------------- */
.stop-form .sf-foot { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 20px; }

@media (max-width: 720px) {
  .stop-rail { padding: 10px 12px; }
  .fl-row { grid-template-columns: 44px 1fr; }
}

/* The Notify tab's stored message. Collapsed to its subject line, since
   a send history is read as a list of events and only occasionally as a
   transcript — and a table row that is forty lines tall stops being a
   row. `pre` because the body was composed with its own line breaks and
   a paragraph reflow would misrepresent what was sent. */
.sent-msg summary { cursor: pointer; color: var(--ink-2); }
.sent-msg summary:hover { color: var(--ink); }
.sent-msg pre {
  margin: 8px 0 2px; padding: 10px 12px; border-radius: var(--r-xs);
  background: var(--bg-soft-2); color: var(--ink-2);
  font-family: var(--font-body); font-size: 14px; line-height: 1.5;
  white-space: pre-wrap; overflow-wrap: anywhere; max-width: 520px;
}

/* ---------- Assistant chat ---------- */
.chat-wrap {
  display: grid;
  grid-template-columns: 280px minmax(0, 1fr);
  gap: 14px;
  height: calc(100dvh - var(--topbar-h) - 118px);
  min-height: 420px;
}
.chat-side { display: flex; flex-direction: column; min-height: 0; }
.thread-list {
  flex: 1; overflow-y: auto; padding: 6px;
  display: flex; flex-direction: column; gap: 2px;
}
.thread-item {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; padding: 9px 10px; border-radius: var(--r-xs);
  color: var(--ink-2); text-decoration: none; font-size: 14.5px;
}
.thread-item:hover { background: var(--bg-soft-2); color: var(--ink); }
.thread-item.active { background: var(--brand-tint); color: var(--brand-ink); }
.ti-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.ti-time { flex: none; font-size: 12.5px; color: var(--ink-3); }
.thread-item.active .ti-time { color: var(--brand-ink); }
.chat-side-foot { padding: 10px; border-top: 1px solid var(--line); }
.chat-main { display: flex; flex-direction: column; min-height: 0; }
.chat-scroll {
  flex: 1; overflow-y: auto; padding: 18px;
  display: flex; flex-direction: column; gap: 12px;
}
.msg { display: flex; }
.msg-body {
  max-width: 72ch; padding: 10px 14px; border-radius: var(--r-sm);
  font-size: 15.5px; line-height: 1.55;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.msg-user { justify-content: flex-end; }
.msg-user .msg-body {
  background: var(--brand-600); color: #fff;
  border-bottom-right-radius: 4px;
}
.msg-assistant .msg-body {
  background: var(--bg-soft-2); border: 1px solid var(--line);
  color: var(--ink); border-bottom-left-radius: 4px;
}
.msg-error .msg-body {
  background: var(--danger-tint); border-color: transparent;
  color: var(--danger-ink);
}
.chat-compose {
  display: flex; align-items: flex-end; gap: 10px;
  padding: 12px 14px; border-top: 1px solid var(--line);
}
.chat-compose .textarea {
  resize: none; height: 42px; min-height: 42px; max-height: 160px;
  padding-top: 10px;
}
/* What the assistant is doing while a tool runs. Replaced by the answer
   as soon as the first text delta arrives. */
.tool-note { color: var(--ink-3); font-style: italic; }

.typing { display: inline-flex; gap: 4px; padding: 4px 0; }
.typing span {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--ink-3);
  animation: chat-typing 1s infinite ease-in-out;
}
.typing span:nth-child(2) { animation-delay: .15s; }
.typing span:nth-child(3) { animation-delay: .3s; }
@keyframes chat-typing {
  0%, 60%, 100% { opacity: .3; transform: translateY(0); }
  30% { opacity: 1; transform: translateY(-3px); }
}
@media (prefers-reduced-motion: reduce) { .typing span { animation: none; } }
@media (max-width: 900px) {
  .chat-wrap { grid-template-columns: 1fr; height: auto; }
  .chat-side { max-height: 220px; }
  .chat-main { height: 62dvh; }
}

/* ============================================================
   ROUTES REWORK (2026-09-22 — docs/superpowers/specs/2026-09-22-routes-
   ui-design.md). The four Routes tabs became one day-scoped workspace
   built from shared partials (views/partials/route_*.html, stop_*.html);
   these are the few rules those partials and the rebuilt screens need
   that the house components above did not already have.
   ============================================================ */

/* The day-nav (partials/route_day_nav.html): ‹ Today › + date + Apply
   in one toolbar row. .pager's margin-left:auto is for a table footer,
   where it sits at the far right; here it leads the row. */
.day-nav { gap: 7px; align-items: center; }
.day-nav .pager { margin-left: 0; }

/* Row actions (partials/stop_actions.html): forms that sit inline
   beside a badge, and the cell that groups a stop's two of them. */
.inline-form { display: inline; }
.stop-actions { display: flex; gap: 6px; justify-content: flex-end; flex-wrap: wrap; }

/* The Route board: a wrapping row of .dispatch-col cards that grow to a
   readable width, stack at phone width and never stretch to fill the
   tallest column's height — not the 280px-fixed, horizontally-scrolling
   strip it replaced. */
.route-board { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-start; }
.route-board > .card { flex: 1 1 320px; max-width: 520px; min-width: 0; }
.board-body { padding: 12px; }
/* The count pill a Stops head wears — the Board's Unassigned column and
   Dispatch's Stops section, one class (2026-09-30; before this Dispatch
   set the colour inline with the light half of --warn-ink, which never
   flipped in dark and measured 2.0:1). --warn-ink is a light-dark()
   pair: 5.31:1 light, 7.93:1 dark on --warn-tint. */
.count-pill {
  font-size: 12px; font-weight: 700; font-family: var(--font-display);
  padding: 2px 8px; border-radius: 999px; line-height: 1.5;
  background: var(--warn-tint); color: var(--warn-ink);
}

/* Two optional lines on the shared stop card (partials/stop_card.html)
   that the board fills and Dispatch leaves empty: the stop's sequence
   number, first in .dsc-top, and its time, pushed to the right. */
.dsc-seq { font-family: var(--font-display); font-variant-numeric: tabular-nums; font-size: 12px; color: var(--ink-3); min-width: 1.5em; }
.dsc-time { margin-left: auto; font-size: 13px; color: var(--ink-2); font-variant-numeric: tabular-nums; }

/* A second, quieter line under a cell's main value (the order's type
   under its number, the contact under the address). */
.td-sub { display: block; font-size: 13px; color: var(--ink-3); margin-top: 2px; }

/* A group header row inside a table (Confirmations groups stops under
   their route). */
.tr-group td { background: var(--bg-soft); font-size: 13.5px; }

/* The tab strip scrolls horizontally on a phone; fade its right edge so
   the fifth tab reads as "more this way" rather than as the end. */
.order-tabs { mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent); padding-right: 24px; }

/* Below 900px the title may truncate — see .page-title's own comment. */
@media (max-width: 900px) {
  .page-title h1 { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
}

/* ============================================================
   ROUTE DETAIL — the run sheet (redesigned 2026-09-26; see
   views/routes/detail.html's header). One swap region holds a summary
   strip, two compact contact cards, the stops as a spined list and the
   map; the rail's form moves last on a phone.
   ============================================================ */

/* Below the two-column breakpoint the rail follows the main column:
   on a phone the run is the first screen, not the edit form. Scoped by
   a modifier so the Order page's rail keeps its own order. */
@media (max-width: 1100px) {
  .order-layout.rail-last .order-rail { order: 2; }
}

/* The summary strip: how far the run has got. The count, a segmented
   bar in the map's own pin fills, its legend, and the answer chips. */
.run-summary { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 24px; padding: 12px 16px; }
.run-summary .rs-ident { display: none; width: 100%; align-items: center; flex-wrap: wrap; gap: 8px 10px; font-size: 14px; font-weight: 600; color: var(--ink-2); }
.run-summary .rs-ident a { color: inherit; }
@media (max-width: 900px) { .run-summary .rs-ident { display: flex; } }
.rs-count { display: flex; align-items: baseline; gap: 7px; white-space: nowrap; }
.rs-num { font-family: var(--font-display); font-weight: 600; font-size: 32px; letter-spacing: -0.03em; line-height: 1; color: var(--ink); font-variant-numeric: tabular-nums; }
.rs-of { font-size: 14px; font-weight: 600; color: var(--ink-3); }
.rs-progress { flex: 1 1 220px; min-width: 0; }
.rs-bar { display: flex; gap: 2px; height: 8px; border-radius: 999px; background: var(--bg-soft-2); overflow: hidden; }
.rs-bar .seg { display: block; height: 100%; min-width: 3px; }
.rs-bar .seg.done { background: var(--pin-done); }
.rs-bar .seg.active { background: var(--pin-active); }
.rs-bar .seg.failed { background: var(--pin-failed); }
.rs-bar .seg.skipped { background: var(--pin-skipped); }
.rs-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 7px; font-size: 12.5px; font-weight: 600; color: var(--ink-3); }
.rs-legend span { display: inline-flex; align-items: center; gap: 6px; }
.rs-legend .sw { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.rs-legend .sw.done { background: var(--pin-done); }
.rs-legend .sw.active { background: var(--pin-active); }
.rs-legend .sw.failed { background: var(--pin-failed); }
.rs-legend .sw.skipped { background: var(--pin-skipped); }
.rs-legend .sw.togo { background: var(--bg); border: 2px solid var(--line-2); box-sizing: border-box; }
.rs-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-left: auto; }
@media (max-width: 640px) { .rs-chips { margin-left: 0; } }

/* Card heads on this page wrap: a title beside a sentence-long
   subtitle (and the map's meter) has to fold at a phone's width rather
   than break the title into two words. */
.contact-row .card-head, .run-card .card-head, .map-panel .card-head { flex-wrap: wrap; }
.contact-row .card-head h3, .run-card .card-head h3, .map-panel .card-head h3 { white-space: nowrap; }
@media (max-width: 640px) {
  .contact-row .card-head .ch-sub, .map-panel .card-head .ch-sub { flex-basis: 100%; order: 3; }
  .map-panel .card-head .map-meter { order: 2; margin-left: auto; }
}

/* The two contact cards: one inline form each, and the history behind a
   disclosure — a 300px column cannot hold a five-column table, and the
   history is read after the button is pressed, not before. */
.contact-form { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; }
.contact-form .select { flex: 1 1 180px; min-width: 0; }
.contact-form .hint { flex-basis: 100%; }
.history { border-top: 1px solid var(--line); }
.history > summary {
  display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none;
  padding: 8px 10px; font-size: 13.5px; font-weight: 700; color: var(--ink-2);
}
.history > summary::-webkit-details-marker { display: none; }
.history > summary::before {
  content: ""; width: 7px; height: 7px; flex: none; margin-left: 2px;
  border-right: 2px solid var(--ink-3); border-bottom: 2px solid var(--ink-3);
  transform: rotate(-45deg); transition: transform .15s ease;
}
.history[open] > summary::before { transform: rotate(45deg); }
.history > summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }
.hist-n { font-family: var(--font-display); font-size: 12px; padding: 1px 8px; border-radius: 999px; background: var(--bg-soft-2); color: var(--ink-2); }
.hist-list { list-style: none; margin: 0; padding: 0 10px 8px; max-height: 300px; overflow-y: auto; }
.hist-item {
  display: grid; grid-template-columns: 118px minmax(0, 1fr) auto; gap: 4px 10px; align-items: start;
  padding: 7px 0; border-top: 1px solid var(--line); font-size: 13.5px; color: var(--ink-2);
}
.hist-item:first-child { border-top: 0; }
.hi-when { color: var(--ink-3); font-variant-numeric: tabular-nums; }
.hi-what { font-weight: 700; color: var(--ink); }
.hi-to { overflow-wrap: anywhere; }
.hi-sub, .hi-reason { display: block; font-size: 13px; color: var(--ink-3); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 260px; }
.hi-status { justify-self: end; text-align: right; }
.hi-status .hi-reason { white-space: normal; max-width: 180px; }
@media (max-width: 640px) {
  .hist-item { grid-template-columns: minmax(0, 1fr) auto; }
  .hi-when { grid-column: 1 / -1; }
}

/* The stops, as a run: an ordered list whose spine carries the same
   numbered, progress-coloured node the map's pins use (the --pin-*
   fills above), so the sheet and the map read as one drawing. */
.run { list-style: none; margin: 0; padding: 0; }
.run-stop {
  position: relative; display: grid; grid-template-columns: 36px 84px minmax(0, 1fr) auto;
  gap: 4px 14px; align-items: start; padding: 12px 12px 12px 10px; border-top: 1px solid var(--line);
}
.run-stop:first-child { border-top: 0; }
/* the spine: one line down the column of nodes, stopping at the ends */
.run-stop::before { content: ""; position: absolute; left: 27px; top: 0; bottom: 0; width: 2px; background: var(--line-2); }
.run-stop:first-child::before { top: 26px; }
.run-stop:last-child::before { bottom: calc(100% - 26px); }
.run-stop:only-child::before { display: none; }
/* The sequence column: the node, and beneath it the two Move buttons
   (route_sequence.go). The column has its own surface so the spine
   passes behind the buttons rather than through them. */
.rs-seq { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 3px; }
.rs-node[draggable="true"] { cursor: grab; }
.rs-node[draggable="true"]:active { cursor: grabbing; }
/* 24px tall each with 2px between (WCAG 2.5.8's floor): on a phone,
   where a native drag never starts, these two are the whole feature. */
.rs-nudge { display: flex; flex-direction: column; gap: 2px; background: var(--bg); border-radius: 6px; padding: 1px; }
.rs-nudge-btn {
  width: 28px; height: 24px; padding: 0; border: 0; border-radius: 6px; cursor: pointer;
  display: grid; place-items: center; background: transparent; color: var(--ink-3);
}
.rs-nudge-btn svg { width: 14px; height: 14px; }
.rs-nudge-btn.up svg { transform: rotate(-90deg); }
.rs-nudge-btn.down svg { transform: rotate(90deg); }
.rs-nudge-btn:hover { background: var(--bg-soft-2); color: var(--ink); }
.rs-nudge-btn:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }
.rs-nudge-btn:disabled { opacity: .3; cursor: default; background: transparent; }
/* While a number is being dragged: the row it came from fades, the row
   under the pointer shows where it will land. */
.run-stop.dragging { opacity: .45; }
.run-stop.drop-before { box-shadow: inset 0 3px 0 0 var(--brand); }
.run-stop.drop-after { box-shadow: inset 0 -3px 0 0 var(--brand); }
.rs-node {
  position: relative; z-index: 1; box-sizing: border-box; margin: 1px 0 0 0;
  width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center;
  font-family: var(--font-display); font-weight: 800; font-size: 12.5px; font-variant-numeric: tabular-nums; line-height: 1;
  color: #fff; border: 2px solid var(--bg);
  box-shadow: 0 0 0 1px var(--line-2);
}
.run-stop.p-done .rs-node { background: var(--pin-done); }
.run-stop.p-active .rs-node { background: var(--pin-active); }
.run-stop.p-failed .rs-node { background: var(--pin-failed); }
.run-stop.p-skipped .rs-node { background: var(--pin-skipped); }
/* Not started: hollow, on the app's own surface (this is chrome, not
   the map's tiles, so the fill AND the ring follow the theme —
   --focus-ring is the light-dark() pair every former --brand-700
   outline became when the dark theme landed; the literal would be
   2.6:1 on a dark card). */
.run-stop.p-pending .rs-node { background: var(--bg); color: var(--ink); box-shadow: 0 0 0 2px var(--focus-ring); border-color: var(--bg); }
/* The stop being worked right now: the map's pulse, here too. */
.run-stop.p-active .rs-node::after {
  content: ""; position: absolute; inset: -5px; border-radius: 50%;
  border: 2px solid var(--brand); pointer-events: none;
  animation: stop-pulse 1.8s ease-out infinite;
}
.rs-time { display: flex; flex-direction: column; padding-top: 4px; min-width: 0; }
.rs-t { font-family: var(--font-display); font-weight: 600; font-size: 16.5px; letter-spacing: -0.01em; color: var(--ink); font-variant-numeric: tabular-nums; white-space: nowrap; }
.rs-t.rs-none { color: var(--ink-3); font-weight: 500; }
.rs-tsub { font-size: 12.5px; color: var(--ink-3); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rs-body { min-width: 0; padding-top: 2px; }
.rs-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
.rs-head .dsc-kind { width: 18px; height: 18px; font-size: 10.5px; }
.rs-order { font-family: var(--font-display); font-weight: 700; font-size: 15px; color: var(--ink); text-decoration: none; white-space: nowrap; }
.rs-order:hover { text-decoration: underline; }
.rs-type { font-size: 13.5px; color: var(--ink-3); }
.rs-badges { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.rs-badges:empty { display: none; }
.rs-badges .status { font-size: 12.5px; padding: 2px 8px 2px 6px; }
a.rs-leg { text-decoration: none; }
a.rs-leg:hover { text-decoration: underline; }
.rs-addr { font-size: 15px; color: var(--ink); margin-top: 4px; line-height: 1.4; }
.rs-contact { font-size: 13.5px; color: var(--ink-3); margin-top: 2px; }
.rs-contact a { color: inherit; }
.rs-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; align-self: start; padding-top: 1px; }
@media (max-width: 760px) {
  .run-stop { grid-template-columns: 36px minmax(0, 1fr); gap: 6px 12px; }
  .rs-time { grid-column: 2; flex-direction: row; align-items: baseline; gap: 8px; padding-top: 3px; }
  .rs-tsub { margin-top: 0; }
  .rs-body { grid-column: 2; }
  .rs-actions { grid-column: 2; justify-content: flex-start; margin-top: 2px; }
  .rs-actions .btn.btn-sm { padding: 7px 12px; }
}

/* Route tags (route_tags.go). A .tag-chip is a tag as read — on the
   strip, on the Routes list under the route's name; a .tag-toggle is
   the same tag as a choice in the rail's card: a .filter-chip wrapped
   round a real checkbox, so the keyboard and a screen reader get the
   control and the eye gets the chip. */
.tag-chip {
  display: inline-flex; align-items: center; padding: 2px 9px; border-radius: 999px;
  font-size: 12.5px; font-weight: 600; background: var(--bg-soft-2); color: var(--ink-2);
  border: 1px solid var(--line); white-space: nowrap;
}
.tag-row { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
.tag-toggles { display: flex; flex-wrap: wrap; gap: 6px; border: 0; padding: 0; margin: 0; min-width: 0; }
.tag-toggle { cursor: pointer; position: relative; user-select: none; }
.tag-toggle input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }
.tag-toggle:has(> input:checked) { background: var(--brand-tint); border-color: transparent; color: var(--brand-ink); }
.tag-toggle:has(> input:focus-visible) { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* The map head's warning about a pin the fit left out. */
.map-warn { color: var(--warn-ink); font-weight: 600; }
/* A pin flagOutliers ringed: the map's own warning tone around it. */
.dispatch-pin-dot.far { box-shadow: 0 0 0 3px var(--warn), 0 1px 2px oklch(0 0 0 / 0.28); }
.dispatch-map-legend .swatch.far { background: var(--bg); box-shadow: 0 0 0 2px var(--warn); box-sizing: border-box; }
.dispatch-pin-far { color: var(--warn-ink); font-weight: 600; margin-bottom: 6px; }

/* The Route calendar's own month-nav .pager sits directly in a
   .tbl-toolbar, not inside a .day-nav form — same fix as .day-nav
   .pager above, so it leads the toolbar row instead of .pager's own
   margin-left:auto (meant for a .tbl-foot) shoving it to the right. */
.tbl-toolbar > .pager { margin-left: 0; }

/* The Route calendar (partials none — routes/calendar.html owns this
   whole block): a real 7-column month grid, styled by class so no cell
   or pill carries an inline style=. */
.cal-body { padding: 0; }
.cal { table-layout: fixed; width: 100%; }
/* Both scoped under .cal (0,2,0), not the bare class (0,1,0) alone: the
   calendar's <td> is inside a table.tbl, so .tbl tbody td { … } (0,1,2)
   would otherwise win and these two never apply — the same
   author-rule-vs-specificity trap CLAUDE.md records for quote.html. */
.cal .cal-cell { vertical-align: top; min-height: 120px; height: 120px; padding: 6px 8px; }
.cal .cal-out { color: var(--ink-3); }
.cal-today { background: var(--brand-tint); }
.cal-day { display: block; font-weight: 700; color: inherit; text-decoration: none; margin-bottom: 6px; }
.cal-today .cal-day { font-weight: 800; }
.cal-pill {
  display: flex; align-items: center; gap: 6px; font-size: 12px;
  padding: 3px 6px; margin-bottom: 3px; border-radius: 4px;
  color: inherit; text-decoration: none; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; min-width: 0;
  background: var(--bg-soft);
}
.cal-pill .sdot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
/* Tinted from the same tokens .status.s-* uses (admin.css's own status
   family, above) — a route's calendar pill and its list badge always
   agree on colour. */
.cal-pill.s-live { background: var(--pos-tint); color: var(--pos-ink); }
.cal-pill.s-live .sdot { background: var(--pos); }
.cal-pill.s-transit { background: var(--brand-tint); color: var(--brand-ink); }
.cal-pill.s-transit .sdot { background: var(--brand); }
.cal-pill.s-pending { background: var(--warn-tint); color: var(--warn-ink); }
.cal-pill.s-pending .sdot { background: var(--warn); }
.cal-pill.s-delayed { background: var(--danger-tint); color: var(--danger-ink); }
.cal-pill.s-delayed .sdot { background: var(--danger); }
.cal-pill.s-done { background: var(--bg-soft-2); color: var(--ink-2); opacity: .6; }
.cal-pill.s-done .sdot { background: var(--ink-3); }
.cp-extra { overflow: hidden; text-overflow: ellipsis; }
.cal-more { display: block; font-size: 12px; color: var(--ink-3); text-decoration: none; }
.cal-more:hover { color: var(--ink-2); text-decoration: underline; }
@media (max-width: 640px) {
  .cal-pill .cp-extra { display: none; }
}

/* Order page presence (2026-09-28): who else has this order open, and
   the refresh modal. .scrim sets display, which beats the [hidden]
   attribute's UA rule, so the modal hides itself explicitly. */
.presence-slot:empty { display: none; }
.presence-line {
  display: flex; align-items: center; gap: 8px; margin: 0 0 12px;
  font-size: 13.5px; color: var(--ink-2);
}
.presence-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--pos); flex: none; }
#stale-modal[hidden] { display: none; }
#stale-modal .modal-box { max-width: 440px; }
#stale-modal p { margin: 0 0 16px; color: var(--ink-2); }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }

/* ============================================================
   DISPATCH PAGE — one day's map with one rail beside it (redesigned
   2026-09-30; see CLAUDE.md's Dispatch bullet and docs/superpowers/
   specs/2026-09-30-dispatch-redesign-design.md). The page is a flex
   column sized to the viewport; the board takes what the toolbar
   leaves; nothing scrolls but the rail's two lists and the map.
   Everything here is scoped under .dispatch-page/.dispatch-board/
   .dispatch-rail so the Board's columns and Route detail's map keep
   their own sizes.
   ============================================================ */
.dispatch-page { height: calc(100dvh - var(--topbar-h) - 24px); min-height: 640px; }
.dispatch-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; flex: none; }
.dispatch-toolbar .spacer { flex: 1; }
/* The day-nav on one row: its hub select at a natural width, not the
   full-width every .select takes inside a form. */
.dispatch-toolbar .day-nav { margin: 0; flex: 1 1 auto; }
.dispatch-toolbar .day-nav > .select { width: auto; flex: 0 1 220px; min-width: 140px; }
.dispatch-links { display: flex; flex-wrap: wrap; gap: 6px; }
/* The phone panes' tab strip: .order-tabs styles its <a>s; these are
   <button>s, so the UA chrome is reset and the selected pane is marked
   the way the Order page marks its active tab (review, 2026-09-30:
   the unstyled buttons measured 1.99:1 in dark). */
.dispatch-tabs { display: none; flex: none; }
.dispatch-tabs .tab { appearance: none; background: none; border: 0; border-bottom: 2px solid transparent; border-radius: 0; cursor: pointer; color: var(--ink-3); }
.dispatch-tabs .tab[aria-selected="true"] { color: var(--brand-ink); border-bottom-color: var(--brand); }
.dispatch-tabs .tab[aria-selected="true"] .tab-count { background: var(--brand-tint); color: var(--brand-ink); }
.dispatch-board {
  display: grid; grid-template-columns: minmax(340px, 400px) minmax(0, 1fr);
  gap: 12px; flex: 1 1 auto; min-height: 0;
}

/* The rail: strip, then Stops (grows), then Routes (capped at half). */
.dispatch-rail { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
.dispatch-rail-body { display: flex; flex-direction: column; min-height: 0; height: 100%; position: relative; }
.dispatch-summary { flex: none; border-bottom: 1px solid var(--line); padding: 10px 14px; gap: 8px 16px; }
.dispatch-summary .rs-num { font-size: 28px; }
.dispatch-summary .rs-pool { margin-left: 0; }
.dispatch-summary .rs-pool .status { text-decoration: none; }
.dispatch-summary .rs-progress { flex-basis: 100%; }
.rail-section { display: flex; flex-direction: column; min-height: 0; }
.rail-pool { flex: 1 1 0; min-height: 240px; }
/* Routes take their own height up to half the rail — the pool keeps
   the rest — and their list scrolls inside that; an open route's stops
   grow the section to the cap, never past it. */
.rail-routes { flex: 0 0 auto; max-height: 50%; border-top: 1px solid var(--line); }
.rail-section > .card-head { flex: none; padding: 8px 14px; }
.rail-list { flex: 1 1 0; min-height: 0; overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 6px; }
.rail-routes .rail-list { flex: 1 1 auto; }
.rail-empty { display: flex; flex-direction: column; gap: 10px; align-items: center; padding: 20px 8px; }

/* Pool segments: the run sheet's own disclosure, with a sticky name. */
.rail-list .pool-seg { border-top: 0; }
.rail-list .pool-seg > summary { position: sticky; top: 0; z-index: 1; background: var(--bg); padding: 6px 4px; }
.pool-cards { display: flex; flex-direction: column; gap: 6px; padding: 2px 0 8px; }
.seg-hint { font-size: 12.5px; color: var(--ink-3); padding: 4px 6px; }

/* Routes: one details each, the whole element a drop zone. */
.dr { border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--bg); }
.dr > summary { list-style: none; cursor: pointer; }
.dr > summary::-webkit-details-marker { display: none; }
.dr-head {
  display: grid; grid-template-columns: 14px minmax(0, 1fr); column-gap: 8px; row-gap: 2px;
  align-items: center; padding: 8px 10px; background: var(--bg-soft); border-radius: var(--r-sm);
}
.dr-head:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }
.dr[open] > .dr-head { border-radius: var(--r-sm) var(--r-sm) 0 0; border-bottom: 1px solid var(--line); }
.dr-swatch { width: 14px; height: 14px; border-radius: 50%; border: 3px solid var(--rt); box-sizing: border-box; }
.dr-title { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.dr-title .status { font-size: 12px; padding: 1px 7px; }
.dr-name { font-family: var(--font-display); font-weight: 600; font-size: 14.5px; color: var(--ink); }
.dr-sub { grid-column: 2; font-size: 12.5px; color: var(--ink-3); }
.dr-full { color: var(--danger-ink); font-weight: 700; }
.dr-head .rs-bar.thin { grid-column: 2; height: 4px; margin-top: 3px; }
.dr[data-closed] .dr-name { color: var(--ink-3); }
.dr-body { padding: 8px; display: flex; flex-direction: column; gap: 6px; }
.dr-open { font-size: 13px; }

/* Cards in the rail: tighter than the Board's, one address line, and
   the sequence number drawn as the run sheet's own node in the pin
   fills so the rail and the map are one drawing. */
.dispatch-rail .dispatch-stop-card { padding: 8px 10px; gap: 3px; }
.dispatch-rail .dsc-addr { display: flex; gap: 4px; white-space: nowrap; min-width: 0; }
.dispatch-rail .dsc-addr-text { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.dispatch-rail .dsc-nopin { flex: none; }
.dispatch-rail .dsc-time { margin-left: auto; white-space: nowrap; }
.dispatch-rail .dsc-seq {
  width: 20px; height: 20px; border-radius: 50%; min-width: 0; flex: none;
  display: grid; place-items: center; font-size: 11px; font-weight: 700; line-height: 1;
  background: var(--bg); color: var(--ink); box-shadow: 0 0 0 2px var(--focus-ring); box-sizing: border-box;
}
.dispatch-rail .dispatch-stop-card.p-done .dsc-seq { background: var(--pin-done); color: #fff; box-shadow: none; }
.dispatch-rail .dispatch-stop-card.p-active .dsc-seq { background: var(--pin-active); color: #fff; box-shadow: none; }
.dispatch-rail .dispatch-stop-card.p-failed .dsc-seq { background: var(--pin-failed); color: #fff; box-shadow: none; }
.dispatch-rail .dispatch-stop-card.p-skipped .dsc-seq { background: var(--pin-skipped); color: #fff; box-shadow: none; }
.dsc-nopin { color: var(--ink-3); font-size: 12.5px; }
.dsc-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; font-size: 12.5px; color: var(--ink-3); }
.dsc-meta .status { font-size: 12px; padding: 1px 7px; }
.dsc-contact a { color: inherit; }
.dsc-locked { font-size: 12.5px; }
.dsc-newroute { align-self: flex-start; }
.dispatch-stop-card.is-hot { outline: 2px solid var(--focus-ring); outline-offset: 1px; }

/* The move control, folded: the rail's script adds .js-folded before
   any card parses, and the card's "Move…" reveals its own form. The
   Board never adds the class, so its select stays open. A class, not
   [hidden] — .dsc-move is display:flex, which would beat the
   attribute (the trap CLAUDE.md records twice). */
.dsc-actions { margin-left: 8px; display: inline-flex; gap: 4px; flex: none; }
.dsc-quick, .dsc-move-open { display: none; }
.dsc-quick.is-quick { display: inline-flex; white-space: nowrap; }
.js-folded .dsc-move:not(.is-open) { display: none; }
.js-folded .dsc-move-open { display: inline-flex; }
.js-folded .dispatch-stop-card:has(form.dsc-move.is-open) .dsc-move-open { display: none; }
.dispatch-rail .dsc-move { margin-top: 6px; }

/* The map card: the whole remaining height, a frame in dark so the
   light tiles read as a window rather than a hole. Tiles and Leaflet's
   own controls are untouched (Theming decision). */
.dispatch-board .dispatch-map-col { display: flex; flex-direction: column; min-height: 0; position: relative; }
.dispatch-board .dispatch-map { height: auto; flex: 1 1 0; min-height: 360px; box-shadow: inset 0 0 0 1px light-dark(transparent, var(--line)); }
.dispatch-board .map-panel .card-head { flex-wrap: wrap; row-gap: 6px; padding: 10px 14px; }
.dispatch-board .map-panel .card-head h3 { white-space: nowrap; }
.map-tools { display: flex; gap: 6px; flex-wrap: wrap; }
.map-tools .filter-chip { font-size: 12.5px; padding: 4px 9px; }
.map-key-row { flex-basis: 100%; display: flex; align-items: center; gap: 6px 14px; flex-wrap: wrap; }
.dispatch-map-legend.in-head {
  background: none; border: 0; box-shadow: none; padding: 0; margin: 0;
  -webkit-backdrop-filter: none; backdrop-filter: none; gap: 4px 12px;
}
.dispatch-map-legend.in-head:empty { display: none; }
.dispatch-map-legend .swatch.lg-flag { display: grid; place-items: center; font-size: 8px; font-weight: 800; color: #fff; line-height: 1; }
.dispatch-map-legend .swatch.lg-muted { opacity: .55; }
.lg-route {
  display: inline-flex; align-items: center; gap: 6px; border: 0; background: none; padding: 2px 7px;
  border-radius: 999px; font: inherit; font-size: 12.5px; font-weight: 600; color: var(--ink-2); cursor: pointer;
}
.lg-route:hover { background: var(--bg-soft-2); color: var(--ink); }
.lg-route:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }
.lg-route[aria-pressed="true"] { background: var(--brand-tint); color: var(--brand-ink); font-weight: 700; }
.lg-route .sw, .dispatch-pin-route .sw { width: 11px; height: 11px; border-radius: 50%; border: 3px solid var(--rt); box-sizing: border-box; flex: none; display: inline-block; }
.map-focus { margin-left: auto; display: flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 600; color: var(--ink-2); }
.map-focus:empty { display: none; }
.map-note {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 650; pointer-events: none;
  background: var(--veil-strong); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 10px 14px;
  font-size: 13.5px; font-weight: 600; color: var(--ink-2); box-shadow: var(--sh-md); text-align: center; max-width: 80%;
}
.leaflet-bar.map-fit a { display: grid; place-items: center; color: #333; }
.dispatch-pin-route { display: flex; align-items: center; gap: 6px; color: var(--ink-2); font-weight: 600; margin-bottom: 6px; }
.dispatch-pin-links { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 6px; }
.dispatch-map .popup-move { margin: 6px 0 2px; }
.dispatch-map .popup-move .select { min-width: 150px; }

/* Pins: the route's hue as the outermost ring — on a hollow (not
   started) pin it is the border, so a pin never wears more than fill,
   gap and ring; a `!` on an overdue stop, a `?` on a quote; dimmed
   when another route is focused, muted while a hidden group is shown,
   lifted while its card is hovered. */
.dispatch-pin-dot.hued { box-shadow: 0 0 0 3px var(--rt), 0 1px 2px oklch(0 0 0 / 0.28), 0 2px 6px oklch(0 0 0 / 0.18); }
.dispatch-pin-dot.hued.p-pending { border-color: var(--rt); box-shadow: 0 1px 2px oklch(0 0 0 / 0.28), 0 2px 6px oklch(0 0 0 / 0.18); }
.dispatch-pin-dot.hued.far { box-shadow: 0 0 0 3px var(--rt), 0 0 0 6px var(--warn), 0 1px 2px oklch(0 0 0 / 0.28); }
.dispatch-pin-dot .pd-flag {
  position: absolute; left: -7px; top: -7px; width: 14px; height: 14px; border-radius: 50%; box-sizing: border-box;
  display: grid; place-items: center; font-family: var(--font-body); font-size: 9px; font-weight: 800; line-height: 1;
  color: #fff; background: var(--pin-failed); border: 1.5px solid #fff;
}
.dispatch-pin-dot .pd-flag.pd-q { background: var(--pin-skipped); }
.leaflet-marker-icon.is-dim { opacity: .35; }
.leaflet-marker-icon.is-later .dispatch-pin-dot { opacity: .55; }
.leaflet-marker-icon.is-hot .dispatch-pin-dot { transform: scale(1.25); }
@media (prefers-reduced-motion: no-preference) { .dispatch-pin-dot { transition: transform .12s ease; } }
.leaflet-overlay-pane .stop-line.hued.travelled, .leaflet-overlay-pane .stop-line.hued.ahead { stroke: var(--rt); }
.leaflet-overlay-pane .stop-line.is-dim { opacity: .35; }
.leaflet-overlay-pane .stop-line.is-focus { stroke-width: 5; }

/* Narrower: the rail at its minimum; the map keeps the rest. */
@media (max-width: 1100px) {
  .dispatch-board { grid-template-columns: minmax(320px, 360px) minmax(0, 1fr); }
}

/* Phone and tablet: three panes — Map · Stops · Routes — switched by
   data-pane on the board, an attribute rule so the sections' own
   display is never fought with [hidden]. Without JavaScript data-pane
   is never set and the three stack in normal flow. */
@media (max-width: 900px) {
  .dispatch-tabs { display: flex; }
  .dispatch-board { display: flex; flex-direction: column; min-height: 0; }
  .dispatch-board[data-pane] > .dispatch-rail, .dispatch-board[data-pane] > .dispatch-map-col { flex: 1 1 auto; min-height: 0; }
  .dispatch-board[data-pane] .rail-routes { max-height: none; flex: 1 1 0; border-top: 0; }
  .dispatch-board[data-pane] .rail-pool { min-height: 0; }
  .dispatch-board[data-pane]:not([data-pane="stops"]) .rail-pool,
  .dispatch-board[data-pane]:not([data-pane="stops"]) .dispatch-summary,
  .dispatch-board[data-pane]:not([data-pane="routes"]) .rail-routes,
  .dispatch-board[data-pane="map"] > .dispatch-rail,
  .dispatch-board[data-pane]:not([data-pane="map"]) > .dispatch-map-col { display: none; }
  .dispatch-board .dispatch-map { min-height: 300px; }
  /* The map head on a phone: the key and the route buttons as one
     row that scrolls sideways, so the map keeps the height. */
  .dispatch-board .map-key-row { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 2px; scrollbar-width: none; }
  .dispatch-board .map-key-row::-webkit-scrollbar { display: none; }
  .dispatch-board .dispatch-map-legend.in-head { flex-wrap: nowrap; }
  .dispatch-board .map-focus { flex: none; }
  .dispatch-board .map-panel .card-head .ch-sub { flex-basis: 100%; order: 3; font-size: 12.5px; }
  .dispatch-board .map-panel .card-head .map-tools { order: 2; }
  .dispatch-page:has(.dispatch-board:not([data-pane])) { height: auto; min-height: 0; }
  .dispatch-board:not([data-pane]) .rail-list { max-height: 70dvh; }
  .dispatch-board:not([data-pane]) .dispatch-map { min-height: 60dvh; }
}
