/* ============================================================
   style.css — Richland Transport design system
   One file, one set of tokens. Every page, the driver app and the
   auth screens read these; a page that needs a value the tokens do
   not have is the thing to question, not the token list.
   ============================================================ */

/* Inter, with the platform sans as the fallback. On an install that
   cannot reach Google (an offline cPanel box) the stack falls through
   to Segoe UI and nothing else changes: every size below is a token,
   so the layout does not depend on which face arrives. */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');

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

:root {
  /* ── Type ─────────────────────────────────────────────────────
     FIVE sizes, and no page picks its own. There were ~60 distinct
     font sizes across this file and the pages (.72rem, .78rem,
     .82rem, 13px …) — each a guess somebody made once. Hierarchy
     comes from WEIGHT first and size second. */
  --font-sans: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
  --font-mono: ui-monospace, 'Cascadia Mono', SFMono-Regular, Menlo, Consolas, monospace;
  /* 90% of the 29 Sep scale (11/13/15/18/26) since 2026-10-08 — "I want the
     size to be 90%", after seeing the app at 90% browser zoom. Spacing,
     control heights and the shell below moved with it. */
  --fs-xs: 10px;   /* captions, badges, helper text, table metadata */
  --fs-sm: 12px;   /* default: body, tables, inputs, buttons, navigation */
  --fs-md: 14px;   /* card titles, section headings, important labels */
  --fs-lg: 16px;   /* page titles */
  --fs-xl: 23px;   /* dashboard KPI numbers — nothing else */
  /* Not a sixth step: the floor iOS Safari needs on a focused field, below
     which it zooms the whole page and does not zoom back. Touch fields only. */
  --fs-input-touch: 16px;
  --fw-regular:  400;
  --fw-medium:   500;   /* labels, navigation, controls */
  --fw-semibold: 600;   /* headings, buttons, important values */
  --fw-bold:     700;   /* KPI numbers only */
  --lh:       1.5;
  --lh-tight: 1.3;

  /* ── Richland brand ───────────────────────────────────────────
     Navy is the brand (the sidebar, the mark, the auth panel).
     --accent stays the ACTION colour — buttons, links, focus — so
     brand and interaction never compete for the same signal. Gold
     is a whisper: the active nav marker and nothing louder. */
  --rt-navy-900: #081627;
  --rt-navy-800: #0F2340;
  --rt-navy-700: #16345C;
  --rt-navy-600: #1E4478;
  --rt-navy-500: #2A5793;
  --rt-navy-400: #4C7CB5;
  --rt-navy-300: #7FA3CC;
  --rt-navy-200: #A9C3E3;
  --rt-gold-500: #F0A92B;
  --rt-gold-600: #D18F16;
  --brand:       var(--rt-navy-800);
  --brand-mark:  var(--rt-navy-800);   /* the RT tile, wherever it appears */
  --brand-tint:  #eef3f9;              /* navy at a whisper: icon tiles, synced */

  /* ── Surfaces & text ──────────────────────────────────────────
     Contrast checked against white AND --bg: muted text is 6.6:1,
     placeholder/dim text 4.6:1 — both clear AA for 14px copy. */
  --bg:            #f4f6f9;
  --surface:       #ffffff;
  --surface-2:     #f8fafc;   /* table heads, footers, hover */
  --surface-3:     #f1f4f8;   /* insets inside a card */
  --card:          var(--surface);
  --card-hover:    var(--surface-2);
  --border:        #e3e8ef;
  --border-strong: #cdd5df;
  /* Controls — fields, buttons, icon buttons — carry their OWN border, darker
     than a card's hairline, so a white control on a white card still has an
     edge (1.6:1 against white, where --border is 1.1:1). */
  --control-border:       #c3ccd8;
  --control-border-hover: #9aa6b6;
  --text:          #0f172a;
  --text-label:    #334155;
  --text-muted:    #4f5d73;
  --text-dim:      #6b778a;

  /* ── Action ──────────────────────────────────────────────── */
  --accent:        #2563eb;
  --accent-hover:  #1d4ed8;
  --accent-dim:    rgba(37, 99, 235, 0.08);
  --accent-glow:   rgba(37, 99, 235, 0.25);
  --focus-ring:        0 0 0 3px rgba(37, 99, 235, 0.30);
  --focus-ring-danger: 0 0 0 3px rgba(220, 38, 38, 0.26);

  /* ── Semantic tones: solid · text · tint ─────────────────────
     Green, amber and red mean something (done, waiting, wrong) and
     are used for nothing else. Every badge, chip and alert in the
     app is one of these tones — see "Status system" below. */
  --success: #16a34a;  --success-text: #15803d;  --success-bg: #effaf3;
  --warning: #d97706;  --warning-text: #b45309;  --warning-bg: #fef7ea;
  --danger:  #dc2626;  --danger-text:  #b91c1c;  --danger-bg:  #fdf0f0;
  --info:    #2563eb;  --info-text:    #1d4ed8;  --info-bg:    #eef4fe;
  --neutral: #64748b;  --neutral-text: #475569;  --neutral-bg: #f1f4f8;
  /* The two trip phases a dispatcher most needs to tell apart. */
  --st-pickup:  #0891b2; --st-pickup-text:  #0e7490; --st-pickup-bg:  #ecf8fb;
  --st-dropoff: #6366f1; --st-dropoff-text: #4338ca; --st-dropoff-bg: #f0f0fe;
  /* Sent to AutoCount: done, and somewhere else now — the brand navy. */
  --synced: var(--rt-navy-500); --synced-text: var(--rt-navy-600); --synced-bg: var(--brand-tint);

  /* Older names, kept so no page has to be rewritten to use the tones. */
  --status-inprogress: var(--info);
  --status-completed:  var(--success);
  --status-pending:    var(--warning);
  --status-overdue:    var(--danger);
  --status-cancelled:  var(--neutral);
  --status-scheduled:  var(--info);
  --bg-subtle: var(--surface-2);
  --hover-bg:  var(--surface-2);
  --rule:      var(--border);
  --rule-2:    var(--border-strong);

  /* ── Shape ────────────────────────────────────────────────────
     xs: checkboxes, inline codes · sm: controls and buttons ·
     md: cards, menus, modals · lg: the driver's bottom sheet ·
     pill: badges and chips. Nothing else. */
  --radius-xs:   4px;
  --radius-sm:   6px;
  --radius:      8px;
  --radius-lg:   12px;
  --radius-pill: 999px;

  /* ── Depth ────────────────────────────────────────────────────
     A card is a border, not a shadow. Shadows are for things that
     FLOAT: a menu over the page, a dialog over everything. */
  --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.05);
  --shadow:    0 6px 20px rgba(15, 23, 42, 0.10), 0 1px 3px rgba(15, 23, 42, 0.06);
  --shadow-lg: 0 20px 48px rgba(15, 23, 42, 0.18), 0 4px 12px rgba(15, 23, 42, 0.06);

  /* ── Motion ───────────────────────────────────────────────────
     Feedback and state only; nothing performs. Menus and dialogs
     ease out fast; colour changes are a plain 150ms. */
  --ease-out:   cubic-bezier(0.23, 1, 0.32, 1);
  --transition: 150ms ease;

  /* ── Layout ──────────────────────────────────────────────── */
  --sidebar-w:   216px;
  --topbar-h:    50px;

  /* ── Spacing scale ──────────────────────────────────────────
     A 4px base. Anything spatial picks a step here; a raw px value
     in a rule means "this is an optical correction", not "nobody
     had decided yet". */
  --space-1:      4px;
  --space-2:      7px;
  --space-3:     11px;
  --space-4:     14px;
  --space-5:     18px;
  --space-6:     22px;
  --space-8:     29px;

  /* Roles. Change the rhythm here, not in the rules that use it. */
  --stack-gap:   var(--space-4);   /* between page-level sections, and between
                                      sibling cards in a grid */
  --card-pad:    var(--space-5);   /* inside a card */
  --field-gap:   var(--space-4);   /* between form fields — a field has no
                                      border to separate itself, so space does */

  /* ONE height for every control: inputs, selects, buttons, toggles.
     A row of controls lines up because they share this, not because
     each one's padding happened to add up. -sm is the in-table action. */
  --control-h:    32px;
  --control-h-sm: 28px;
  --control-lh:   18px;   /* a control's text line: 6 + 18 + 6 + 2px border = --control-h */
}

/* 15px, NOT a type token: every size is a px token now, and this root only
   scales the rem-based spacing the file already carries. Moving it would
   move every padding on every page. */
html { font-size: 13.5px; -webkit-text-size-adjust: 100%; }   /* was 15px: 90%, 2026-10-08 */

body {
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  font-weight: var(--fw-regular);
  background: var(--bg);
  color: var(--text);
  min-height: 100vh;
  line-height: var(--lh);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Weight, not size, makes hierarchy — so a bare <strong> is the heading
   weight, not a heavier one this scale does not have. */
b, strong { font-weight: var(--fw-semibold); }
h1, h2, h3, h4, h5, h6 { font-weight: var(--fw-semibold); line-height: var(--lh-tight); color: var(--text); }
code, kbd, samp { font-family: var(--font-mono); font-size: var(--fs-xs); }

/* The parts of the page the browser draws, themed from the same palette. */
::selection { background: rgba(37, 99, 235, 0.16); color: var(--text); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
input, textarea { caret-color: var(--accent); }

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
img { max-width: 100%; }
button { cursor: pointer; font-family: inherit; }

/* ── Layout ────────────────────────────────────────────────── */

/* JGRH: <body> > <div.app-layout> + <footer.app-footer>
   Make body a column so the footer is pushed to viewport bottom */
body:has(> .app-layout) {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
body:has(> .app-layout) > .app-layout {
  flex: 1;
}

.app-layout {
  display: flex;
  min-height: 100vh;
}

/* RT: <body.app-layout> > <div.main-wrapper> > (<main> + <footer>)
   Make the wrapper a column so the footer is pushed to viewport bottom */
.main-wrapper {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 100vh;
  /* body is a flex row and this is a flex item, so its min-width defaults to
     `auto` — meaning it refuses to shrink below its content's intrinsic width.
     One wide table pinned the whole layout to 992px and every page reported
     horizontal overflow on anything narrower than a desktop. .main-content
     already sets this; without it on the parent that had no effect. */
  min-width: 0;
}

/* A measure for the content, the way the OCR app holds its pages. On a wide
   monitor a full-bleed table stretches a six-column form across 2000px and the
   eye has to travel the whole way to pair a label with its field; capping it and
   centring in the space beside the sidebar keeps the reading distance sane. The
   cap is generous — this is Operate mode and a dense table still wants room —
   and it never shrinks a narrow screen, where max-width simply does not bind. */
.main-content > * { max-width: var(--content-max, 1700px); margin-inline: auto; }

/* …but a full-viewport layer is not content and must not be measured like it.
   `> *` catches every direct child, and 14 pages put a .modal-overlay there —
   so the cap shrank a position:fixed backdrop and the auto margins centred it,
   leaving the sidebar and a strip of the right edge unblurred and untinted
   while the middle of the screen dimmed. The overlay covers the viewport or it
   is not a backdrop; anything else full-screen added later belongs here too. */
.main-content > .modal-overlay { max-width: none; margin-inline: 0; }

.main-content {
  flex: 1;
  margin-left: var(--sidebar-w);
  /* One gutter for every page: 24px under the bar and at the sides. */
  padding: calc(var(--topbar-h) + var(--space-6)) var(--space-6) var(--space-8);
  min-width: 0;
  transition: margin-left var(--transition);
}

/* ── Sidebar ───────────────────────────────────────────────────
   The brand surface. Navy, because the rest of the app is white and
   slate and the one place that is always on screen is where the
   company's colour belongs — not on buttons, where it would compete
   with the action colour.

   Everything the shell draws lives in this block: the nav, its
   groups, Finance's children, and who is signed in. They used to be
   spread across four sections of this file, each overriding the last. */
.sidebar {
  position: fixed;
  top: 0; left: 0; bottom: 0;
  width: var(--sidebar-w);
  background: var(--brand);
  color: var(--rt-navy-200);
  display: flex;
  flex-direction: column;
  z-index: 100;
  transition: transform 200ms var(--ease-out);
}
.sidebar :focus-visible { outline-color: var(--rt-navy-200); }

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 0 var(--space-5);
  height: var(--topbar-h);
  flex-shrink: 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

/* The RT tile. Gold on the navy — the one place gold is more than a line. */
.brand-icon,
.co-icon {
  width: 32px; height: 32px;
  flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm);
  background: var(--rt-gold-500);
  color: var(--rt-navy-900);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
}

.brand-text { display: flex; flex-direction: column; line-height: var(--lh-tight); min-width: 0; }
.brand-name { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: #fff; }
.brand-sub  { font-size: var(--fs-xs); color: var(--rt-navy-300); }

.sidebar-nav {
  flex: 1;
  padding: var(--space-4) var(--space-3);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.16) transparent;
}

.nav-group + .nav-group { margin-top: var(--space-5); }

/* Sentence case, quiet, and closer to the items it names than to the
   group above — the label belongs to what follows. */
.nav-group-label {
  margin: 0 0 var(--space-1);
  padding: 0 var(--space-3);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  color: var(--rt-navy-300);
}

.nav-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--control-h);
  padding: 0 var(--space-3);
  margin: 1px 0;
  border-radius: var(--radius-sm);
  color: var(--rt-navy-200);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  text-decoration: none;
  transition: background var(--transition), color var(--transition);
}
.nav-item:hover { background: rgba(255, 255, 255, 0.06); color: #fff; text-decoration: none; }
.nav-item.active { background: rgba(255, 255, 255, 0.10); color: #fff; }

/* Where you are: a short gold rule on the sidebar's own edge. */
.nav-active-bar {
  position: absolute;
  left: calc(-1 * var(--space-3));
  top: 8px; bottom: 8px;
  width: 3px;
  background: var(--rt-gold-500);
  border-radius: 0 2px 2px 0;
}

.nav-icon { width: 18px; height: 18px; flex-shrink: 0; }

/* Finance's children, shown only while Finance is the section you are in —
   see the note in components/sidebar.php. Indented so the text lines up with
   the parent's label; the rail says they belong to the row above. */
.nav-kids {
  display: flex; flex-direction: column; gap: 1px;
  margin: 2px 0 var(--space-2) 21px;
  padding-left: var(--space-3);
  border-left: 1px solid rgba(255, 255, 255, 0.14);
}
.nav-kid {
  padding: 6px var(--space-3);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--rt-navy-300);
  text-decoration: none;
  transition: background var(--transition), color var(--transition);
}
.nav-kid:hover  { color: #fff; background: rgba(255, 255, 255, 0.06); text-decoration: none; }
.nav-kid.active { color: #fff; font-weight: var(--fw-semibold); }

/* Who is signed in, at the foot of the navigation it governs. Not a button:
   it goes nowhere, so it has no hover. */
.sidebar-footer {
  padding: var(--space-3);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.sidebar-user {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-1);
}
.sidebar-avatar {
  width: 32px; height: 32px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
}
.sidebar-user-text { display: flex; flex-direction: column; min-width: 0; line-height: var(--lh-tight); }
.sidebar-user-name {
  font-size: var(--fs-sm); font-weight: var(--fw-medium); color: #fff;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sidebar-user-meta {
  font-size: var(--fs-xs); color: var(--rt-navy-300);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nav-logout:hover { color: #fff; background: rgba(220, 38, 38, 0.22); }

/* Collapsed. -100%, NOT -1 × --sidebar-w: under 900px that variable is 0px,
   so the collapsed sidebar computed to translateX(0) and stayed pinned open. */
.sidebar-collapsed .sidebar  { transform: translateX(-100%); }
.sidebar-collapsed .main-content { margin-left: 0; }

/* ── Topbar ────────────────────────────────────────────────── */
.topbar {
  position: fixed;
  top: 0;
  left: var(--sidebar-w);
  right: 0;
  height: var(--topbar-h);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: 0 var(--space-5) 0 var(--space-4);
  z-index: 90;
  transition: left 200ms var(--ease-out);
}
.sidebar-collapsed .topbar { left: 0; }

.topbar-right { display: flex; align-items: center; gap: var(--space-1); flex-shrink: 0; margin-left: auto; }

.sidebar-toggle {
  width: var(--control-h); height: var(--control-h);
  display: flex; align-items: center; justify-content: center;
  background: none; border: none;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  cursor: pointer;
  transition: background var(--transition), color var(--transition);
}
.sidebar-toggle:hover { background: var(--surface-3); color: var(--text); }

/* The page name in the bar repeats the heading below it and the highlighted
   nav item beside it. It earns its place only on a phone, where the sidebar
   is hidden and the heading scrolls away. */
.topbar-title {
  display: none;
  min-width: 0;
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* The company mark in the bar exists only while the sidebar is hidden on a
   desktop — otherwise it is the second copy of a name already on screen. */
.topbar-brand {
  display: none;
  position: absolute; left: 50%; transform: translateX(-50%);
  align-items: center; gap: var(--space-2);
  pointer-events: none;   /* it names the app, it does not do anything */
}
.sidebar-collapsed .topbar-brand { display: flex; }
.topbar-brand-mark {
  width: 26px; height: 26px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm);
  background: var(--brand-mark);
  color: #fff; font-size: var(--fs-xs); font-weight: var(--fw-semibold);
}
.topbar-brand-name { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text); }

/* ── Cards ─────────────────────────────────────────────────────
   One card: same border, radius, padding and heading everywhere. A card
   separates itself with its border; the shadow is barely there. */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--card-pad);
  box-shadow: var(--shadow-sm);
  /* Cards had no bottom margin at all, so two in a row touched. The rhythm is
     a token, not a guess. */
  margin-bottom: var(--stack-gap);
}

.card-title {
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-tight);
  color: var(--text);
  margin-bottom: var(--space-4);
}

/* ── Checkbox list ─────────────────────────────────────────── */
.checkbox-grid {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.checkbox-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  font-size: var(--fs-sm);
  color: var(--text);
  border-bottom: 1px solid var(--border);
  transition: background var(--transition), color var(--transition);
  user-select: none;
}
.checkbox-item:last-child { border-bottom: none; }
.checkbox-item:hover { background: var(--surface-2); }
.checkbox-item:has(input:checked) { background: var(--accent-dim); color: var(--accent-hover); }

.checkbox-item input[type="checkbox"] {
  appearance: none;
  width: 16px; height: 16px;
  border: 1.5px solid var(--border-strong);
  border-radius: var(--radius-xs);
  background: var(--surface);
  cursor: pointer;
  flex-shrink: 0;
  transition: background var(--transition), border-color var(--transition);
  position: relative;
}
.checkbox-item input[type="checkbox"]:checked {
  background: var(--accent);
  border-color: var(--accent);
}
.checkbox-item input[type="checkbox"]:checked::after {
  content: '';
  position: absolute;
  left: 4px; top: 1px;
  width: 5px; height: 9px;
  border: 2px solid #fff;
  border-top: none;
  border-left: none;
  transform: rotate(45deg);
}

/* ── Stat cards (KPIs) ─────────────────────────────────────────
   Label, number, and the icon that says which section it counts. The
   number is the one place --fs-xl and weight 700 are used. No lift on
   hover: a transformed ancestor becomes the containing block for
   position:fixed, which is how the info tips once landed 680px away. */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--stack-gap);
  margin-bottom: var(--stack-gap);
}

.stat-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: var(--space-4) var(--space-5);
  display: flex;
  align-items: center;
  gap: var(--space-4);
  color: inherit;
  transition: border-color var(--transition), background var(--transition);
}
a.stat-card:hover { border-color: var(--border-strong); background: var(--surface-2); text-decoration: none; }

.stat-icon {
  width: 40px; height: 40px;
  border-radius: var(--radius);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  background: var(--neutral-bg); color: var(--neutral-text);
}
.stat-icon svg { width: 20px; height: 20px; }
.stat-icon.blue   { background: var(--info-bg);      color: var(--info); }
.stat-icon.green  { background: var(--success-bg);   color: var(--success); }
.stat-icon.amber  { background: var(--warning-bg);   color: var(--warning); }
.stat-icon.red    { background: var(--danger-bg);    color: var(--danger); }
/* On the road right now — the Pick-up hue, the same one its badge uses. */
.stat-icon.teal   { background: var(--st-pickup-bg); color: var(--st-pickup); }

.stat-info { min-width: 0; }
.stat-value {
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  line-height: 1.15;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.stat-label { font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--text-muted); margin-top: 2px; }

/* ── Tables ────────────────────────────────────────────────────
   The screen this app is read from most. Compact rows, one hairline
   between them, a quiet header, and a hover that is obvious enough to
   follow a row across twelve columns. */
.table-wrapper {
  overflow-x: auto;
  background: var(--surface);
  border-radius: var(--radius);
  border: 1px solid var(--border);
}

/* Tabular figures in every table: a column of money or dates lines up without
   each table having to ask. Not on body — in running text they read spaced out. */
table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

thead th {
  background: var(--surface-2);
  color: var(--text-muted);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-tight);
  padding: 10px var(--space-4);
  text-align: left;
  white-space: nowrap;
  border-bottom: 1px solid var(--border);
}

tbody tr {
  border-bottom: 1px solid var(--border);
  transition: background var(--transition);
}
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: var(--surface-2); }

/* tfoot is included deliberately: a totals row got 0 padding and sat flush
   against the table edge while every body cell was inset. */
tbody td,
tfoot td { padding: 10px var(--space-4); vertical-align: middle; color: var(--text); }

/* Money and counts line up on the right, in tabular figures. */
th.num, td.num, .num { text-align: right; font-variant-numeric: tabular-nums; }

.table-actions { display: flex; gap: var(--space-2); }

/* ── Status badges ─────────────────────────────────────────────
   One shape for every status in the app; the TONE comes from the
   status system below (--b-bg / --b-fg / --b-dot). */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 22px;
  padding: 0 var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--b-bg, var(--neutral-bg));
  color: var(--b-fg, var(--neutral-text));
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  line-height: 1;
  white-space: nowrap;
}
.badge::before {
  content: '';
  display: inline-block;
  width: 6px; height: 6px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--b-dot, currentColor);
}

/* ── Buttons ───────────────────────────────────────────────────
   Five kinds, one size, and EVERY one of them visible on a white card.

     primary    the one thing to do here — solid action blue
     secondary  everything supporting it — white, raised, a real border
     ghost      quieter — transparent, the same border, no shadow
     danger     destroys something — red outline, red fill on hover
     icon       a glyph in a row or toolbar — square, bordered

   A .btn with NO variant renders as secondary. Two pages carried a bare
   `btn btn-sm` and rendered white-on-white with no border at all; a
   default that can disappear is the wrong default.

   Borders use --control-border, not the card hairline: a button drawn in the
   same tone as the card it sits on is a button nobody finds. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  /* 6 + 18 + 6 + 2px border = --control-h (32px), from padding rather than
     a height: .btn-sm is the in-table action and a toolbar sets its own
     height, and neither should have to fight this one. */
  padding: 6px var(--space-4);
  border-radius: var(--radius-sm);
  border: 1px solid var(--control-border);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-sm);
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  line-height: var(--control-lh);
  text-decoration: none;
  white-space: nowrap;
  user-select: none;
  cursor: pointer;
  transition: background-color var(--transition), border-color var(--transition),
              color var(--transition), box-shadow var(--transition),
              transform 120ms var(--ease-out);
}
.btn:hover { background: var(--surface-2); border-color: var(--control-border-hover); color: var(--text); text-decoration: none; }
/* Pressed: a step darker as well as the press, so it reads without motion. */
.btn:active { transform: scale(0.97); background-color: var(--surface-3); }
@media (prefers-reduced-motion: reduce) { .btn:active { transform: none; } }
.btn:focus-visible { outline: none; box-shadow: var(--focus-ring); border-color: var(--accent); }
/* THE `hidden` ATTRIBUTE, MADE TO ACTUALLY HIDE.
   The UA sheet's rule is `[hidden] { display: none }` at specificity 0,1,0 — the
   same as any class — and it comes from the user-agent sheet, so ANY class rule
   carrying `display` beats it. This file held a dozen one-off
   `.x[hidden] { display: none }` patches, and two more bugs still shipped for
   want of a thirteenth. One rule instead. `.auth-pane` is the single deliberate
   exception — it keeps `hidden` set while it animates out. */
[hidden]:not(.auth-pane) { display: none !important; }
.btn svg { width: 16px; height: 16px; flex-shrink: 0; }

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.btn-primary:hover,
.btn-primary:active { background: var(--accent-hover); border-color: var(--accent-hover); color: #fff; }

/* Identical to a bare .btn — named so markup can say what it means. */
.btn-secondary { background: var(--surface); border-color: var(--control-border); color: var(--text); }

.btn-ghost {
  background: transparent;
  border-color: var(--control-border);
  color: var(--text-label);
  box-shadow: none;
}
.btn-ghost:hover { background: var(--surface-3); border-color: var(--control-border-hover); color: var(--text); }

.btn-danger {
  background: var(--surface);
  border-color: rgba(220, 38, 38, 0.45);
  color: var(--danger-text);
}
.btn-danger:hover,
.btn-danger:active { background: var(--danger-bg); border-color: var(--danger); color: var(--danger-text); }
.btn-danger:focus-visible { box-shadow: var(--focus-ring-danger); border-color: var(--danger); }

.btn-success {
  background: var(--surface);
  border-color: rgba(22, 163, 74, 0.45);
  color: var(--success-text);
}
.btn-success:hover,
.btn-success:active { background: var(--success-bg); border-color: var(--success); color: var(--success-text); }

/* Disabled looks the same whatever the variant: flat, grey, still legible.
   Fading a primary to half opacity left white text on pale blue — readable
   neither as enabled nor as off. */
.btn:disabled,
.btn[aria-disabled="true"] {
  background: var(--surface-3);
  border-color: var(--border);
  color: var(--text-dim);
  box-shadow: none;
  cursor: not-allowed;
  transform: none;
}

/* The in-table size: same type, a shorter box — 4 + 20 + 4 + 2 = 30px. */
.btn-sm { padding: 4px var(--space-3); gap: 6px; }
.btn-sm svg { width: 14px; height: 14px; }

.btn-block { width: 100%; }

/* Icon-only buttons outside .btn — row actions, the stop remover. One box:
   a real border at rest, not only on hover, because a row of glyphs with no
   edge reads as decoration rather than as things to press. */
.icon-btn {
  width: var(--control-h-sm); height: var(--control-h-sm);
  display: grid; place-items: center;
  border-radius: var(--radius-sm);
  border: 1px solid var(--control-border);
  background: var(--surface);
  color: var(--text-muted);
  cursor: pointer;
  transition: background var(--transition), color var(--transition), border-color var(--transition), transform 120ms var(--ease-out);
}
.icon-btn svg { width: 15px; height: 15px; }
.icon-btn:hover { background: var(--surface-2); color: var(--text); border-color: var(--control-border-hover); }
.icon-btn:active { transform: scale(0.94); background: var(--surface-3); }
.icon-btn:focus-visible { outline: none; box-shadow: var(--focus-ring); border-color: var(--accent); }
.icon-btn:disabled { background: var(--surface-3); color: var(--text-dim); border-color: var(--border); cursor: not-allowed; transform: none; }
.icon-btn.is-danger:hover  { background: var(--danger-bg);  color: var(--danger-text);  border-color: rgba(220, 38, 38, 0.45); }
.icon-btn.is-go:hover      { background: var(--info-bg);    color: var(--info-text);    border-color: rgba(37, 99, 235, 0.4); }
.icon-btn.is-success:hover { background: var(--success-bg); color: var(--success-text); border-color: rgba(22, 163, 74, 0.45); }
@media (prefers-reduced-motion: reduce) { .icon-btn:active { transform: none; } }

/* ── Tabs ──────────────────────────────────────────────────────
   WRAPS rather than scrolls: a tab strip that scrolls hides its own
   options, and six payroll tabs once overflowed into a permanent
   scrollbar. */
.tab-bar {
  display: flex;
  gap: var(--space-1);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--stack-gap);
  flex-wrap: wrap;
}

.tab-btn {
  background: none;
  border: none;
  color: var(--text-muted);
  padding: 10px var(--space-3);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  border-bottom: 2px solid transparent;
  transition: color var(--transition), border-color var(--transition);
  white-space: nowrap;
  margin-bottom: -1px;
}
.tab-btn:hover  { color: var(--text); }
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }

.tab-panel { display: none; }
.tab-panel.active { display: block; }

/* ── Page header ───────────────────────────────────────────────
   Title (20px) + one line of context on the left, the page's actions
   on the right. Every page opens this way. */
.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--stack-gap);
  gap: var(--space-4);
  flex-wrap: wrap;
}
.page-header h1,
.page-header h2,
.page-title {
  font-size: var(--fs-lg);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-tight);
  letter-spacing: -0.01em;
  color: var(--text);
}

/* ── Forms ─────────────────────────────────────────────────────
   Every input, select, date and textarea: one height, one border, one
   radius, one focus ring. Labels are 14px/500 in sentence case — they
   name a field, they are not a heading. */
.form-group { margin-bottom: var(--field-gap); }
.form-label {
  display: block;
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text-label);
  margin-bottom: 6px;
}
.form-control {
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--control-border);
  border-radius: var(--radius-sm);
  color: var(--text);
  padding: 6px var(--space-3);   /* 6 + 18 + 6 + 2 = --control-h */
  font-size: var(--fs-sm);
  line-height: var(--control-lh);
  outline: none;
  transition: border-color var(--transition), box-shadow var(--transition);
  font-family: inherit;
}
.form-control:focus { border-color: var(--accent); box-shadow: var(--focus-ring); }
.form-control::placeholder { color: var(--text-dim); }
select.form-control { appearance: none; cursor: pointer; }
textarea.form-control { resize: vertical; min-height: 88px; }

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--field-gap);
}
@media (max-width: 600px) { .form-row { grid-template-columns: 1fr; } }

/* ── Trip route: pick-ups and drop-offs ────────────────────────────────────
   Here rather than inline in tasks/new.php, because tasks/edit.php draws the
   same component — an inline copy is how .view-toggle came out as bare blue
   links the first time a second page rendered it. */
.rt-route { display: grid; gap: var(--field-gap); grid-template-columns: 1fr 1fr; }
@media (max-width: 900px) { .rt-route { grid-template-columns: 1fr; } }

.rt-leg  { display: flex; flex-direction: column; gap: .4rem; min-width: 0; }
.rt-req  { color: var(--accent); }

.rt-stops { display: flex; flex-direction: column; gap: .4rem; }
.rt-stop  { display: flex; gap: .4rem; align-items: center; min-width: 0; }
.rt-stop input[type="text"] { flex: 1 1 auto; min-width: 0; }

/* The order the lorry drives them, which is the one thing a list of addresses
   cannot say on its own. Numbered by JS so removing row 2 renumbers the rest —
   a stale "3" beside the second stop is worse than no number. */
.rt-stop-n {
    flex: 0 0 auto; width: 20px; text-align: center;
    font-size: var(--fs-xs); font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}

/* ONE customer per trip, unless the job type says otherwise — only a console
   job carries several, one per drop-off (client, item 5). Every other type
   bills to the single Customer field under Assignment.

   The class is on a WRAPPER, never on the <select>. controls.js replaces a
   native select with its own .ui-select built as a sibling, so hiding the
   select left the visible control on screen and the customer picker showed on
   every job type. Hiding the wrapper takes the enhanced control with it.

   Rendered either way rather than conditionally: the job type changes without
   a reload, and building the option list in JS would ship the customer list to
   the page twice. */
.rt-stop-cust { flex: 0 1 40%; min-width: 0; display: none; }
.rt-route[data-console="1"] .rt-stop-cust { display: block; }
.rt-stop-cust select, .rt-stop-cust .ui-select { width: 100%; }

.rt-stop-x {
    flex: 0 0 auto;
    width: var(--control-h); height: var(--control-h);
    display: inline-flex; align-items: center; justify-content: center;
    font-size: var(--fs-md); line-height: 1;
    background: var(--surface); border: 1px solid var(--control-border); border-radius: var(--radius-sm);
    color: var(--text-muted); cursor: pointer;
    transition: color 150ms ease-out, border-color 150ms ease-out, transform 150ms ease-out;
}
.rt-stop-x:active { transform: scale(0.94); }
@media (hover: hover) and (pointer: fine) {
    .rt-stop-x:hover { color: var(--danger); border-color: var(--danger); }
}
/* The only row left is the one the trip cannot do without, so its remove
   control goes away rather than sitting there refusing to work. */
.rt-stops:has(> .rt-stop:only-child) .rt-stop-x { visibility: hidden; }

.rt-stop-add {
    align-self: flex-start;
    display: inline-flex; align-items: center; gap: .35rem;
    margin-top: .1rem; padding: 0;
    background: none; border: none; cursor: pointer;
    font-size: var(--fs-xs); font-weight: 600; color: var(--accent);
    transition: opacity 150ms ease-out;
}
.rt-stop-add:hover { opacity: .75; }

.rt-route-hint {
    grid-column: 1 / -1; margin: 0;
    font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5;
}

/* ── Modal ─────────────────────────────────────────────────────
   Header, body, footer; the footer's buttons on the right. The backdrop
   dims — it does not blur: frosted glass over a data screen is
   decoration, and it costs a repaint of everything underneath. */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, 0.45);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 300;
  padding: var(--space-5);
}
.modal-overlay.open { display: flex; }

.modal,
.modal-box {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  width: 100%;
  max-width: 580px;
  max-height: 90vh;
  overflow-y: auto;
  box-shadow: var(--shadow-lg);
  animation: modalIn 180ms var(--ease-out);
}

/* Centred, so it grows from the centre — a modal belongs to the page, not to
   the button that opened it. Starts nearly full size: nothing appears from
   nothing. */
@keyframes modalIn {
  from { opacity: 0; transform: scale(0.98) translateY(4px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  @keyframes modalIn { from { opacity: 0; } to { opacity: 1; } }
}

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  border-radius: var(--radius) var(--radius) 0 0;
}
.modal-title { font-size: var(--fs-md); font-weight: var(--fw-semibold); color: var(--text); }

.modal-close {
  flex-shrink: 0;
  width: 32px; height: 32px;
  display: flex; align-items: center; justify-content: center;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-size: var(--fs-lg);
  line-height: 1;
  cursor: pointer;
  transition: background var(--transition), color var(--transition);
}
.modal-close:hover { background: var(--surface-3); color: var(--text); }

.modal-body    { padding: var(--space-5); }
.modal-footer  {
  display: flex; gap: var(--space-2); justify-content: flex-end;
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--border);
  background: var(--surface-2);
  border-radius: 0 0 var(--radius) var(--radius);
}

/* A form inside a modal gets the body's padding. */
.modal-box > form,
.modal > form { padding: var(--space-5); }

/* Native <dialog>: the same box and the same backdrop. */
dialog::backdrop { background: rgba(15, 23, 42, 0.45); }
dialog {
  padding: 0;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}

/* ── Filters bar ───────────────────────────────────────────── */
.filter-bar {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--stack-gap);
  flex-wrap: wrap;
  align-items: center;
}
.filter-bar .form-control { width: auto; min-width: 160px; }

/* ── Login page ────────────────────────────────────────────── */
.login-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: radial-gradient(ellipse at 60% 40%, rgba(37,99,235,0.06) 0%, transparent 60%), var(--bg);
}

.login-card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 44px 40px;
  width: 100%;
  max-width: 420px;
  box-shadow: var(--shadow);
  text-align: center;
}

.login-logo {
  width: 56px; height: 56px;
  background: linear-gradient(135deg, var(--accent), var(--accent-hover));
  border-radius: var(--radius);
  display: flex; align-items: center; justify-content: center;
  font-weight: 600; font-size: var(--fs-lg); color: #fff;
  margin: 0 auto 20px;
}

.login-title    { font-size: var(--fs-lg); font-weight: 600; margin-bottom: 6px; color: var(--text); }
.login-subtitle { color: var(--text-muted); font-size: var(--fs-sm); margin-bottom: 32px; }

.login-divider {
  height: 1px;
  background: var(--border);
  margin: 28px 0;
  position: relative;
}

.btn-microsoft {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  width: 100%;
  padding: 12px 20px;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  font-weight: 600;
  transition: box-shadow var(--transition), transform var(--transition), background var(--transition);
  text-decoration: none;
}
.btn-microsoft:hover {
  background: var(--accent-hover);
  box-shadow: none;
  transform: translateY(-1px);
  text-decoration: none;
  color: #fff;
}
.ms-logo { width: 20px; height: 20px; flex-shrink: 0; }

/* ── Detail view ───────────────────────────────────────────── */
.detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--stack-gap);
  margin-bottom: 12px;
  /* Grid items stretch to the tallest row by default, so a short card sat with
     a slab of dead space under its content to match a taller neighbour — most
     visible on the trip page, where the timeline card ran ~200px past its last
     row. Each card is now as tall as what is in it. */
  align-items: start;
}

/* Same grid, but for cards carrying a TABLE rather than a few labelled fields.
   220px would put two six-column tables side by side on a laptop and leave
   both scrolling sideways inside their own card, which is worse than one
   table you can read. 640px means the pair only splits once there is room for
   it — roughly a 1600px window, given the sidebar and the page padding — and
   auto-fit drops back to one full-width column below that on its own, so
   there is no breakpoint to keep in step with anything.

   These two DO stretch to match, unlike the grid above: they are the same
   table split in half, and a pair of six-column lists ending at different
   heights reads as a rendering fault rather than as a difference in the data.
   The cap below bounds how much empty space that can cost. */
/* min(640px, 100%), not a bare 640px: minmax() treats its first argument as a
   FLOOR, so the track could never go below 640 and a 390px phone got a 640px
   column — the dashboard scrolled sideways and the two widest cards hung off
   the edge. min() lets the floor collapse to the container when the container
   is the smaller of the two. */
.detail-grid.is-wide {
  grid-template-columns: repeat(auto-fit, minmax(min(640px, 100%), 1fr));
  align-items: stretch;
}

.detail-field label {
  display: block;
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
  margin-bottom: 2px;
}
.detail-field p {
  font-size: var(--fs-sm);
  color: var(--text);
}

/* ── Breadcrumb ────────────────────────────────────────────── */
.breadcrumb {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin-bottom: var(--stack-gap);
}
.breadcrumb a { color: var(--text-muted); }
.breadcrumb a:hover { color: var(--accent); }
.breadcrumb-sep { color: var(--text-dim); }
.breadcrumb span { color: var(--text); }

/* ── Footer ────────────────────────────────────────────────── */
.app-footer {
  padding: var(--space-3) var(--space-6);
  font-size: var(--fs-xs);
  color: var(--text-muted);
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: space-between;
  margin-left: var(--sidebar-w);
  background: var(--surface);
}
.sidebar-collapsed .app-footer { margin-left: 0; }

/* ── Alerts ────────────────────────────────────────────────────
   A tinted box with a hairline of its own tone — not a thick coloured
   left edge, which reads as a decoration before it reads as a message. */
.alert {
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--a-line, var(--border));
  border-radius: var(--radius);
  background: var(--a-bg, var(--surface-2));
  color: var(--a-fg, var(--text));
  font-size: var(--fs-sm);
  line-height: var(--lh);
  margin-bottom: var(--stack-gap);
}
.alert-success { --a-bg: var(--success-bg); --a-fg: var(--success-text); --a-line: rgba(22, 163, 74, 0.25); }
.alert-error   { --a-bg: var(--danger-bg);  --a-fg: var(--danger-text);  --a-line: rgba(220, 38, 38, 0.25); }
.alert-info    { --a-bg: var(--info-bg);    --a-fg: var(--info-text);    --a-line: rgba(37, 99, 235, 0.22); }
.alert-warning { --a-bg: var(--warning-bg); --a-fg: var(--warning-text); --a-line: rgba(217, 119, 6, 0.28); }

/* ── Payroll stats row ─────────────────────────────────────── */
.payroll-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 14px;
  margin-bottom: var(--stack-gap);
}
.payroll-stat {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4) var(--space-5);
}
.payroll-stat .value { font-size: var(--fs-lg); font-weight: 600; color: var(--text); }
.payroll-stat .label { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 4px; }

/* ── Responsive ────────────────────────────────────────────── */
@media (max-width: 900px) {
  :root { --sidebar-w: 0px; }
  /* The bar outranks the drawer here, and the drawer starts below it.
     Without this the open sidebar (z-index 100) covered the top bar (90) —
     including the hamburger at its left edge, the one control that closes it.
     A click aimed at the toggle landed on the brand block underneath and
     opened the company switcher instead, which is what "the sidebar cannot be
     closed" was: it could, but only by clicking to the right of the drawer. */
  .topbar  { z-index: 110; }
  .sidebar { top: var(--topbar-h); }
  .sidebar { transform: translateX(-240px); width: 240px; }
  .main-content { margin-left: 0; }
  .main-wrapper { margin-left: 0; }
  .topbar { left: 0; }
  .app-footer { margin-left: 0; }
  .sidebar.open { transform: translateX(0); box-shadow: var(--shadow-lg); }
  .topbar-title { display: block; }
  .topbar-brand, .sidebar-collapsed .topbar-brand { display: none; }
}

@media (max-width: 600px) {
  .page-header { flex-direction: column; align-items: flex-start; }
}

/* ── Scrollbar ─────────────────────────────────────────────── */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: var(--radius-pill); border: 2px solid transparent; background-clip: padding-box; }
::-webkit-scrollbar-thumb:hover { background-color: var(--text-dim); }

/* ── Utility ───────────────────────────────────────────────── */
.text-muted { color: var(--text-muted); }

/* Visible to screen readers only — for legends and labels that would be
   redundant on screen but are needed to name a group. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ── Sub-navigation ────────────────────────────────────────────
   The same underline tabs as .tab-bar, as links. Wraps rather than
   scrolls: a tab strip that scrolls hides its own options. */
.sub-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-bottom: var(--stack-gap);
  border-bottom: 1px solid var(--border);
}
.sub-nav-item {
  padding: 10px var(--space-3);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  white-space: nowrap;
  transition: color var(--transition), border-color var(--transition);
}
.sub-nav-item:hover  { color: var(--text); text-decoration: none; }
.sub-nav-item.active { color: var(--accent); border-bottom-color: var(--accent); }
.sub-nav-item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

/* Row count carried inside a nav item — part of the label, not a loose number. */
.sub-nav-count {
  display: inline-block;
  margin-left: 6px;
  padding: 0 6px;
  border-radius: var(--radius-pill);
  background: var(--surface-3);
  color: var(--text-muted);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  line-height: 18px;
  font-variant-numeric: tabular-nums;
  transition: background var(--transition), color var(--transition);
}
.sub-nav-item.active .sub-nav-count { background: var(--accent-dim); color: var(--accent); }
.sub-nav-item:hover  .sub-nav-count { color: var(--text); }

/* ── Form grid & helpers ───────────────────────────────────── */
.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--field-gap);
}
.form-group-full { grid-column: 1 / -1; }

/* Names a band of fields and is NOT one of them — a section heading, so it
   is the heading size, with a rule running out to the edge. Lives here and
   not in tasks/new.php: the trip form, the invoice dialog and the console
   dialog all draw it. */
.form-section {
  grid-column: 1 / -1;
  display: flex; align-items: center; gap: var(--space-3);
  margin: var(--space-3) 0 0;
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  color: var(--text);
}
.form-section:first-child { margin-top: 0; }
.form-section::after { content: ''; flex: 1; height: 1px; background: var(--border); }
.form-actions {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
@media (max-width: 640px) { .form-grid { grid-template-columns: 1fr; } }

/* Global form element styling
   ────────────────────────────
   Selected by EXCLUSION, not by an allowlist of types: a field is styled
   unless we say otherwise, so a new input type inherits the design system
   rather than falling out of it. The exclusions sit inside :where() so
   they add NO specificity — a base rule must be the easiest thing in the
   stylesheet to override, not the hardest.

   36px from line-height + padding rather than a fixed height, so a field a
   component deliberately sizes (a qty cell, a filter in a popover) can
   still shrink by overriding its padding. */
input:not(:where([type="checkbox"], [type="radio"], [type="submit"], [type="button"],
                 [type="reset"], [type="file"], [type="range"], [type="color"],
                 [type="image"], [type="hidden"])),
select,
textarea {
  width: 100%;
  background-color: var(--surface);
  border: 1px solid var(--control-border);
  border-radius: var(--radius-sm);
  color: var(--text);
  padding: 6px var(--space-3);   /* 6 + 18 + 6 + 2 = --control-h */
  font-size: var(--fs-sm);
  line-height: var(--control-lh);
  outline: none;
  transition: border-color var(--transition), box-shadow var(--transition);
  font-family: inherit;
  box-sizing: border-box;
}
input:not(:where([type="checkbox"], [type="radio"], [type="submit"], [type="button"],
                 [type="reset"], [type="file"], [type="range"], [type="color"],
                 [type="image"], [type="hidden"])):hover,
select:hover,
textarea:hover { border-color: var(--control-border-hover); }
input:not(:where([type="checkbox"], [type="radio"], [type="submit"], [type="button"],
                 [type="reset"], [type="file"], [type="range"], [type="color"],
                 [type="image"], [type="hidden"])):focus,
select:focus,
textarea:focus {
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
}
input:disabled, select:disabled, textarea:disabled {
  background-color: var(--surface-3);
  color: var(--text-muted);
  cursor: not-allowed;
}
input::placeholder, textarea::placeholder { color: var(--text-dim); }
/* A native select keeps a chevron: with no script, or on a touch screen,
   controls.js leaves it as the visible control, and an appearance:none
   select with no arrow does not read as a choice. */
select { appearance: none; cursor: pointer; }
select:not([multiple]) {
  padding-right: 32px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%234f5d73' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 14px;
}
textarea { resize: vertical; min-height: 72px; line-height: var(--lh); }

/* Safari/Chrome give search inputs their own chrome — a pill radius and a
   native clear affordance — which would make one field in a filter row look
   unlike its neighbours. */
input[type="search"] { appearance: none; -webkit-appearance: none; }
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }

/* A field's caption: 14px/500 in sentence case. */
label {
  display: block;
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text-label);
  margin-bottom: 6px;
}

/* THE OTHER KIND OF LABEL: one that WRAPS its own control is that control's
   readable text, not a caption above a field. :has() decides it structurally,
   so the next one written is right without anybody remembering. */
label:has(input) {
  text-transform: none;
  letter-spacing: normal;
}

/* ── Card header ───────────────────────────────────────────── */
.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--border);
}

/* ── Empty state ───────────────────────────────────────────── */
.empty-state {
  text-align: center;
  padding: var(--space-8) var(--space-6);
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

/* ── Detail list ───────────────────────────────────────────── */
.detail-list {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2) var(--space-5);
  font-size: var(--fs-sm);
}
.detail-list dt { color: var(--text-muted); font-weight: var(--fw-medium); white-space: nowrap; }
.detail-list dd { color: var(--text); margin: 0; }

/* ── Status system ─────────────────────────────────────────────
   EVERY status in the app — trips, invoices, the cashbook, claims,
   leave, trucks, maintenance — is one of these tones. A badge reads
   --b-bg/--b-fg/--b-dot; a filter chip and the trip page's hero read the
   solid --st. One place, so "Approved" is the same green on an invoice
   as on a claim, and "Pick up" can never come out the colour of "Drop off".

     neutral   Cancelled · Inactive
     warning   Pending assign · Draft · Pending · Awaiting approval
     info      Assigned · Scheduled · In progress
     pickup    Pick up (on the road, loading)
     dropoff   Drop off (on the road, delivering)
     success   Completed · Approved · Active · Paid
     danger    Overdue · Rejected
     synced    Sent / Synced to AutoCount

   The PHP side names a phase with rtTripStatusColor(); this block is the
   only thing that turns that name into a colour. */
.badge-neutral, .badge-inactive, .badge-cancelled, .badge-unavailable,
.st-cancelled, .status-chip.chip-cancelled {
  --b-bg: var(--neutral-bg); --b-fg: var(--neutral-text); --b-dot: var(--neutral); --st: var(--neutral);
}
.badge-warning, .badge-pending, .badge-draft, .badge-unassigned, .badge-submitted, .badge-under-maintenance,
.st-unassigned, .status-chip.chip-unassigned, .status-chip.chip-pending {
  --b-bg: var(--warning-bg); --b-fg: var(--warning-text); --b-dot: var(--warning); --st: var(--warning);
}
.badge-info, .badge-accent, .badge-inprogress, .badge-assigned, .badge-scheduled, .badge-received,
.st-assigned, .status-chip.chip-assigned, .status-chip.chip-scheduled {
  --b-bg: var(--info-bg); --b-fg: var(--info-text); --b-dot: var(--info); --st: var(--info);
}
.badge-departed, .badge-at_detour, .badge-on-trip,
.st-departed, .st-at_detour, .status-chip.chip-departed, .status-chip.chip-at_detour {
  --b-bg: var(--st-pickup-bg); --b-fg: var(--st-pickup-text); --b-dot: var(--st-pickup); --st: var(--st-pickup);
}
.badge-arrived,
.st-arrived, .status-chip.chip-arrived {
  --b-bg: var(--st-dropoff-bg); --b-fg: var(--st-dropoff-text); --b-dot: var(--st-dropoff); --st: var(--st-dropoff);
}
.badge-success, .badge-completed, .badge-approved, .badge-active, .badge-paid, .badge-confirmed, .badge-available,
.st-completed, .status-chip.chip-completed, .status-chip.chip-approved {
  --b-bg: var(--success-bg); --b-fg: var(--success-text); --b-dot: var(--success); --st: var(--success);
}
.badge-danger, .badge-overdue, .badge-rejected,
.st-overdue, .status-chip.chip-rejected, .status-chip.chip-overdue {
  --b-bg: var(--danger-bg); --b-fg: var(--danger-text); --b-dot: var(--danger); --st: var(--danger);
}
.badge-synced,
.status-chip.chip-synced {
  --b-bg: var(--synced-bg); --b-fg: var(--synced-text); --b-dot: var(--synced); --st: var(--synced);
}

/* ── Stats grid (the dashboard's KPI rows) ─────────────────────
   auto-FILL, not auto-fit: auto-fit hands empty tracks' width to the cards
   that exist, so a row's cards got WIDER the fewer of them there were. */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--stack-gap);
  margin-bottom: var(--stack-gap);
}

/* A caption for a group of stat cards. margin-BLOCK, never the `margin`
   shorthand: `.main-content > *` centres every direct child with
   margin-inline:auto, and the shorthand would reset that to 0. */
.stat-group-label {
  margin-block: var(--space-6) var(--space-2);
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--text-muted);
}
.stat-group-label:first-child { margin-block-start: 0; }

/* On a phone, two KPIs across with the icon above the number — one per row
   was eight tall cards before the first line of real content. */
@media (max-width: 600px) {
  .stats-grid, .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); }
  .stat-card { flex-direction: column; align-items: flex-start; gap: var(--space-2); padding: var(--space-3) var(--space-4); }
  .stat-icon { width: 32px; height: 32px; }
  .stat-icon svg { width: 18px; height: 18px; }
}

/* ── Finance: cashbook row types ───────────────────────────── */
/* IN/OUT is already a badge and a column of its own; a coloured spine on the
   row said it a third time. The amounts carry the direction in their tone. */
.amount-in  { color: var(--success-text); font-weight: var(--fw-semibold); }
.amount-out { color: var(--danger-text);  font-weight: var(--fw-semibold); }
/* A category is a filing label, not a state, so it is neutral. Each page used to
   paint its categories from its own rgba() palette — Fuel amber, Maintenance red
   — which put the status colours on things that are not statuses. */
.cat-chip {
  display: inline-flex;
  align-items: center;
  height: 22px;
  padding: 0 var(--space-2);
  border-radius: var(--radius-xs);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-label);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  white-space: nowrap;
}
.tfoot-row td { font-weight: var(--fw-semibold); padding: 10px var(--space-4); background: var(--surface-2); }

/* ── The cashbook (finance/index.php) ──────────────────────────────────────
   Redesigned 2026-10-08 with the standby-cash documents that now wait here.
   A row says what happened in one line and, for a standby-cash entry, the
   document it will become underneath — folded, because it is read when it is
   being approved and not on every scan of the list. */
.cb-table .cb-date { width: 96px; white-space: nowrap; color: var(--text-muted); font-size: var(--fs-xs); }
.cb-table .cb-cat  { width: 128px; }
.cb-table .cb-st   { width: 108px; }
.cb-table .cb-act  { width: 150px; }
.cb-table .cb-part { min-width: 280px; }
.cb-num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
th.cb-num { width: 112px; }
.cb-table td { vertical-align: top; }
.cb-desc { color: var(--text); line-height: 1.4; }
.cb-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
  margin-top: 4px; font-size: var(--fs-xs); color: var(--text-muted);
}
.cb-doc { font-weight: var(--fw-medium); color: var(--text-label); }
/* A journal moves no cash, so it does not sit in either cash column: it spans
   both, quietly, and the balance beside it does not move. */
.cb-jv { color: var(--text-muted); text-align: center; }
.cb-jv span { font-size: var(--fs-xs); }
.is-jv .cb-bal { color: var(--text-muted); font-weight: var(--fw-regular, 400); }
.cb-bal { font-weight: var(--fw-semibold); }
.cb-bal.is-neg { color: var(--danger-text); }
.cb-problem {
  display: flex; align-items: flex-start; gap: 6px;
  margin-top: 6px; font-size: var(--fs-xs); line-height: 1.45; color: var(--warning-text);
}
.cb-problem svg { width: 14px; height: 14px; flex: none; margin-top: 1px; }
.cb-problem a { font-weight: var(--fw-semibold); color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.cb-post { margin-top: 6px; }
.cb-post > summary {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: var(--fs-xs); font-weight: var(--fw-medium); color: var(--accent);
  cursor: pointer; list-style: none; user-select: none; border-radius: var(--radius-xs);
}
.cb-post > summary::-webkit-details-marker { display: none; }
.cb-post > summary:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.cb-post > summary svg { width: 12px; height: 12px; transition: transform var(--transition); }
.cb-post[open] > summary svg { transform: rotate(90deg); }
.cb-lines {
  width: 100%; margin-top: var(--space-2);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  border-collapse: separate; border-spacing: 0; overflow: hidden;
  background: var(--surface-2); font-size: var(--fs-xs);
}
.cb-lines th, .cb-lines td { padding: 6px var(--space-2); text-align: left; white-space: nowrap; border-bottom: 1px solid var(--border); }
.cb-lines th { font-weight: var(--fw-medium); color: var(--text-muted); background: var(--surface-3); }
.cb-lines tr:last-child td { border-bottom: 0; }
.cb-lines .cb-num { text-align: right; }
.cb-acc { font-weight: var(--fw-semibold); color: var(--text); font-variant-numeric: tabular-nums; }
.cb-missing { color: var(--warning-text); font-weight: var(--fw-medium); }
/* Where a sent entry went — the synced tone, the one this app gives "in AutoCount". */
.cb-acdoc { font-weight: var(--fw-semibold); color: var(--synced-text); font-variant-numeric: tabular-nums; }
.cb-problem.is-ac { color: var(--danger-text); }
.cb-receipt { font-weight: var(--fw-medium); }
.cb-noreceipt { color: var(--warning-text); font-weight: var(--fw-medium); }
.cb-queue-acts form { display: contents; }
.cb-journals { margin: calc(var(--space-2) - var(--stack-gap)) 0 var(--stack-gap); font-size: var(--fs-xs); color: var(--text-muted); }
.cb-empty-title { font-weight: var(--fw-semibold); color: var(--text); margin-bottom: 2px; }
/* What is waiting, above everything else on the page: one line, and the
   blocked count is the only part that takes a colour. */
.cb-queue {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2) var(--space-4);
  padding: var(--space-3) var(--card-pad); margin-bottom: var(--stack-gap);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm); font-size: var(--fs-sm); color: var(--text);
}
.cb-queue-what { font-weight: var(--fw-semibold); }
.cb-queue-n { font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }
.cb-queue-blocked { display: inline-flex; align-items: center; gap: 6px; color: var(--warning-text); font-weight: var(--fw-medium); }
.cb-queue-blocked svg { width: 14px; height: 14px; }
.cb-queue-acts { display: flex; gap: var(--space-2); margin-left: auto; }
@media (max-width: 640px) { .cb-queue-acts { margin-left: 0; } }

/* The add-entry dialog. Direction is two answers that say what they cover; the
   chosen one takes its tone, the way an amount in that column does. */
.cb-modal { max-width: 560px; }
.cb-dir { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); border: 0; padding: 0; margin: 0 0 var(--field-gap); min-width: 0; }
.cb-dir legend { padding: 0; margin-bottom: 6px; font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--text-label); }
.cb-dir-opt { position: relative; margin: 0; cursor: pointer; }
.cb-dir-opt input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.cb-dir-box {
  display: flex; flex-direction: column; gap: 2px; height: 100%;
  padding: var(--space-3); border: 1px solid var(--control-border); border-radius: var(--radius-sm);
  background: var(--surface);
  transition: border-color var(--transition), background-color var(--transition), box-shadow var(--transition);
}
.cb-dir-box strong { display: flex; align-items: center; gap: 6px; font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text); }
.cb-dir-box small  { font-size: var(--fs-xs); font-weight: 400; color: var(--text-muted); }
.cb-dir-box svg    { width: 15px; height: 15px; }
.cb-dir-box.is-in svg  { color: var(--success-text); }
.cb-dir-box.is-out svg { color: var(--danger-text); }
.cb-dir-opt:hover .cb-dir-box { border-color: var(--control-border-hover); }
.cb-dir-opt input:focus-visible + .cb-dir-box { border-color: var(--accent); box-shadow: var(--focus-ring); }
.cb-dir-opt input:checked + .is-in  { border-color: var(--success); background: var(--success-bg); }
.cb-dir-opt input:checked + .is-out { border-color: var(--danger);  background: var(--danger-bg); }
.cb-opt { font-weight: 400; color: var(--text-muted); font-size: var(--fs-xs); }

/* An amount with its currency in front, as one control. */
.money-field { display: flex; align-items: stretch; height: var(--control-h); }
.money-field > span {
  display: flex; align-items: center; padding: 0 var(--space-3);
  border: 1px solid var(--control-border); border-right: 0;
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
  background: var(--surface-2); color: var(--text-muted);
  font-size: var(--fs-sm); font-weight: var(--fw-medium);
}
.money-field > input { flex: 1; min-width: 0; height: 100%; border-radius: 0 var(--radius-sm) var(--radius-sm) 0; font-variant-numeric: tabular-nums; }

/* ── Settings → Standby cash (admin/standby-accounts.php) ───────────────────
   One card per question, sections inside it divided by a hairline, the save
   bar at the foot of the card it saves. */
/* Payroll → Standby Cash: what AutoCount holds beside each balance (2026-10-14).
   The amount, then ONE note saying what a difference means, in the status
   colours' own words — green it matches, amber not sent yet, red AutoCount holds
   something this app did not send. Filled after the tab is on screen. */
.pc-ac-col { text-align: right; width: 210px; }
.pc-ac-amt { display: block; font-weight: var(--fw-semibold); color: var(--text); font-variant-numeric: tabular-nums; }
.pc-ac-note { display: inline-flex; align-items: center; gap: 5px; margin-top: 2px;
              font-size: var(--fs-xs); font-weight: var(--fw-medium); color: var(--text-muted); }
.pc-ac-note::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: .85; }
.pc-ac-note:not([class*="is-"])::before { display: none; }
.pc-ac-note.is-ok   { color: var(--success-text); }
.pc-ac-note.is-warn { color: var(--warning-text); }
.pc-ac-note.is-bad  { color: var(--danger-text); }
.pc-ac-why { display: block; font-size: var(--fs-xs); color: var(--text-muted); }
.pc-ac-status { margin-left: var(--space-1); }
.pc-ac-wait { display: inline-block; width: 72px; height: 10px; border-radius: var(--radius-pill);
              background: var(--surface-3); animation: pcAcPulse 1.2s ease-in-out infinite alternate; }
@keyframes pcAcPulse { from { opacity: .45; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .pc-ac-wait { animation: none; } }

.sa-card { padding: 0; overflow: hidden; }
.sa-sec { padding: var(--card-pad); }
.sa-sec + .sa-sec { border-top: 1px solid var(--border); }
.sa-sec-head { max-width: 72ch; margin-bottom: var(--space-4); }
.sa-sec-title { font-size: var(--fs-md); font-weight: var(--fw-semibold); color: var(--text); line-height: var(--lh-tight); }
.sa-sec-sub { margin-top: 3px; font-size: var(--fs-sm); color: var(--text-muted); line-height: 1.5; }
.sa-fields { display: flex; flex-wrap: wrap; gap: var(--field-gap) var(--space-6); }
.sa-field { flex: 0 1 300px; }
.sa-field label { display: flex; align-items: center; gap: 6px; }
/* "Where a top-up's money comes from": two editable tables STACKED on one
   column grid (the <colgroup> widths below), capped so an account picker does
   not stretch across a wide monitor. Side by side, each picker sized itself to
   its longest account name and squeezed the name box beside it to two letters. */
.sa-stack { display: grid; gap: var(--space-6); max-width: 68rem; }
.sa-stack > * { min-width: 0; }            /* a grid item will not shrink below its table otherwise */
.sa-wrap { overflow-x: auto; }               /* so a narrow screen scrolls the table, never clips it */
.sa-col-name { width: 12rem; }
.sa-col-side { width: 12.5rem; }   /* room for "— Choose —" inside the cell's padding */
.sa-col-x    { width: 48px; }
.sa-sub { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text); }
.sa-doc { padding: 1px var(--space-2); border: 1px solid var(--border); border-radius: var(--radius-pill); font-size: var(--fs-xs); font-weight: var(--fw-medium); color: var(--text-muted); }
.sa-table { width: 100%; }
.sa-stack .sa-table { table-layout: fixed; min-width: 560px; }
.sa-table td { vertical-align: middle; }
.sa-table input[type="text"], .sa-table select { width: 100%; }
.sa-table .sa-method { text-transform: uppercase; }
.sa-table .sa-method::placeholder { text-transform: none; }
.sa-c { width: 76px; text-align: center; }
.sa-x { text-align: right; }
.sa-x .icon-btn { display: inline-grid; vertical-align: middle; }
.sa-default { width: 16px; height: 16px; margin-left: var(--space-2); accent-color: var(--accent); vertical-align: middle; }
.sa-new td { background: var(--surface-2); }
.sa-acc-no { width: 8.5rem; font-variant-numeric: tabular-nums; }
.sa-field .sa-acc-pick, .sa-field .ui-select { width: 100%; }
.sa-who { font-weight: var(--fw-medium); color: var(--text); }
.sa-code { font-size: var(--fs-xs); color: var(--text-muted); }
.sa-warn { color: var(--warning-text); font-weight: var(--fw-medium); }
.sa-actions {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-3);
  padding: var(--space-3) var(--card-pad);
  border-top: 1px solid var(--border); background: var(--surface-2);
}

/* ============================================================
   AUTH SURFACES — sign in, register, password reset
   Split layout: a committed dark brand panel carrying the route
   motif (origin → detour → destination, the same shape as a trip
   record), beside a restrained white form panel.
   ============================================================ */

/* Motion scale — one place to retune every auth transition */
:root {
  --auth-swap:  620ms;
  --auth-ease:  cubic-bezier(0.22, 1, 0.36, 1);   /* ease-out-quint */
  --z-auth-brand: 2;
  --z-auth-form:  3;
}

.auth-page {
  min-height: 100vh;
  min-height: 100svh;
  background: var(--card);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

/* ── Two-panel shell ───────────────────────────────────────────
   Both halves are absolutely placed and moved with transform only,
   so swapping sides costs no layout and cannot shift the fields
   under the user's cursor mid-transition. `perspective` on the
   shell gives the form cross-fade real depth rather than a flat
   dissolve. */
.auth-shell {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  overflow: hidden;
  perspective: 1800px;
}

.auth-side {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 50%;
  /* Flex column rather than leaving the child on height:100% — a percentage
     height against an auto-height abs-positioned parent is exactly the case
     browsers resolve inconsistently, and it silently left the form top-aligned. */
  display: flex;
  flex-direction: column;
  transition: transform var(--auth-swap) var(--auth-ease);
  will-change: transform;
}

/* Sign-in: form on the right, brand on the left.
   Register: they trade places, so the form arrives on the left. */
.auth-side-forms { left: 50%; z-index: var(--z-auth-form); }
.auth-side-brand { left: 0;   z-index: var(--z-auth-brand); }

.auth-page.mode-register .auth-side-forms { transform: translateX(-100%); }
.auth-page.mode-register .auth-side-brand { transform: translateX(100%); }

/* ── Brand panel ───────────────────────────────────────────── */
.auth-brand {
  position: relative;
  overflow: hidden;
  flex: 1;
  min-height: 0;
  padding: 3rem 3.25rem;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  background: var(--rt-navy-800);
  color: var(--rt-navy-200);
  isolation: isolate;
  /* Copy always faces the form. Sign-in puts the brand on the left, so its
     text sets right; register moves it to the right, so its text sets left. */
  text-align: right;
  align-items: flex-end;
}
.mode-register .auth-brand {
  text-align: left;
  align-items: flex-start;
}

/* Soft light source behind the route, keeps the panel from reading flat.
   Its position tracks the pointer via --px/--py (set in auth.js, defaulted
   here so it renders correctly with JS off). */
.auth-brand {
  --px: 40%;
  --py: 30%;
}
.auth-brand::before {
  content: '';
  position: absolute;
  inset: -30% -20% auto -20%;
  height: 110%;
  background: radial-gradient(ellipse at var(--px) var(--py), rgba(42, 87, 147, 0.55), transparent 62%);
  z-index: -1;
  transition: background 400ms ease-out;
}

/* Faint engineering grid — texture without becoming a pattern */
.auth-brand::after {
  content: '';
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(148, 163, 184, 0.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(148, 163, 184, 0.055) 1px, transparent 1px);
  background-size: 46px 46px;
  mask-image: radial-gradient(ellipse at 50% 40%, #000 20%, transparent 78%);
  -webkit-mask-image: radial-gradient(ellipse at 50% 40%, #000 20%, transparent 78%);
  z-index: -1;
  pointer-events: none;
}

.auth-brand-mark {
  display: flex;
  align-items: center;
  gap: 12px;
}
.auth-brand-badge {
  width: 42px; height: 42px;
  border-radius: var(--radius);
  display: grid; place-items: center;
  font-weight: 600; font-size: var(--fs-sm); letter-spacing: 0.02em;
  color: var(--rt-navy-800);
  background: linear-gradient(135deg, var(--rt-gold-500), var(--rt-gold-600));
  flex-shrink: 0;
}
.auth-brand-mark { flex-shrink: 0; display: block; }
.auth-brand-name { font-size: var(--fs-sm); font-weight: 600; color: var(--surface-2); line-height: 1.25; }
.auth-brand-co   { font-size: var(--fs-xs); color: var(--rt-navy-300); letter-spacing: 0.08em; text-transform: uppercase; }

/* Lockup and footer mirror with the panel, so nothing is left hanging off the
   outer edge while the copy faces inward. */
.auth-brand-mark-row { flex-direction: row-reverse; }
.mode-register .auth-brand-mark-row { flex-direction: row; }

.auth-brand-foot { justify-content: flex-end; }
.mode-register .auth-brand-foot { justify-content: flex-start; }

.auth-brand-copy { max-width: 30ch; margin-left: auto; }
.mode-register .auth-brand-copy { margin-left: 0; margin-right: auto; }

/* Typewriter: the headline retypes when the mode flips, so the panel reads as
   speaking rather than swapping. Height is reserved by the longest line so the
   block never reflows mid-type. */
.auth-brand-copy h2 { min-height: 3.55em; }
.auth-type-caret {
  display: inline-block;
  width: 3px;
  height: 0.86em;
  margin-left: 3px;
  vertical-align: -0.06em;
  background: var(--rt-gold-500);
  animation: caretBlink 1s steps(1) infinite;
}
.auth-type-caret[hidden] { display: none; }
@keyframes caretBlink { 50% { opacity: 0; } }
.auth-brand-copy h2 {
  font-size: var(--fs-xl);
  line-height: 1.18;
  font-weight: 600;
  color: var(--surface-2);
  letter-spacing: -0.02em;
  text-wrap: balance;
}
.auth-brand-copy p {
  margin-top: 0.85rem;
  font-size: var(--fs-sm);
  line-height: 1.65;
  color: #9fb0c7;
}

/* ── Route motif ───────────────────────────────────────────── */
/* The illustration tracks the copy block rather than the panel, so it doesn't
   sit adrift on the far side while the text is aligned inward. */
.auth-route { margin: 2.25rem 0; max-width: 380px; margin-left: auto; }
.mode-register .auth-route { margin-left: 0; margin-right: auto; }
.auth-route svg { width: 100%; height: auto; overflow: visible; display: block; }

.auth-route .route-line {
  stroke: rgba(148, 163, 184, 0.35);
  stroke-width: 2;
  stroke-dasharray: 7 9;
  fill: none;
}
/* The dashes travel the road. The line is fully drawn at rest, so the panel
   never renders blank if the animation is suppressed or never starts. */
.auth-route .route-line-live {
  stroke: #60a5fa;
  stroke-width: 2;
  stroke-dasharray: 7 9;
  fill: none;
  animation: routeFlow 2.6s linear infinite;
}
@keyframes routeFlow { to { stroke-dashoffset: -32; } }

.auth-route .route-node   { fill: #0d1b2e; stroke: #60a5fa; stroke-width: 2.5; }
.auth-route .route-node-b { fill: #60a5fa; stroke: #60a5fa; }
.auth-route .route-label  { fill: var(--rt-navy-300); font-size: var(--fs-xs); font-weight: 600; letter-spacing: 0.09em; text-transform: uppercase; }
.auth-route .route-truck  { fill: #fbbf24; }

.auth-brand-foot {
  font-size: var(--fs-xs);
  color: #64789a;
  display: flex;
  gap: 1.25rem;
  flex-wrap: wrap;
}
.auth-brand-foot a { color: #8ea3bd; }
.auth-brand-foot a:hover { color: var(--border-strong); }

/* ── Mode-differentiated illustration ────────────────────────
   One idea per mode, rather than three competing at once:
     login    — the ROUTE is the hero. The truck runs the road;
                the shield sits back, small and quiet.
     register — the SHIELD is the hero. It scales up and draws
                its check; the road recedes to a faint line.
   Both states are reachable from the same markup, so the swap
   is a class change on <body> and needs no JS. */

.auth-route .illo-shield,
.auth-route .illo-route {
  transition:
    opacity   var(--auth-swap) var(--auth-ease),
    transform var(--auth-swap) var(--auth-ease);
  transform-box: fill-box;
  transform-origin: center;
}

/* Login: route forward, shield receded */
.mode-login .auth-route .illo-route  { opacity: 1;   transform: none; }
.mode-login .auth-route .illo-shield { opacity: .45; transform: scale(.7) translateY(6px); }

/* Register: shield forward, route receded */
.mode-register .auth-route .illo-shield { opacity: 1;   transform: scale(1.35) translateY(10px); }
.mode-register .auth-route .illo-route  { opacity: .28; transform: translateY(10px); }

.auth-route .shield-body {
  fill: rgba(42, 87, 147, 0.35);
  stroke: var(--rt-navy-300);
  stroke-width: 2;
}
.auth-route .shield-check {
  stroke: var(--rt-gold-500);
  stroke-width: 2.6;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 26;
  stroke-dashoffset: 26;       /* drawn in only when the shield is the hero */
}
.mode-register .auth-route .shield-check {
  animation: tickIn 520ms var(--auth-ease) 220ms forwards;
}
.mode-login .auth-route .shield-check { stroke-dashoffset: 0; }
@keyframes tickIn { to { stroke-dashoffset: 0; } }

.auth-route .node-link    { stroke: rgba(169, 195, 227, 0.28); stroke-width: 1.5; }
.auth-route .node-dot     { fill: var(--rt-navy-300); }
.auth-route .node-dot.dim { fill: rgba(169, 195, 227, 0.4); }

/* The truck drives the road in login mode, and parks in register mode.
   offset-path is transform-only, so it composites on the GPU. */
.auth-route .route-truck {
  fill: var(--rt-gold-500);
  offset-path: path('M20 122 C 90 122, 100 96, 168 96 S 250 122, 320 122');
  offset-rotate: 0deg;
  offset-distance: 62%;
  transition: offset-distance var(--auth-swap) var(--auth-ease);
}
.mode-login .auth-route .route-truck {
  animation: truckRun 9s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
@keyframes truckRun {
  0%        { offset-distance: 2%;  opacity: 0; }
  8%        { opacity: 1; }
  92%       { opacity: 1; }
  100%      { offset-distance: 98%; opacity: 0; }
}

/* Drifting motes. Transform-only, three of them — ambient, not a particle field. */
.auth-mote {
  position: absolute;
  border-radius: 50%;
  background: rgba(96, 165, 250, 0.5);
  filter: blur(0.5px);
  animation: moteDrift 14s ease-in-out infinite;
  pointer-events: none;
}
.auth-mote:nth-child(1) { width: 5px; height: 5px; top: 24%; left: 16%; animation-delay: 0s;    }
.auth-mote:nth-child(2) { width: 3px; height: 3px; top: 62%; left: 74%; animation-delay: -4.5s; }
.auth-mote:nth-child(3) { width: 4px; height: 4px; top: 78%; left: 33%; animation-delay: -9s;   }
@keyframes moteDrift {
  0%, 100% { transform: translate3d(0, 0, 0);        opacity: 0.25; }
  50%      { transform: translate3d(10px, -22px, 0); opacity: 0.7;  }
}

/* ── Form panel ────────────────────────────────────────────── */
.auth-panel {
  flex: 1;
  min-height: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  /* `safe` keeps a form taller than the viewport reachable: it falls back to
     start-alignment instead of overflowing past the top where the scrollbar
     can't reach it. Browsers without `safe` keep the plain `center` above. */
  align-items: safe center;
  padding: 3rem 2rem;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.auth-form-wrap { width: 100%; max-width: 384px; }

/* ── Login ⇄ register cross-fade ────────────────────────────
   Both panes occupy the same grid cell so the shell never changes
   height mid-swap. The outgoing pane rotates away from the viewer;
   the incoming one arrives flat. */
.auth-swap { display: grid; }
.auth-pane {
  grid-area: 1 / 1;
  width: 100%;
  transition:
    opacity   calc(var(--auth-swap) * 0.55) ease-out,
    transform var(--auth-swap) var(--auth-ease);
  transform-origin: center left;
  backface-visibility: hidden;
}
.auth-pane[hidden] { display: block; }         /* animate it out, don't snap it away */
.auth-pane.is-out {
  opacity: 0;
  transform: rotateY(-14deg) translateZ(-60px);
  pointer-events: none;
}
/* Once the swap has finished, take the hidden pane out of the layout, the
   a11y tree and the tab order entirely. JS adds .is-gone on transitionend.
   `display: none` rather than `visibility: hidden` — the two panes share one
   grid cell, and a merely-invisible pane still sets that cell's height, which
   makes the shorter form sit at the top of a register-form-sized box instead
   of centred in the panel. */
.auth-pane.is-gone { display: none; }

/* Mode switch link/button row */
.auth-switch {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--border);
  font-size: var(--fs-sm);
  color: var(--text-muted);
  text-align: center;
}
/* Items sit on one line, centred, with even spacing — a nested <form> is a
   block box and would otherwise stack the button above the link. */
.auth-switch {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 0.4rem;
}
.auth-switch-form { display: contents; }
.auth-switch-sep  { color: var(--text-dim); }

.auth-switch a,
.auth-switch button {
  background: none;
  border: none;
  padding: 0;
  font-size: inherit;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
  cursor: pointer;
}
.auth-switch a:hover,
.auth-switch button:hover { text-decoration: underline; }

/* ── Social sign-in ────────────────────────────────────────── */
.auth-or {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  margin: 1.35rem 0 1rem;
  font-size: var(--fs-xs);
  color: var(--text-dim);
  white-space: nowrap;
}
.auth-or::before,
.auth-or::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}

.sso-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
.sso-row:has(> :only-child) { grid-template-columns: 1fr; }

.sso-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  height: 42px;
  padding: 0 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--card);
  color: var(--text);
  font-size: var(--fs-sm);
  font-weight: 600;
  text-decoration: none;
  transition: background var(--transition), border-color var(--transition), box-shadow var(--transition);
}
.sso-btn:hover {
  background: var(--card-hover);
  border-color: var(--border-strong);
  text-decoration: none;
  color: var(--text);
  box-shadow: var(--shadow-sm);
}
.sso-btn svg { flex-shrink: 0; }
.sso-btn span { overflow: hidden; text-overflow: ellipsis; }

/* ── Remember me ───────────────────────────────────────────── */
.auth-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.1rem;
  flex-wrap: wrap;
}
.check-inline {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--text-muted);
  text-transform: none;
  letter-spacing: 0;
  margin: 0;
  cursor: pointer;
  user-select: none;
}
.check-inline input {
  width: 16px; height: 16px;
  margin: 0;
  accent-color: var(--accent);
  flex-shrink: 0;
  cursor: pointer;
}
.check-inline:hover { color: var(--text); }

/* Terms acceptance — same control, allowed to wrap to several lines */
.check-start { align-items: flex-start; line-height: 1.55; }
.check-start input { margin-top: 2px; }

/* `.auth-field > label` is the uppercase field-label style and out-specifies
   `.check-inline`, which would set this sentence in all caps. */
.auth-field > label.check-inline {
  display: inline-flex;
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--text-muted);
}
.auth-field > label.check-inline a { font-size: inherit; }

.auth-head { margin-bottom: 1.75rem; }
.auth-head h1 {
  font-size: var(--fs-lg);
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--text);
  margin-bottom: 0.35rem;
}
.auth-head p { font-size: var(--fs-sm); color: var(--text-muted); line-height: 1.6; }

/* Mobile-only brand bar; hidden while the full panel is on screen */
.auth-mobile-mark { display: none; align-items: center; gap: 10px; margin-bottom: 1.75rem; }

.auth-field { margin-bottom: 1.05rem; }
.auth-field > label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 0.4rem;
}
.auth-field > label a { font-size: var(--fs-xs); font-weight: 600; text-transform: none; letter-spacing: 0; }

.auth-input {
  position: relative;
  display: flex;
  align-items: center;
}
.auth-input > svg {
  position: absolute;
  left: 12px;
  width: 16px; height: 16px;
  color: var(--text-dim);
  pointer-events: none;
  transition: color var(--transition);
}
.auth-input > input { padding-left: 38px; height: 42px; }
.auth-input:focus-within > svg { color: var(--accent); }

/* Edge and IE draw their OWN reveal control inside every password field, and it
   landed underneath this button — which is why the sign-in screen showed a native
   eye printed through the middle of the word SHOW. Chrome has no such control, so
   it looked fine on the machine it was built on and broken on Windows' default
   browser. Suppressing it is the only way: it cannot be moved or restyled. */
input[type="password"]::-ms-reveal,
input[type="password"]::-ms-clear { display: none; }

.auth-input-action {
  position: absolute;
  right: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  padding: 6px 8px;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-size: var(--fs-xs);
  font-weight: 600;
}
.auth-input-action:hover { color: var(--accent); background: var(--accent-dim); }

/* One control, two presentations, chosen by width rather than by JS. An eye is
   unambiguous next to a password field on a desktop; at phone width the same
   glyph is 16px of guesswork, and the word is worth its space there. Exactly
   one of them is ever displayed, so they cannot overlap the way the native
   control above did. */
.auth-pw-eye  { width: 18px; height: 18px; }
.auth-pw-word { display: none; }

/* Which eye: open means "click to reveal", struck through means "click to
   hide". Driven by aria-pressed so the visual state and the state a screen
   reader announces are the same fact, not two that can drift. */
.auth-input-action[aria-pressed="false"] .auth-pw-eye-hide,
.auth-input-action[aria-pressed="true"]  .auth-pw-eye-show { display: none; }

@media (max-width: 460px) {
  .auth-pw-eye  { display: none; }
  .auth-pw-word { display: inline; }
}

.auth-hint  { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 0.4rem; line-height: 1.5; }
.auth-error { font-size: var(--fs-xs); color: var(--danger-text); margin-top: 0.4rem; }

.auth-submit { width: 100%; justify-content: center; height: 42px; margin-top: 0.35rem; font-size: var(--fs-sm); }

.auth-alt {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--border);
  font-size: var(--fs-sm);
  color: var(--text-muted);
  text-align: center;
}
.auth-alt a { font-weight: 600; }

.auth-back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin-bottom: 1.25rem;
  font-weight: 500;
}
.auth-back:hover { color: var(--accent); text-decoration: none; }

/* Demo credential helper — development builds only */
.auth-demo {
  margin-top: 1.5rem;
  padding: 0.85rem 1rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: var(--fs-xs);
  color: var(--text-muted);
  line-height: 1.7;
}
.auth-demo strong { color: var(--text); font-weight: 600; }
.auth-demo code {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  padding: 1px 5px;
  font-size: var(--fs-xs);
  color: var(--text);
}

/* ── Reset flow step rail ──────────────────────────────────── */
.auth-steps {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 1.75rem;
  padding: 0;
  list-style: none;   /* it's an <ol> for semantics; the dots are the markers */
}
.auth-steps li::marker { content: none; }
.auth-step {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-dim);
}
.auth-step-dot {
  width: 22px; height: 22px;
  border-radius: 50%;
  display: grid; place-items: center;
  border: 1.5px solid var(--border);
  background: var(--card);
  font-size: var(--fs-xs);
  color: var(--text-dim);
  transition: background var(--transition), border-color var(--transition), color var(--transition);
}
.auth-step.is-current .auth-step-dot { border-color: var(--accent); background: var(--accent); color: #fff; }
.auth-step.is-current { color: var(--text); }
.auth-step.is-done .auth-step-dot { border-color: var(--status-completed); background: var(--status-completed); color: #fff; }
.auth-step.is-done { color: var(--text-muted); }
.auth-step-line { flex: 1; height: 1.5px; background: var(--border); border-radius: 2px; min-width: 12px; }
.auth-step-line.is-done { background: var(--status-completed); }

/* ── OTP entry ─────────────────────────────────────────────── */
.otp-inputs {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 8px;
  margin-bottom: 0.6rem;
}
.otp-inputs input {
  height: 52px;
  text-align: center;
  font-size: var(--fs-lg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  padding: 0;
  color: var(--text);
}
.otp-inputs input:focus { border-color: var(--accent); box-shadow: var(--focus-ring); }
.otp-inputs input.is-filled { border-color: rgba(37, 99, 235, 0.45); background: var(--accent-dim); }
.otp-inputs.is-wrong { animation: otpShake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97); }
@keyframes otpShake {
  10%, 90% { transform: translateX(-1.5px); }
  20%, 80% { transform: translateX(3px); }
  30%, 50%, 70% { transform: translateX(-5px); }
  40%, 60% { transform: translateX(5px); }
}

.otp-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin-bottom: 1.1rem;
}
.otp-resend {
  background: none;
  border: none;
  padding: 0;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--accent);
}
.otp-resend[disabled] { color: var(--text-dim); cursor: default; }
.otp-countdown { font-variant-numeric: tabular-nums; }

/* ── Recovery method choice ────────────────────────────────── */
.method-cards { display: grid; gap: 0.6rem; margin-bottom: 1.25rem; }
.method-card {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.95rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition);
  text-transform: none;
  letter-spacing: 0;
  margin: 0;
  font-weight: 400;
}
.method-card:hover { background: var(--card-hover); }
.method-card:has(input:checked) { border-color: var(--accent); background: var(--accent-dim); }
.method-card:focus-within { border-color: var(--accent); box-shadow: var(--focus-ring); }
.method-card input { width: auto; margin: 0; accent-color: var(--accent); flex-shrink: 0; }
.method-card-icon {
  width: 36px; height: 36px;
  border-radius: var(--radius);
  display: grid; place-items: center;
  background: var(--bg);
  color: var(--text-muted);
  flex-shrink: 0;
}
.method-card:has(input:checked) .method-card-icon { background: rgba(37, 99, 235, 0.14); color: var(--accent); }
.method-card-icon svg { width: 17px; height: 17px; }
.method-card-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.method-card-name { font-size: var(--fs-sm); font-weight: 600; color: var(--text); }
.method-card-desc { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.45; }

/* ── Phone with country code ───────────────────────────────── */
.phone-group { display: flex; gap: 8px; }
.phone-group select {
  width: auto;
  min-width: 108px;
  flex-shrink: 0;
  height: 42px;
  font-size: var(--fs-sm);
  background-image: none;
}
.phone-group .auth-input { flex: 1; min-width: 0; }

/* ── Password requirement checklist ────────────────────────── */
.pw-rules {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 2px 0.9rem;
  margin-top: 0.6rem;
}
.pw-rule {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: var(--fs-xs);
  color: var(--text-muted);
  transition: color var(--transition);
}
.pw-rule-mark {
  width: 14px; height: 14px;
  border-radius: 50%;
  border: 1.5px solid var(--border);
  display: grid; place-items: center;
  flex-shrink: 0;
  transition: background var(--transition), border-color var(--transition);
}
.pw-rule-mark svg { width: 9px; height: 9px; color: #fff; opacity: 0; transition: opacity var(--transition); }
.pw-rule.is-met { color: var(--status-completed); }
.pw-rule.is-met .pw-rule-mark { background: var(--status-completed); border-color: var(--status-completed); }
.pw-rule.is-met .pw-rule-mark svg { opacity: 1; }

/* ── Password strength ─────────────────────────────────────── */
.pw-meter { display: flex; gap: 4px; margin-top: 0.5rem; }
.pw-meter span {
  flex: 1;
  height: 3px;
  border-radius: 2px;
  background: var(--border);
  transition: background var(--transition);
}
.pw-meter[data-level="1"] span:nth-child(-n+1) { background: var(--status-overdue); }
.pw-meter[data-level="2"] span:nth-child(-n+2) { background: var(--status-pending); }
.pw-meter[data-level="3"] span:nth-child(-n+3) { background: var(--st-pickup); }
.pw-meter[data-level="4"] span { background: var(--status-completed); }

/* ── 403 / notice page ─────────────────────────────────────── */
.auth-notice {
  grid-column: 1 / -1;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  text-align: center;
  padding: 2rem;
  background: var(--bg);
}
.auth-notice-code  { font-size: var(--fs-xs); font-weight: 600; letter-spacing: 0.14em; color: var(--text-dim); }
.auth-notice-title { font-size: var(--fs-lg); font-weight: 600; color: var(--text); }
.auth-notice-text  { font-size: var(--fs-sm); color: var(--text-muted); max-width: 44ch; margin-bottom: 0.75rem; }

/* ── Success state ─────────────────────────────────────────── */
.auth-success { text-align: center; }
.auth-success-mark {
  width: 60px; height: 60px;
  margin: 0 auto 1.25rem;
  border-radius: 50%;
  display: grid; place-items: center;
  background: rgba(22, 163, 74, 0.1);
  color: var(--status-completed);
}
.auth-success-mark svg { width: 28px; height: 28px; }
.auth-success-mark svg path { animation: tickDraw 520ms var(--auth-ease) forwards; stroke-dasharray: 32; stroke-dashoffset: 0; }
@keyframes tickDraw { from { stroke-dashoffset: 32; } to { stroke-dashoffset: 0; } }
.auth-success h1 { font-size: var(--fs-lg); font-weight: 600; color: var(--text); margin-bottom: 0.5rem; }
.auth-success p  { font-size: var(--fs-sm); color: var(--text-muted); line-height: 1.6; }
.auth-redirect-note { font-size: var(--fs-xs); color: var(--text-dim); margin-top: 1rem; }

/* ── Invalid field state ───────────────────────────────────── */
.auth-input input[aria-invalid="true"],
input[aria-invalid="true"], select[aria-invalid="true"] {
  border-color: var(--status-overdue);
}
.auth-input input[aria-invalid="true"]:focus,
input[aria-invalid="true"]:focus {
  box-shadow: var(--focus-ring-danger);
}

/* Focus visibility — keyboard users must always see where they are */
.auth-page a:focus-visible,
.auth-page button:focus-visible,
.check-inline input:focus-visible,
.seg a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* ── Auth responsive ─────────────────────────────────────────
   Below the two-panel breakpoint the shell becomes normal flow:
   compact brand header, then the form. The side-swap transform is
   dropped so nothing can translate off-screen on a narrow device. */
@media (max-width: 900px) {
  .auth-shell { min-height: 0; overflow: visible; perspective: none; }
  .auth-side {
    position: static;
    width: 100%;
    transform: none !important;
  }
  .auth-side-brand { display: none; }
  .auth-mobile-mark { display: flex; }
  .auth-panel {
    padding: 2rem 1.25rem calc(2rem + env(safe-area-inset-bottom));
    align-items: flex-start;
    min-height: 100svh;
    overflow: visible;
  }
  .auth-pane { transform-origin: center; }
  .auth-pane.is-out { transform: translateY(10px); }   /* slide+fade, no rotation */
}

@media (max-width: 460px) {
  /* Three labels plus their connectors don't fit a phone. Keep the label of
     the step you're actually on — that's the one carrying the information —
     and let the others read as dots. */
  .auth-step:not(.is-current) span:not(.auth-step-dot) { display: none; }
  .auth-steps { gap: 6px; }
}

@media (max-width: 400px) {
  .auth-panel { padding: 1.5rem 1rem calc(1.5rem + env(safe-area-inset-bottom)); }
  .auth-head h1 { font-size: var(--fs-lg); }
  .otp-inputs { gap: 6px; }
  .otp-inputs input { height: 46px; font-size: var(--fs-md); }
  .pw-rules { grid-template-columns: 1fr 1fr; }
  .phone-group { flex-wrap: wrap; }
  .phone-group select { width: 100%; }
  .auth-row { gap: 0.5rem; }
}

/* Touch targets: 44px minimum on coarse pointers */
@media (pointer: coarse) {
  .auth-input > input,
  .auth-submit,
  .phone-group select { min-height: 44px; }
  .auth-input-action { min-height: 38px; min-width: 44px; }
  .check-inline input { width: 20px; height: 20px; }
}

/* ============================================================
   USER MANAGEMENT — admin roster and detail
   ============================================================ */

.user-cell { display: flex; align-items: center; gap: 0.7rem; min-width: 0; }

/* One avatar for everyone: the initial on a navy tint. A role is a category,
   not a state, so it gets no status hue — three roles painted and three not
   read as if the painted three meant something. */
.user-avatar-sm {
  width: 32px; height: 32px;
  border-radius: 50%;
  display: grid; place-items: center;
  font-weight: 600; font-size: var(--fs-xs);
  color: var(--rt-navy-600);
  background: var(--brand-tint);
  flex-shrink: 0;
}

.user-cell-text { min-width: 0; display: flex; flex-direction: column; line-height: 1.35; }
.user-cell-name { font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-cell-code {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.03em;
}

/* Staff ID chip, for detail headers and anywhere the code stands alone */
.code-chip {
  display: inline-block;
  padding: 2px 8px;
  border-radius: var(--radius-xs);
  background: var(--bg);
  border: 1px solid var(--border);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

.role-pill {
  display: inline-flex;
  align-items: center;
  height: 22px;
  padding: 0 var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--neutral-bg);
  color: var(--neutral-text);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  white-space: nowrap;
}

/* Status reads as a dot + word, so it doesn't compete with the role pill */
.status-tag {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--text-muted);
  white-space: nowrap;
}
.status-tag::before {
  content: '';
  width: 7px; height: 7px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}
.status-tag.is-active    { color: var(--status-completed); }
.status-tag.is-pending   { color: var(--status-pending); }
.status-tag.is-suspended { color: var(--status-overdue); }

/* Row emphasis for accounts waiting on the admin */
tbody tr.row-pending { background: rgba(217, 119, 6, 0.04); }
tbody tr.row-pending:hover { background: rgba(217, 119, 6, 0.08); }
tbody tr.row-suspended .user-cell-name { color: var(--text-muted); }
tbody tr.row-suspended .user-avatar-sm { opacity: 0.55; }

/* ── Filter rail ───────────────────────────────────────────── */
.user-toolbar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-bottom: var(--stack-gap);
}
.user-search { position: relative; flex: 1 1 240px; max-width: 340px; }
.user-search > svg {
  position: absolute;
  left: 11px; top: 50%;
  transform: translateY(-50%);
  width: 15px; height: 15px;
  color: var(--text-dim);
  pointer-events: none;
}
.user-search input { padding-left: 34px; height: var(--control-h); }

/* WRAPS, the same call .sub-nav already makes and for the same reason: five
   status filters with their counts come to 482px, which on a phone pushed the
   whole roster page sideways — the top bar with it, since the body then scrolls
   as one. Wrapping to a second line beats scrolling, because a filter strip
   that scrolls hides the options it exists to offer. */
.seg {
  display: inline-flex;
  flex-wrap: wrap;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 2px;
  gap: 2px;
}
.seg a {
  padding: 5px 13px;
  border-radius: var(--radius-sm);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-muted);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--transition), color var(--transition);
}
.seg a:hover { color: var(--text); text-decoration: none; }
.seg a.active { background: var(--card); color: var(--accent); box-shadow: var(--shadow-sm); }
.seg a .seg-count {
  display: inline-block;
  margin-left: 5px;
  font-size: var(--fs-xs);
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.seg a.active .seg-count { color: var(--accent); }

/* ── Row actions ───────────────────────────────────────────── */
.row-actions { display: flex; gap: 0.35rem; align-items: center; justify-content: flex-end; }
.row-actions form { display: inline; }

/* .icon-btn lives with the other buttons, in the Buttons section. */

/* ── Timeline (auth history) ───────────────────────────────── */
.timeline { display: flex; flex-direction: column; }
.timeline-row {
  display: grid;
  grid-template-columns: 18px 1fr auto;
  gap: 0.85rem;
  align-items: start;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--border);
  font-size: var(--fs-xs);
}
.timeline-row:last-child { border-bottom: none; }
.timeline-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--border);
  margin-top: 7px;
  margin-left: 5px;
}
.timeline-dot.ev-ok     { background: var(--status-completed); }
.timeline-dot.ev-fail   { background: var(--status-overdue); }
.timeline-dot.ev-warn   { background: var(--status-pending); }
.timeline-what { color: var(--text); }
.timeline-sub  { color: var(--text-muted); font-size: var(--fs-xs); margin-top: 1px; }
.timeline-when { color: var(--text-dim); font-size: var(--fs-xs); white-space: nowrap; font-variant-numeric: tabular-nums; }

/* ── Modal helpers used by the user forms ──────────────────── */
.field-note {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin-top: 0.35rem;
  line-height: 1.5;
}
.radio-cards { display: grid; gap: 0.5rem; }
.radio-card {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  padding: 0.7rem 0.85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition);
}
.radio-card:hover { background: var(--card-hover); }
.radio-card:has(input:checked) { border-color: var(--accent); background: var(--accent-dim); }
.radio-card input { width: auto; margin-top: 3px; flex-shrink: 0; accent-color: var(--accent); }
.radio-card-text { display: flex; flex-direction: column; gap: 2px; }
.radio-card-name { font-size: var(--fs-sm); font-weight: 600; color: var(--text); text-transform: none; letter-spacing: 0; }
.radio-card-desc { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.5; }

/* ── Motion opt-out ────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  /* The panel swap becomes an instant side change; the forms cross-fade only.
     No rotation, no travel, no drifting. */
  .auth-side { transition: none; }
  .auth-pane {
    transition: opacity 120ms linear;
    transform: none !important;
  }
  .auth-pane.is-out { transform: none !important; }

  .auth-route .route-line-live { animation: none; }
  .auth-mote { animation: none; opacity: 0.35; }
  .auth-brand::before { transition: none; }
  .auth-success-mark svg path { animation: none; stroke-dashoffset: 0; }
  .otp-inputs.is-wrong { animation: none; outline: 2px solid var(--status-overdue); outline-offset: 2px; }
  .modal, .modal-box { animation: none; }
  * { scroll-behavior: auto !important; }
}

/* ── Finance: view-switcher pills ──────────────────────────── */
/* --stack-gap, not 1.25rem: this separates one page section from the next, and
   1.25rem lands on 18.75px — off the 4px scale and 7px wider than every other
   section break on the page. Measured on Finance and Invoices, where the pills
   sat noticeably further from the card below them than anything else did. */
.view-pills { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: var(--stack-gap); }
.view-pill {
  padding: 6px 18px;
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-decoration: none;
  border: 1px solid var(--control-border);
  color: var(--text-muted);
  transition: background var(--transition), color var(--transition), border-color var(--transition);
  white-space: nowrap;
}
.view-pill:hover { background: var(--surface-2); color: var(--text); border-color: var(--control-border-hover); text-decoration: none; }
.view-pill.active {
  background: var(--accent-dim);
  color: var(--accent);
  border-color: rgba(37,99,235,0.3);
}

/* ════════════════════════════════════════════════════════════════════════════
   Table pagination
   Sits inside the card, below the table — page size on the left, position and
   controls on the right. See components/pagination.php.
   ════════════════════════════════════════════════════════════════════════════ */
.pager {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  /* Thin on purpose (2026-10-09): every pixel of this bar is a table row the
     pinned card cannot show. 6 + 28 + 6, the in-row control height. */
  padding: 6px var(--space-4);
  border-top: 1px solid var(--border);
  background: var(--bg);
  border-radius: 0 0 var(--radius) var(--radius);
}
.pager-size { display: flex; align-items: center; gap: 8px; margin: 0; }
.pager-size label {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin: 0;
  white-space: nowrap;
}
/* Compact variant. The chevron itself comes from the global `select` rule. */
.pager-size select {
  width: auto;
  min-width: 74px;
  height: var(--control-h-sm);
  padding: 0 28px 0 10px;
  font-size: var(--fs-xs);
  background-position: right 9px center;
  background-size: 13px;
}
.pager-range {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.pager-range strong { color: var(--text); font-weight: 600; }

.pager-nav { display: flex; align-items: center; gap: 4px; }
.pager-btn {
  min-width: var(--control-h-sm);
  height: var(--control-h-sm);
  padding: 0 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--control-border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text-muted);
  font-size: var(--fs-xs);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-decoration: none;
  transition: background var(--transition), color var(--transition), border-color var(--transition);
}
.pager-btn svg { width: 14px; height: 14px; }
.pager-btn:hover { background: var(--surface-2); color: var(--text); border-color: var(--control-border-hover); text-decoration: none; }
.pager-btn:focus-visible { outline: none; box-shadow: var(--focus-ring); border-color: var(--accent); }
.pager-btn.is-current {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  cursor: default;
}
.pager-btn.is-disabled { background: var(--surface-3); border-color: var(--border); color: var(--text-dim); pointer-events: none; }
.pager-gap { padding: 0 2px; color: var(--text-dim); font-size: var(--fs-xs); }

/* A table that ends in a pager shouldn't also have a rounded bottom of its own. */
.card > .table-wrapper + .pager { margin-top: 0; }

@media (max-width: 640px) {
  .pager { gap: 10px; }
  .pager-range { margin-left: 0; width: 100%; order: 3; }
  .pager-nav   { margin-left: auto; }
}

/* ════════════════════════════════════════════════════════════════════════════
   Classes the markup referenced but the stylesheet never defined
   ────────────────────────────────────────────────────────────────────────────
   Each of these was in use on a live page and rendering completely unstyled —
   .page-sub on four pages, .user-badge and .topbar-title on every page via the
   header. Same class of defect as the search input: written into the markup,
   never given a rule.
   ════════════════════════════════════════════════════════════════════════════ */

/* Subtitle under a page title — row counts, short context. */
.page-sub {
  font-size: var(--fs-sm);
  color: var(--text-muted);
  margin-top: 3px;
}

/* Top bar: the page name, and the signed-in user's role chip. */
.topbar-title {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.user-badge {
  display: inline-flex;
  align-items: center;
  height: 22px;
  padding: 0 var(--space-2);
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  background: var(--neutral-bg);
  color: var(--neutral-text);
  white-space: nowrap;
}

/* Sidebar item label — sizing lives here rather than being inherited by luck. */
.nav-label { font-size: var(--fs-sm); font-weight: 500; }

/* Card footer holding a single "view all" style link. */
.card-footer {
  padding: 12px 18px;
  border-top: 1px solid var(--border);
  background: var(--bg);
  border-radius: 0 0 var(--radius) var(--radius);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.link-muted {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-muted);
  text-decoration: none;
  transition: color var(--transition);
}
.link-muted:hover { color: var(--accent); text-decoration: none; }

/* Helper text under a form control. */
.form-hint {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin-top: 5px;
  line-height: 1.5;
}

/* Wrapper around a tab bar and its panels. */
.tabs-container { display: block; }

/* Invoice editor: a line-item row and its right-aligned amount cell. */
.line-item td { vertical-align: middle; }
.row-amount   { text-align: right; font-variant-numeric: tabular-nums; }

/* Stat-card icon tint used by the petty-cash summary. */
.stat-icon.yellow { background: rgba(234, 179, 8, 0.12); color: #ca8a04; }

/* Inline brand mark emitted by components/brand-mark.php. */
.brand-mark { display: inline-flex; align-items: center; vertical-align: middle; }
.brand-mark svg { display: block; }

/* ════════════════════════════════════════════════════════════════════════════
   List toolbar — the row above a table holding search, filters and actions.
   One definition so search looks the same on Customers, Trips, Users and the
   AutoCount tabs, instead of each page hand-rolling its own.
   ════════════════════════════════════════════════════════════════════════════ */
.toolbar-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: var(--stack-gap);
}
.toolbar-row > .view-pills { margin-bottom: 0; }
.toolbar-spacer { margin-left: auto; }

/* Search box with a leading icon. The icon is positioned over the input, so the
   input keeps its normal styling and focus ring. */
.search-field {
  position: relative;
  flex: 1 1 260px;
  max-width: 360px;
  margin: 0;
}
.search-field > svg {
  position: absolute;
  left: 11px;
  top: 50%;
  transform: translateY(-50%);
  width: 15px;
  height: 15px;
  color: var(--text-dim);
  pointer-events: none;
  transition: color var(--transition);
}
.search-field > input {
  padding-left: 34px;
  height: var(--control-h);
}
.search-field:focus-within > svg { color: var(--accent); }

/* ════════════════════════════════════════════════════════════════════════════
   Table container
   ────────────────────────────────────────────────────────────────────────────
   .table-wrapper carries its own border and radius, which is right when it
   stands alone but draws a second border just inside the card when it's nested
   in one — visible on Cashbook, Invoices and Customers. These two rules make a
   table look the same whether it sits in a .card or a .table-card, and give the
   pager somewhere to attach that isn't inside the horizontal scroll area.
   ════════════════════════════════════════════════════════════════════════════ */
.table-card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.table-card > .table-wrapper,
.card > .table-wrapper {
  border: none;
  border-radius: 0;
}
/* Clip the table's corners to the card's radius. */
.card:has(> .table-wrapper) { overflow: hidden; }
/* A pager is the last thing in the container, so it owns the bottom corners. */
.table-card > .pager:last-child,
.card > .pager:last-child { border-radius: 0; }

/* ════════════════════════════════════════════════════════════════════════════
   Scrolling tables
   ────────────────────────────────────────────────────────────────────────────
   A long list scrolls inside its own container instead of pushing the page
   down, so the filters above it and the pager below it stay put — you pick a
   page size and change pages without scrolling back up to find the controls.

   The header sticks to the top of that scroll area. Without it, scrolling row
   200 leaves you looking at seven unlabelled columns.
   ════════════════════════════════════════════════════════════════════════════ */
.table-wrapper {
  /* Leave room for the top bar, page header, filters and the pager. Tuned so a
     laptop viewport shows a full table with its controls, and a tall screen
     shows more rows rather than more whitespace. */
  max-height: var(--table-h, calc(100vh - 340px));
  overflow: auto;
  overscroll-behavior: contain;   /* don't hand the scroll back to the page */
}
/* Taller variant for pages where the table is the whole point. */
.table-wrapper.is-tall { max-height: var(--table-h, calc(100vh - 280px)); }
/* A list that keeps ONE size whatever it holds (client, 2026-09-28: "the table
   footer stick the bottom so the table view not change"). A HEIGHT, not a
   max-height, so one row and fifty draw the same box: the totals row sits on
   the box's floor and the pager under it never moves as a filter narrows the
   list. The table fills the box and a spacer row (.tfill) takes the slack —
   every other row keeps its natural height. */
.table-wrapper.is-fill { height: var(--table-h, calc(100vh - 330px)); min-height: 260px; max-height: none; }
.table-wrapper.is-fill > table { height: 100%; }
.table-wrapper.is-fill tr.tfill td { height: 100%; padding: 0; border: 0; }
.table-wrapper.is-fill tr.tfill:hover { background: none; }
/* A phone scrolls the page, not a box inside it: a fixed height there only
   pushes the pager below the fold. */
@media (max-width: 720px) {
  .table-wrapper.is-fill { height: auto; min-height: 0; max-height: var(--table-h, calc(100vh - 340px)); }
  .table-wrapper.is-fill > table { height: auto; }
  .table-wrapper.is-fill tr.tfill { display: none; }
}
/* Opt out for short embedded tables (invoice line items, detail panels). */
/* Height is free, but a wide table must still scroll sideways — plain
   `overflow: visible` dropped the horizontal scroll and let the table push the
   whole page wider than the viewport on laptop and tablet. */
.table-wrapper.is-auto { max-height: none; overflow-x: auto; overflow-y: visible; }
/* A table inside a modal is already inside a scrolling box — a second scroll
   area within it traps the pointer and hides rows behind an inner scrollbar. */
.modal .table-wrapper,
.modal-box .table-wrapper { max-height: none; overflow: visible; }

.table-wrapper > table > thead th,
.table-wrapper > table thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  /* thead th is a solid colour already; repeat it here so rows scrolling
     underneath never show through. */
  background: var(--surface-2);
}
/* A totals row pinned to the bottom stays visible the same way. */
.table-wrapper > table tfoot td {
  position: sticky;
  bottom: 0;
  z-index: 2;
  background: var(--surface-2);
}

/* Slim scrollbars, so a scrolling table doesn't gain a heavy grey gutter. */
.table-wrapper { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
.table-wrapper::-webkit-scrollbar { width: 10px; height: 10px; }
.table-wrapper::-webkit-scrollbar-track { background: transparent; }
.table-wrapper::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border: 3px solid var(--card);
  border-radius: var(--radius-pill);
}
.table-wrapper::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }

/* ── The dashboard caps every list to one height ──────────────────────────
   This page is a set of summaries, not one table you work in, so the default
   `calc(100vh - 340px)` is wrong here: it is ~600px on a 1080p screen, which
   let Pending Approvals (up to 25 rows — five each of trips, invoices, claims,
   leave and reimbursements) push everything under it off the screen, and left
   the paired cards ending at different heights.

   Nothing new is needed to fix it. .table-wrapper above already caps, scrolls,
   sticks its column headings and draws a slim scrollbar; it just reads its
   height from --table-h. Setting that one variable per card is the whole
   change. 320px is the height the maintenance panel already scrolls at
   (.panel-accordion-body), so every list on the page stops in the same place.

   On the CARD, not on .main-content, so a card that wants a different height
   can say so without unpicking this. */
.is-dashboard .card { --table-h: 320px; }

/* Short viewports: a fixed 340px reservation would leave almost no table. */
@media (max-height: 720px) {
  .table-wrapper,
  .table-wrapper.is-tall { max-height: var(--table-h, 60vh); }
}
@media (max-width: 640px) {
  .table-wrapper,
  .table-wrapper.is-tall { max-height: var(--table-h, 70vh); }
}

/* ════════════════════════════════════════════════════════════════════════════
   Select controls
   ────────────────────────────────────────────────────────────────────────────
   `appearance: none` was set globally without drawing an arrow back, so all 41
   selects in the app rendered as plain boxes with nothing to say they open. The
   chevron is an inline SVG data URI (no extra request, no icon font) and the
   right padding reserves room so a long option never runs under it.

   The OPEN PANEL is drawn by the operating system and cannot be styled — that
   is a platform constraint, not an omission. Keeping the native control is also
   the right call: it gets the OS picker on mobile, real keyboard support and
   screen-reader semantics for free, none of which a div-based imitation
   reliably matches.
   ════════════════════════════════════════════════════════════════════════════ */
select,
select.form-control {
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  padding-right: 34px;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  background-size: 15px;
  transition: border-color var(--transition), box-shadow var(--transition), background-color var(--transition);
}
select:hover:not(:disabled) { border-color: var(--border-strong); }
/* Chevron picks up the accent while the control is focused, so the whole field
   reads as active rather than just its border. */
select:focus {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232563eb' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
}
select:disabled {
  background-color: var(--bg);
  color: var(--text-dim);
  cursor: not-allowed;
  opacity: 1;   /* Safari fades disabled selects to near-invisible otherwise */
}
/* A multi-select or a sized listbox is a list, not a dropdown — no chevron. */
select[multiple],
select[size]:not([size="1"]) {
  background-image: none;
  padding-right: 13px;
  cursor: default;
}

/* ════════════════════════════════════════════════════════════════════════════
   Date and time controls
   ────────────────────────────────────────────────────────────────────────────
   Same treatment: a leading calendar icon so a date field is recognisable at a
   glance next to a text field, and the whole control opens the picker instead
   of only the small native icon at the right edge.

   Scoped to WebKit/Blink, which is the only engine exposing the indicator as a
   pseudo-element. Firefox draws its own affordance; overlaying ours there would
   show two icons.
   ════════════════════════════════════════════════════════════════════════════ */
@supports selector(::-webkit-calendar-picker-indicator) {
  input[type="date"],
  input[type="datetime-local"],
  input[type="month"],
  input[type="time"] {
    padding-left: 36px;
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: left 11px center;
    background-size: 15px;
    cursor: pointer;
  }
  input[type="time"] {
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpolyline points='12 7 12 12 15.5 14'/%3E%3C/svg%3E");
  }
  input[type="date"]:focus,
  input[type="datetime-local"]:focus,
  input[type="month"]:focus,
  input[type="time"]:focus {
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232563eb' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E");
  }
  /* Stretch the invisible native trigger over the whole field: clicking the
     text should open the calendar, not just the 16px icon at the far right. */
  input[type="date"]::-webkit-calendar-picker-indicator,
  input[type="datetime-local"]::-webkit-calendar-picker-indicator,
  input[type="month"]::-webkit-calendar-picker-indicator,
  input[type="time"]::-webkit-calendar-picker-indicator {
    position: absolute;
    left: 0; top: 0; right: 0; bottom: 0;
    width: auto; height: auto;
    margin: 0; padding: 0;
    opacity: 0;
    cursor: pointer;
  }
  input[type="date"],
  input[type="datetime-local"],
  input[type="month"],
  input[type="time"] { position: relative; }
  /* Blink pads the date's own fields 1px top and bottom, which made every date
     box 34px beside 32px controls — the toolbar-height drift --control-h exists
     to end. Measured on the cashbook's add-entry dialog, 2026-10-08. */
  input[type="date"]::-webkit-datetime-edit-fields-wrapper,
  input[type="datetime-local"]::-webkit-datetime-edit-fields-wrapper,
  input[type="month"]::-webkit-datetime-edit-fields-wrapper,
  input[type="time"]::-webkit-datetime-edit-fields-wrapper { padding: 0; }
}

/* ════════════════════════════════════════════════════════════════════════════
   Date-range presets
   ────────────────────────────────────────────────────────────────────────────
   The genuinely useful half of a range picker. "Last 30 days" is one click here
   versus two calendar journeys, and it needs no custom calendar to provide.
   ════════════════════════════════════════════════════════════════════════════ */
.date-presets {
  display: flex;
  align-items: center;
  gap: 5px;
  flex-wrap: wrap;
}
.date-preset {
  padding: 5px 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--card);
  color: var(--text-muted);
  font-size: var(--fs-xs);
  font-weight: 600;
  font-family: inherit;
  white-space: nowrap;
  text-decoration: none;
  transition: background var(--transition), color var(--transition), border-color var(--transition);
}
.date-preset:hover { background: var(--card-hover); color: var(--text); border-color: var(--border-strong); text-decoration: none; }
.date-preset:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.date-preset.is-active {
  background: var(--accent-dim);
  border-color: var(--accent);
  color: var(--accent);
}

/* ── Field label + control, the shape a filter bar repeats ─────────────────── */
.field-inline { display: flex; flex-direction: column; gap: 6px; }
.field-inline > label {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text-label);
  margin: 0;
}
.field-inline > input,
.field-inline > select { width: auto; min-width: 150px; height: var(--control-h); padding-top: 0; padding-bottom: 0; }
.field-inline > input[type="date"] { min-width: 168px; }

/* ════════════════════════════════════════════════════════════════════════════
   Filter card — the presets row and the field row above a list.
   Replaces the block of inline styles that was copy-pasted onto every field of
   every filter bar, which is why no two of them lined up.
   ════════════════════════════════════════════════════════════════════════════ */
.filter-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: var(--space-4) var(--space-5);
  margin-bottom: var(--stack-gap);
}
/* Presets sit above the fields, separated by a hairline: they set the same two
   dates the fields hold, so they belong to that row, not beside it. */
.filter-card > .date-presets {
  padding-bottom: 12px;
  margin-bottom: 12px;
  border-bottom: 1px solid var(--border);
}
.filter-form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: flex-end;
}
.filter-actions { display: flex; gap: var(--space-2); margin-left: auto; }

@media (max-width: 640px) {
  .filter-form > .field-inline { flex: 1 1 100%; }
  .filter-form > .field-inline > input,
  .filter-form > .field-inline > select { width: 100%; }
  .filter-actions { margin-left: 0; width: 100%; }
  .filter-actions > .btn { flex: 1; justify-content: center; }
}

/* ════════════════════════════════════════════════════════════════════════════
   Custom select and calendar panels          (behaviour in assets/controls.js)
   ────────────────────────────────────────────────────────────────────────────
   A native <select>'s dropdown and a native date input's calendar are drawn by
   the OS and cannot be styled. These panels replace them while the native
   control stays underneath and remains what the form submits.

   Panels are position:fixed on <body> so they escape the scrolling table
   containers and modal stacking contexts they're opened inside.
   ════════════════════════════════════════════════════════════════════════════ */

/* ── Trigger ─────────────────────────────────────────────────────────────── */
.ui-select { position: relative; display: block; }
/* The native select stays focusable-adjacent and submits, but is not shown.
   display:none would drop it from the form on some older engines. */
.ui-select-native {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  margin: 0;
}
.ui-select-trigger {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: var(--control-h);
  padding: 6px 10px 6px var(--space-3);
  line-height: var(--control-lh);
  background: var(--surface);
  border: 1px solid var(--control-border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: var(--fs-sm);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--transition), box-shadow var(--transition), background var(--transition);
}
.ui-select-trigger:hover:not(:disabled) { border-color: var(--control-border-hover); }
.ui-select-trigger:focus-visible,
.ui-select.is-open .ui-select-trigger {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
}
.ui-select-trigger:disabled { background: var(--surface-3); color: var(--text-muted); cursor: not-allowed; }
.ui-select-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ui-select.is-placeholder .ui-select-label { color: var(--text-dim); }
.ui-select-caret {
  display: grid;
  place-items: center;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: var(--text-muted);
  transition: transform var(--transition), color var(--transition);
}
.ui-select-caret svg { width: 15px; height: 15px; }
.ui-select.is-open .ui-select-caret { transform: rotate(180deg); color: var(--accent); }

/* Compact contexts keep their own metrics. */
.pager-size .ui-select-trigger,
.field-inline > .ui-select .ui-select-trigger { min-height: var(--control-h); padding-top: 0; padding-bottom: 0; }
.pager-size .ui-select { min-width: 84px; }
.pager-size .ui-select-trigger { min-height: var(--control-h-sm); padding-top: 4px; padding-bottom: 4px; }
.field-inline > .ui-select { min-width: 150px; }

/* ── The floating panel ──────────────────────────────────────────────────── */
.ui-panel {
  position: fixed;
  z-index: 900;                     /* over sticky headers, under modals */
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--space-1);
  opacity: 0;
  transform: translateY(-4px);
}
.ui-anim .ui-panel { transition: opacity 140ms var(--ease-out), transform 140ms var(--ease-out); }
.ui-panel.is-open { opacity: 1; transform: none; }
.ui-panel.is-above { transform-origin: bottom; }
/* Reduced motion: appear, don't travel. */
@media (prefers-reduced-motion: reduce) {
  .ui-panel { transition: none; transform: none; }
}

.ui-select-panel { max-height: 300px; overflow-y: auto; scrollbar-width: thin; }

/* A filter, on the lists long enough to need one (controls.js: SEARCH_FROM).
   Sticky, so it stays reachable while 73 products scroll under it. */
.ui-select-search {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 8px;
  background: var(--card);
  border-bottom: 1px solid var(--border);
}
.ui-select-search-input {
  width: 100%;
  height: var(--control-h-sm);
  padding: 0 var(--space-2);
  font-size: var(--fs-sm);
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  outline: none;
}
.ui-select-search-input::placeholder { color: var(--text-dim); }
.ui-select-search-input:focus {
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
}
/* Search HIDES rows rather than removing them — items[] has to stay 1:1 with
   sel.options, which is the invariant the whole panel is built on. */
.ui-select-panel .is-hidden { display: none; }
.ui-select-empty {
  padding: 14px 10px;
  text-align: center;
  font-size: var(--fs-xs);
  color: var(--text-muted);
}
.ui-option {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  color: var(--text);
  cursor: pointer;
  user-select: none;
}
.ui-option-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ui-option-tick { width: 15px; height: 15px; flex-shrink: 0; color: var(--accent); opacity: 0; }
.ui-option-tick svg { width: 15px; height: 15px; display: block; }
/* Keyboard and pointer share one highlight, so there is never a second
   "where am I" marker competing with the first. */
.ui-option.is-active   { background: var(--accent-dim); }
.ui-option.is-selected { color: var(--accent-hover); font-weight: var(--fw-medium); }
.ui-option.is-selected .ui-option-tick { opacity: 1; }
.ui-option.is-disabled { color: var(--text-dim); cursor: not-allowed; }
.ui-option.is-disabled.is-active { background: transparent; }

/* A group header inside the dropdown — "Suggested for 20ft" over "Other trucks".
   Same vocabulary as .form-section, because it does the same job one level down:
   it names a band of things, it is not one of them. Deliberately not clickable
   and not in the keyboard order; controls.js never puts it in items[]. */
.ui-optgroup {
  padding: 8px 10px 4px;
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  color: var(--text-muted);
  user-select: none;
  cursor: default;
}
/* A rule only BETWEEN groups. The first header needs no line above it — the
   panel edge already is one, and a second reads as a stray divider. */
.ui-select-panel > [role="group"] + [role="group"] {
  border-top: 1px solid var(--border);
  margin-top: 4px;
  padding-top: 2px;
}

/* ── The line-items grid ──────────────────────────────────────────────────────
   A borderless spreadsheet: one row per charge, the inputs invisible until you
   are in them, so it reads as a document rather than as a form. Lives HERE and
   not inline on one page, because two screens draw it — the invoice
   (finance/invoices.php) and the trip's Charges section — and they have to look
   like the same thing: one prefills the other, and an operator compares them.
   It was inline on the invoice page, which is how .view-toggle and the .tl-*
   timeline styles each shipped broken the first time a second page used them. */
.inv-items-table { width: 100%; border-collapse: collapse; min-width: 680px; }
.inv-items-table thead tr { background: var(--bg); }
.inv-items-table th {
  padding: 8px 10px;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border);
}
.inv-items-table tbody tr { border-bottom: 1px solid var(--border); transition: background .1s; }
/* Was rgba(255,255,255,.02) — white on a white card, i.e. no hover at all.
   Left over from when this app was dark navy. */
.inv-items-table tbody tr:hover { background: var(--card-hover); }
.inv-items-table tbody tr:last-child { border-bottom: none; }
.inv-items-table input, .inv-items-table select {
  border: none !important;
  background: transparent !important;
  color: var(--text);
  outline: none !important;
  padding: 4px;
  font-size: var(--fs-sm);
  box-shadow: none !important;
}
.inv-items-table tr:focus-within { background: var(--accent-dim); }
/* controls.js builds its own control as a SIBLING of the native select, so the
   borderless treatment has to name it too or every picker in the grid arrives
   with a box around it that nothing else in the table has. */
.inv-items-table .ui-select-trigger {
  border: none !important;
  background: transparent !important;
  box-shadow: none !important;
  height: auto;
  padding: 4px;
  font-size: var(--fs-sm);
}
.inv-items-table .ui-select { width: 100%; }

.inv-cell-no   { text-align: center; padding: 7px 4px; color: var(--text-muted); font-size: var(--fs-xs); width: 36px; }
.inv-cell-desc { padding: 4px 6px; }
.inv-cell-desc input { width: 100%; }
.inv-cell-uom  { padding: 4px; text-align: center; }
.inv-cell-uom  input { width: 58px; text-align: center; color: var(--text-muted); font-size: var(--fs-xs); }
.inv-cell-num  { padding: 4px 6px; text-align: right; }
.inv-cell-num  input { width: 100%; text-align: right; }
.inv-cell-disc input { color: var(--text-muted); font-size: var(--fs-xs); }
.inv-cell-amt  { padding: 6px 10px; text-align: right; font-weight: 600; font-size: var(--fs-sm); white-space: nowrap; }
.inv-cell-del  { padding: 4px 5px; text-align: center; }
.inv-foot-label { text-align: right; padding: 7px 10px; font-size: var(--fs-xs); color: var(--text-muted); font-weight: 600; }
.inv-foot-val   { text-align: right; padding: 7px 10px; font-size: var(--fs-sm); white-space: nowrap; }
.inv-total-row  { background: var(--accent-dim); font-weight: 600; }
.inv-total-row .inv-foot-label { color: var(--text); font-size: var(--fs-sm); }

/* ── The trip's Charges section ───────────────────────────────────────────────
   The same grid, sized for a form column rather than a full-width document. */
/* min-width: 0 is what MAKES the scroller below work. A grid item defaults to
   min-width: auto, i.e. "never shrink below your content" — so the 680px charge
   table never overflowed into .rt-charges-wrap's overflow-x, it pushed the grid
   COLUMN out to 916px instead and took the whole form with it. Measured at
   390px: the New Trip page scrolled to 946px and every unrelated field —
   customer, dates, route — was dragged off screen by a table three sections
   below them. The scroller was written with the right intent and had never once
   engaged on a narrow screen. */
.rt-charges { display: block; min-width: 0; }
.rt-charges-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-sm); }
/* The per-line customer only applies to a console job, which carries several.
   CSS-driven off the same data-console the route fields already use, so there
   is one switch for "this job type has several customers", not two. */
.rt-charges .charge-cust { display: none; }
.rt-charges[data-console="1"] .charge-cust { display: table-cell; }
/* Same rule, one column over. A trip form has one truck, so the Department
   picker is only drawn where a line can genuinely differ from it — the invoice
   dialogs, which pass the department list. Hidden, never omitted: every reader
   pairs its arrays by POSITION. */
.rt-charges .charge-dept { display: none; }
.rt-charges[data-depts="1"] .charge-dept { display: table-cell; }
/* A variant select with nothing to offer READS disabled without BEING disabled.
   The distinction is the whole point: a disabled select posts nothing, and this
   grid pairs its arrays by position, so one unpostable line shifts every variant
   below it onto the wrong row. */
.rt-charges .charge-variant.is-empty {
  color: var(--text-muted);
  background: var(--bg-subtle, rgba(0, 0, 0, .03));
}
/* A product with one rate needs no rate picker; JS sets .has-rates per row. */
.rt-charges .charge-rate .ui-select,
.rt-charges .charge-rate select { width: 100%; }
.rt-charges-empty { padding: var(--space-5); text-align: center; color: var(--text-muted); font-size: var(--fs-sm); }

/* ── The New Sales Invoice dialog ─────────────────────────────────────────────
   Operate work: somebody finishing a task. So it is laid out in the order the
   job is done — who and which trip, when, then the lines — and the lines get
   the room, because that is where the money is and where the eye goes.

   Wider than the 580px .modal-box default because the line grid carries eight
   columns; capped at the viewport so it never has to scroll sideways to reach
   the submit. */
/* WIDE ENOUGH FOR THE AMOUNT COLUMN. The charge grid is eleven columns —
   product, variant, description, department, UOM, qty, price, discount, amount
   and a remove — and at 1060px the last two fell off the right edge into a
   horizontal scroll. So the number you opened the dialog to check was the one
   number you could not see without scrolling sideways for it. 1240 fits the lot
   on any screen from 1280 up, and the viewport clamp still governs below that. */
.inv-dialog { max-width: min(1240px, calc(100vw - var(--space-8))); }
.inv-dialog-sub {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin-top: 3px;
  line-height: 1.45;
}
.inv-dialog-sub strong { color: var(--text); font-weight: 600; }

/* The body scrolls, the header and footer do not — so the running total and
   Create stay put while a long console job's lines go past. */
.inv-dialog-body {
  padding: var(--space-5) var(--space-6);
  max-height: calc(90vh - 132px);
  overflow-y: auto;
}
.inv-dialog-alert { margin-bottom: var(--space-4); }

/* Four header fields on one row at desktop width, collapsing to two and then
   one. Not a card — a card inside a dialog is a box inside a box. */
.inv-dialog-head {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--field-gap);
  padding-bottom: var(--space-5);
  margin-bottom: var(--space-5);
  border-bottom: 1px solid var(--border);
}
.inv-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.inv-field > label {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-muted);
}
.inv-field select,
.inv-field input[type="date"] { width: 100%; height: var(--control-h); }
.inv-field .ui-select { width: 100%; }
/* A value that is already decided reads as a value, not as a disabled input —
   a greyed-out control invites clicking something that will not respond. */
.inv-field-static {
  display: flex;
  align-items: center;
  height: var(--control-h);
}
/* A stated value that is MISSING, and whose absence blocks something. AutoCount
   refuses a document with no credit term, so an empty box would be the quietest
   possible way to say "this will not send". */
/* The hint under the credit-term box warns the same way the static line did —
   the field went from read-only to editable, and "no term" still has to read as
   a problem rather than as an empty box nobody filled in yet. */
.inv-field-static.inv-field-warn,
.inv-field-hint.inv-field-warn   { color: var(--warning-text); font-style: italic; }
.inv-field-hint { font-size: var(--fs-xs); color: var(--text-muted); font-style: italic; }

/* The total belongs beside the button that commits it, not scrolled away above. */
.inv-dialog-foot { align-items: center; }
.inv-dialog-total {
  margin-right: auto;
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
}
.inv-dialog-total span {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-muted);
}
.inv-dialog-total strong { font-size: var(--fs-md); color: var(--text); }

@media (max-width: 900px) {
  .inv-dialog-head { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .inv-dialog-head { grid-template-columns: 1fr; }
  .inv-dialog-body { padding: var(--space-4); }
  /* The total keeps its own line rather than squeezing the buttons off screen. */
  .inv-dialog-foot { flex-wrap: wrap; }
  .inv-dialog-total { width: 100%; margin: 0 0 var(--space-2); }
}

/* ── Calendar ────────────────────────────────────────────────────────────── */
/* Our panel replaces the native one, so suppress the OS trigger entirely —
   otherwise clicking the field could open both. */
.ui-date-input::-webkit-calendar-picker-indicator { display: none !important; }
.ui-date-input { cursor: pointer; }
.ui-date-input.is-open {
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
}

.ui-cal-panel { padding: 12px; width: 280px; }
.ui-cal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 10px;
}
.ui-cal-title { font-size: var(--fs-sm); font-weight: 600; color: var(--text); }
.ui-cal-nav { display: flex; gap: 2px; }
.ui-cal-navbtn {
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-muted);
  transition: background var(--transition), color var(--transition);
}
.ui-cal-navbtn svg { width: 15px; height: 15px; }
.ui-cal-navbtn:hover { background: var(--accent-dim); color: var(--accent); }
.ui-cal-navbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.ui-cal-dow {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
  margin-bottom: 4px;
}
.ui-cal-dow span {
  text-align: center;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-dim);
  padding: 4px 0;
}
.ui-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.ui-cal-day {
  height: 34px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text);
  font-size: var(--fs-xs);
  font-family: inherit;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background var(--transition), color var(--transition), border-color var(--transition);
}
.ui-cal-day:hover:not(:disabled) { background: var(--card-hover); }
.ui-cal-day:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
.ui-cal-day.is-outside { color: var(--text-dim); }
/* Today is outlined, the selection is filled — so "today" stays legible even
   when a different day is chosen. */
.ui-cal-day.is-today { border-color: var(--accent); color: var(--accent); font-weight: 600; }
.ui-cal-day.is-selected {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  font-weight: 600;
}
.ui-cal-day.is-selected:hover { background: var(--accent-hover); }
.ui-cal-day.is-blocked { color: var(--text-dim); opacity: 0.4; cursor: not-allowed; }

.ui-cal-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}
.ui-cal-action {
  padding: 5px 10px;
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-muted);
  font-size: var(--fs-xs);
  font-weight: 600;
  font-family: inherit;
  transition: background var(--transition), color var(--transition);
}
.ui-cal-action:hover { background: var(--card-hover); color: var(--text); }
.ui-cal-action.is-primary { color: var(--accent); }
.ui-cal-action.is-primary:hover { background: var(--accent-dim); }
.ui-cal-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Source chip — where a record came from. Shared by Customers and Trucks. */
.cust-src { display:inline-flex; align-items:center; gap:.3rem; font-size:var(--fs-xs); font-weight:600;
            border-radius:var(--radius-pill); padding:.16rem .5rem; white-space:nowrap; }
.cust-src.is-ac    { color:var(--accent-hover); background:rgba(37,99,235,.1); }
.cust-src.is-local { color:var(--text-muted); background:rgba(100,116,139,.1); }
.cust-src svg { width:11px; height:11px; }
.dot-retired { background:var(--text-dim); }

/* ════════════════════════════════════════════════════════════════════════════
   Date-range field                            (behaviour in assets/controls.js)
   ────────────────────────────────────────────────────────────────────────────
   One field reading "28/07/2025 – 28/07/2026", opening presets alongside a
   calendar. The two native date inputs stay in the DOM carrying the real
   values; with JavaScript off they are simply what you see and use.
   ════════════════════════════════════════════════════════════════════════════ */
.ui-range { display: flex; align-items: center; gap: 6px; position: relative; }
.ui-range > input { width: auto; min-width: 150px; height: var(--control-h); padding-top: 0; padding-bottom: 0; }
.ui-range-sep { color: var(--text-muted); font-size: var(--fs-sm); }
/* On a phone the two native boxes share the row; at 150px each they ran out of the card. */
@media (max-width: 640px) { .ui-range > input { min-width: 0; width: 0; flex: 1 1 0; } }

/* Enhanced: the real inputs go under the trigger, still submitting. */
.ui-range.is-enhanced > input,
.ui-range.is-enhanced > .ui-range-sep {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 1px;
  height: 1px;
  min-width: 0;
  padding: 0;
  border: 0;
  margin: 0;
}
.ui-range-trigger {
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 250px;
  height: var(--control-h);
  padding: 0 var(--space-3);
  background: var(--surface);
  border: 1px solid var(--control-border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: var(--fs-sm);
  font-family: inherit;
  font-variant-numeric: tabular-nums;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--transition), box-shadow var(--transition);
}
.ui-range-trigger > svg { width: 15px; height: 15px; flex-shrink: 0; color: var(--text-muted); }
.ui-range-trigger:hover { border-color: var(--control-border-hover); }
.ui-range.is-open .ui-range-trigger,
.ui-range-trigger:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
}
.ui-range.is-open .ui-range-trigger > svg { color: var(--accent); }
.ui-range-text.is-empty { color: var(--text-muted); }

/* ── Panel: presets left, calendar right ─────────────────────────────────── */
.ui-range-panel { display: flex; padding: 0; overflow: hidden; }
.ui-range-presets {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 10px 8px;
  border-right: 1px solid var(--border);
  background: var(--bg);
  min-width: 132px;
}
.ui-range-preset {
  padding: 7px 12px;
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text);
  font-size: var(--fs-xs);
  font-family: inherit;
  text-align: left;
  white-space: nowrap;
  transition: background var(--transition), color var(--transition);
}
.ui-range-preset:hover { background: var(--accent-dim); color: var(--accent); }
.ui-range-preset:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.ui-range-preset.is-active { background: var(--accent-dim); color: var(--accent); font-weight: 600; }
/* Clear sits apart from the shortcuts — it undoes rather than chooses. */
.ui-range-preset.is-clear {
  margin-top: auto;
  color: var(--text-muted);
  border-top: 1px solid var(--border);
  border-radius: 0;
  padding-top: 10px;
}
.ui-range-preset.is-clear:hover { background: none; color: var(--status-overdue); }

.ui-range-cal { padding: 12px; width: 278px; }
.ui-range-cal .ui-cal-head { justify-content: space-between; }
.ui-range-cal .ui-cal-title { flex: 1; text-align: center; }

/* ── Range highlighting ──────────────────────────────────────────────────── */
/* The band sits behind the day buttons via a pseudo-element so it can run edge
   to edge between cells, which a background on the button itself cannot. */
.ui-cal-grid.is-range .ui-cal-day { position: relative; border-radius: 0; }
.ui-cal-grid.is-range .ui-cal-day::before {
  content: '';
  position: absolute;
  inset: 0 -1px;
  background: transparent;
  z-index: -1;
}
.ui-cal-grid.is-range .ui-cal-day.is-in-range::before { background: var(--accent-dim); }
.ui-cal-grid.is-range .ui-cal-day.is-in-range { color: var(--accent); }
.ui-cal-grid.is-range .ui-cal-day.is-start,
.ui-cal-grid.is-range .ui-cal-day.is-end {
  background: var(--accent);
  color: #fff;
  font-weight: 600;
  border-radius: var(--radius-sm);
  z-index: 1;
}
/* Square off the inner edge so the endpoints read as caps on one band rather
   than two separate chips. */
.ui-cal-grid.is-range .ui-cal-day.is-start:not(.is-end) { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.ui-cal-grid.is-range .ui-cal-day.is-end:not(.is-start) { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
.ui-cal-grid.is-range .ui-cal-day.is-start:not(.is-end)::before,
.ui-cal-grid.is-range .ui-cal-day.is-end:not(.is-start)::before { background: var(--accent-dim); }
.ui-cal-grid.is-range .ui-cal-day.is-start:not(.is-end)::before { left: 50%; right: -1px; }
.ui-cal-grid.is-range .ui-cal-day.is-end:not(.is-start)::before { right: 50%; left: -1px; }

@media (max-width: 560px) {
  .ui-range-panel  { flex-direction: column; max-width: calc(100vw - 16px); }
  .ui-range-presets {
    flex-direction: row;
    flex-wrap: wrap;
    border-right: none;
    border-bottom: 1px solid var(--border);
    min-width: 0;
  }
  .ui-range-preset.is-clear { margin-top: 0; border-top: none; padding-top: 7px; }
  .ui-range-cal     { width: auto; }
  .ui-range-trigger { min-width: 0; width: 100%; }
}

/* ════════════════════════════════════════════════════════════════════════════
   Pinned table footer
   ────────────────────────────────────────────────────────────────────────────
   A paged table gets a FIXED height rather than one that follows its row count,
   so the pager sits in the same place on every page. Without this the last page
   of a list — three rows out of 103 — collapses the card and the pager jumps
   several hundred pixels up the screen, right where you are about to click
   "previous".

   Scoped with :has(> .pager) so it only applies to tables that actually have a
   footer to pin — except .table-card, which is only ever a primary paged list:
   rtPager() prints nothing for zero results, and keying on the pager let an
   empty search collapse the whole card. A short embedded table (truck history, an invoice's line
   items) keeps sizing to its content, which is what it should do.
   ════════════════════════════════════════════════════════════════════════════ */
.table-card:has(> .table-wrapper),
.card:has(> .table-wrapper):has(> .pager),
.card:has(> form > .table-wrapper + .pager),
.card:has(> .empty-state:only-child) {
  display: flex;
  flex-direction: column;
  /* The card owns the height now, so the wrapper's own max-height is released
     below. One value governs both, which is what keeps the pager steady. */
  height: var(--table-card-h, calc(100vh - 300px));
  min-height: 340px;
}
.table-card:has(> .table-wrapper) > .table-wrapper,
.card:has(> .table-wrapper):has(> .pager) > .table-wrapper {
  flex: 1 1 auto;
  /* Without min-height:0 a flex child refuses to shrink below its content, so
     the table would overflow the card instead of scrolling inside it. */
  min-height: 0;
  max-height: none;
}
.table-card:has(> .table-wrapper) > .pager,
.card:has(> .table-wrapper):has(> .pager) > .pager {
  flex: 0 0 auto;
  margin-top: auto;
}

/* The invoice list wraps its table AND its pager in the bulk-select <form>, so
   the rule above could not see them and that card took its own height — 33px
   off every other list. The same pinned card, one level down. */
.card:has(> form > .table-wrapper + .pager) > form {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.card:has(> form > .table-wrapper + .pager) > form > .table-wrapper { flex: 1 1 auto; min-height: 0; height: auto; }
.card:has(> form > .table-wrapper + .pager) > form > .pager { flex: 0 0 auto; margin-top: auto; }

/* A filter that matches nothing replaces the table with a message — and the
   card used to collapse to the message's height, so emptying a list moved
   everything under it by several hundred pixels. The same box, the message in
   its middle. */
.card:has(> .empty-state:only-child) { justify-content: center; }

/* Short viewports: a fixed 300px reservation leaves too little table. */
@media (max-height: 760px) {
  .table-card:has(> .table-wrapper),
  .card:has(> .table-wrapper):has(> .pager),
  .card:has(> form > .table-wrapper + .pager),
  .card:has(> .empty-state:only-child) {
    height: var(--table-card-h, 62vh);
    min-height: 260px;
  }
}
@media (max-width: 640px) {
  .table-card:has(> .table-wrapper),
  .card:has(> .table-wrapper):has(> .pager),
  .card:has(> form > .table-wrapper + .pager),
  .card:has(> .empty-state:only-child) {
    height: auto;          /* on a phone, vertical space is the scarce thing */
    min-height: 0;
  }
  .table-card:has(> .table-wrapper) > .table-wrapper,
  .card:has(> .table-wrapper):has(> .pager) > .table-wrapper { max-height: 70vh; }
}

/* Browsers without :has() fall back to a content-height card and an unpinned
   footer — the old behaviour, not a broken one. */

/* ── A list with no pager still holds its shape (.table-wrapper.is-list) ────
   Payroll's lists and a driver's standby-cash history are not paged, so the
   rule above never reached them and each box was exactly as tall as its rows:
   two claimants, then six, then one, and everything below moved with them.
   A steady MINIMUM rather than a fixed height — these are the tables that may
   run long, and a list of forty should scroll the page, not a box inside it —
   and modest, so a two-row list is not stranded in a screen of white. */
.table-wrapper.is-list {
  display: flex;
  flex-direction: column;
  min-height: var(--list-min-h, 320px);
}
.table-wrapper.is-list > table { flex: 1 0 auto; }
/* …and when the list is empty, its message holds the same box — not the
   pinned height a PAGED list's empty card takes, or emptying a pager-less
   list would make it grow. */
.card:has(> .empty-state.is-list:only-child) { height: auto; min-height: calc(var(--list-min-h, 320px) + 2 * var(--card-pad)); }
@media (max-width: 640px) {
  .table-wrapper.is-list,
  .card:has(> .empty-state.is-list:only-child) { min-height: 0; }
}

/* ── Totals stay at the foot of their table ────────────────────────────────
   A <tfoot> — Period Total, Total Float — used to sit directly under the last
   row, so it rose and fell with the row count while the pager below it stayed
   put. Two rules pin it where a footer belongs:

   · the table fills its box, and an invisible last row takes the spare height,
     so real rows keep their own height instead of each being stretched;
   · the footer is sticky, so on a long list the totals stay in view while the
     rows scroll under them.

   Where the box has no set height (an embedded table) the percentage resolves
   to auto and none of this changes anything. */
.table-wrapper > table:has(> tfoot) { height: 100%; }
.table-wrapper > table:has(> tfoot) > tbody:last-of-type::after,
.table-wrapper.is-list > table > tbody:last-of-type::after {
  content: '';
  display: table-row;
  height: 100%;
}
.table-wrapper > table > tfoot { position: sticky; bottom: 0; z-index: 2; }
.table-wrapper > table > tfoot td { background: var(--surface-2); }
/* A shadow, not a border: a collapsed border belongs to the table grid and is
   left behind when the row sticks. */
.table-wrapper > table > tfoot tr:first-child td { box-shadow: inset 0 1px 0 var(--border-strong); }

/* ── Row actions ───────────────────────────────────────────────────────────
   Icon buttons at the end of a table row. Text buttons repeated down a
   19-row fleet were three words of furniture on every line; the label moves
   to title + aria-label, so it is still announced and still hoverable.
   They used to fade to 55% until the row was hovered; with a muted glyph they
   are already quiet, and a control you cannot see until you find it by
   accident is not quiet, it is hidden. */
.row-actions { display: flex; gap: var(--space-1); justify-content: flex-end; align-items: center; }
.row-actions form { display: contents; }

/* ════════════════════════════════════════════════════════════════════════════
   Page heading                            (markup in components/page-head.php)
   ────────────────────────────────────────────────────────────────────────────
   Icon tile, title, one line of context, primary action. The tile carries the
   same mark as the sidebar item you clicked to get here — that is its job, so
   the icon set lives in one file (components/icons.php) and both draw from it.
   ════════════════════════════════════════════════════════════════════════════ */
.page-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--stack-gap);
}
/* Navy, not the action blue: the tile says WHERE you are (it is the sidebar
   item's own icon), and where-you-are is the brand's job, not a button's. */
.page-head-icon {
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  border-radius: var(--radius);
  background: var(--brand-tint);
  color: var(--rt-navy-600);
}
.page-head-icon svg { width: 20px; height: 20px; }
.page-head-text { min-width: 0; flex: 1; }
.page-head-title {
  font-size: var(--fs-lg);
  font-weight: var(--fw-semibold);
  color: var(--text);
  line-height: var(--lh-tight);
  letter-spacing: -0.01em;
}
.page-head-sub {
  font-size: var(--fs-sm);
  color: var(--text-muted);
  margin-top: 2px;
  line-height: var(--lh);
}
.page-head-actions { display: flex; align-items: center; gap: var(--space-2); flex-shrink: 0; margin-left: auto; }

@media (max-width: 640px) {
  .page-head { flex-wrap: wrap; gap: 12px; }
  .page-head-actions { margin-left: 0; width: 100%; }
  .page-head-actions > .btn { flex: 1; justify-content: center; }
}

/* ════════════════════════════════════════════════════════════════════════════
   Accordion panel
   ────────────────────────────────────────────────────────────────────────────
   A summary line you can open for the detail. Built on <details>/<summary>
   because the browser already implements this properly — keyboard, screen
   readers and Ctrl+F all work, and it survives JavaScript being off. A div
   with a click handler would have to reimplement all three, worse.
   ════════════════════════════════════════════════════════════════════════════ */
.panel-accordion {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: var(--stack-gap);
  overflow: hidden;
}
.panel-accordion > summary {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 13px 16px;
  cursor: pointer;
  list-style: none;
  user-select: none;
  transition: background var(--transition);
}
/* Suppress the native disclosure triangle; we draw our own caret. */
.panel-accordion > summary::-webkit-details-marker { display: none; }
.panel-accordion > summary::marker { content: ''; }
.panel-accordion > summary:hover { background: var(--card-hover); }
.panel-accordion > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.panel-accordion-icon {
  width: 30px; height: 30px; flex-shrink: 0;
  display: grid; place-items: center;
  border-radius: var(--radius);
  background: var(--accent-dim);
  color: var(--accent);
}
.panel-accordion-icon svg { width: 16px; height: 16px; }
.panel-accordion-title { flex: 1; min-width: 0; font-size: var(--fs-sm); color: var(--text); }
.panel-accordion-title strong { font-weight: 600; }
.panel-accordion-caret {
  width: 16px; height: 16px; flex-shrink: 0;
  color: var(--text-muted);
  transition: transform var(--transition);
}
.panel-accordion[open] > summary .panel-accordion-caret { transform: rotate(180deg); }
.panel-accordion[open] > summary { border-bottom: 1px solid var(--border); }

.panel-accordion-body { padding: 6px; max-height: 320px; overflow-y: auto; }

/* Warning tone — something needs attention, without the full-width alert slab. */
.panel-accordion.is-warning .panel-accordion-icon {
  background: rgba(217, 119, 6, 0.12);
  color: var(--status-pending);
}
/* Danger tone — at least one row is inside the 15-day HIGH maintenance
   reminder window, not just the 30-day one. */
.panel-accordion.is-danger .panel-accordion-icon {
  background: rgba(220, 38, 38, 0.12);
  color: var(--status-overdue);
}

/* Rows inside the upcoming-maintenance panel. */
.upcoming-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 11px;
  border-radius: var(--radius-sm);
  color: var(--text);
  text-decoration: none;
  font-size: var(--fs-sm);
  transition: background var(--transition);
}
.upcoming-row:hover { background: var(--accent-dim); text-decoration: none; }
/* HIGH reminder — inside 15 days, the client's own second threshold. */
.upcoming-row.is-urgent { background: rgba(220, 38, 38, 0.05); }
.upcoming-row.is-urgent:hover { background: rgba(220, 38, 38, 0.1); }
.upcoming-plate {
  font-weight: 600;
  min-width: 104px;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
}
.upcoming-type {
  flex: 1;
  color: var(--text-muted);
  font-size: var(--fs-xs);
  text-transform: capitalize;
}
.upcoming-date { color: var(--text-muted); font-size: var(--fs-xs); white-space: nowrap; }

@media (prefers-reduced-motion: reduce) {
  .panel-accordion > summary,
  .panel-accordion-caret,
  .upcoming-row { transition: none; }
}

/* ── Icon tile on the existing .page-header ────────────────────────────────
   Pages build their title and subtitle inline, so rather than rewrite twenty
   headers into one function call they keep their markup and gain the tile.
   Two shapes exist in the wild — <h1> as a direct child, or wrapped in a div
   with a subtitle — so the layout is expressed as "the element AFTER the icon
   takes the room", which is true of both, instead of naming either. */
.page-header.has-icon {
  justify-content: flex-start;
  gap: var(--space-3);
}
.page-header.has-icon > .page-head-icon { flex-shrink: 0; }
.page-header.has-icon > .page-head-icon + * { flex: 1 1 auto; min-width: 0; }
/* Everything past the title — buttons, toggles — keeps its size and sits right. */
.page-header.has-icon > .page-head-icon ~ *:not(:nth-child(2)) { flex-shrink: 0; }
/* ════════════════════════════════════════════════════════════════════════════
   Layout corrections — from screenshotting every page
   ════════════════════════════════════════════════════════════════════════════ */

/* The heading is the first thing under that gap, so it doesn't need its own
   large margin on top of it. */
.main-content > .page-header:first-child,
.main-content > .page-head:first-child { margin-top: 0; }

/* .card-title carries its own bottom margin for the case where it stands alone.
   Inside .card-header that stacked with the header's padding AND margin — 48px
   of empty space under every dashboard section title before its table began.
   The header owns the spacing when there is one. */
.card-header > .card-title { margin-bottom: 0; }

/* ── Overflow action menu (.rt-menu) ──────────────────────────────────────
   A trigger plus a menu of secondary actions, so a row or a page header shows
   one primary button instead of five competing ones.

   position:fixed, not absolute. These live inside cards with overflow:hidden
   and tables with overflow:auto; an absolutely positioned menu is clipped by
   both — on the trip list it was cut off after the first item and dragged the
   table into scrolling sideways. Fixed escapes every ancestor, which means the
   coordinates must be set from the trigger in JS (rtMenu() in app.js) and the
   menu must close on scroll rather than drift away from what opened it. */
.rt-menu-wrap { display: inline-block; }
.rt-menu {
    position: fixed; z-index: 400; visibility: hidden;
    background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
    min-width: 176px; max-height: min(320px, 60vh); overflow-y: auto;
    box-shadow: var(--shadow);
    padding: 4px;
    opacity: 0; transform: scale(.97) translateY(-2px);
    transform-origin: top right;
    transition: opacity 140ms cubic-bezier(.23,1,.32,1),
                transform 140ms cubic-bezier(.23,1,.32,1),
                visibility 140ms;
}
.rt-menu.open      { visibility: visible; opacity: 1; transform: scale(1) translateY(0); }
.rt-menu.flip-up   { transform-origin: bottom right; }
.rt-menu a,
.rt-menu button {
    display: flex; align-items: center; gap: .55rem;
    width: 100%; padding: 7px 10px; border-radius: var(--radius-xs);
    font-size: var(--fs-xs); font-family: inherit; color: var(--text);
    text-decoration: none; white-space: nowrap;
    background: none; border: none; text-align: left; cursor: pointer;
}
.rt-menu a:hover,  .rt-menu button:hover,
.rt-menu a:focus-visible, .rt-menu button:focus-visible {
    background: var(--accent-dim); color: var(--accent); outline: none;
}
.rt-menu svg {
    width: 14px; height: 14px; stroke: currentColor; fill: none;
    stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round;
    flex-shrink: 0; opacity: .75;
}
.rt-menu-label {
    padding: 7px 10px 3px; font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted);
}
.rt-menu-sep  { height: 1px; background: var(--border); margin: 4px 2px; }

/* A small form inside a dropdown — a date range that submits from the menu
   rather than opening a page to ask for two dates and then come back. */
/* Wider than the 176px a list of links needs: two date inputs and their labels
   do not fit, and the browser squeezed the dd/mm/yyyy fields until the calendar
   icon overlapped the day. */
.rt-menu:has(.rt-menu-form) { min-width: 268px; }
.rt-menu-form { padding: 4px 10px 8px; display: flex; flex-direction: column; gap: 6px; }
.rt-menu-form label {
    display: flex; align-items: center; gap: 6px;
    font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); white-space: nowrap;
}
.rt-menu-form label > span { flex: 0 0 34px; }
/* Vertical and right padding only. A date input carries a calendar SVG as a
   background at left 11px, and the base rule reserves room for it with
   padding-left: 36px — a `padding` shorthand here wiped that and the date
   rendered underneath the icon. */
.rt-menu-form input[type="date"] {
    flex: 1 1 auto; min-width: 0; width: 100%; font-size: var(--fs-xs);
    padding-top: 5px; padding-bottom: 5px; padding-right: 8px;
}
.rt-menu-form-actions { display: flex; gap: 6px; margin-top: 2px; }
.rt-menu-form-actions .btn { flex: 1 1 0; justify-content: center; }
.rt-menu form { margin: 0; }
.rt-menu .danger { color: var(--danger); }
.rt-menu .danger:hover, .rt-menu .danger:focus-visible { background: rgba(239,68,68,.1); color: var(--danger); }

@media (prefers-reduced-motion: reduce) {
    .rt-menu { transition: opacity 100ms linear, visibility 100ms; transform: none; }
    .rt-menu.open { transform: none; }
}

/* ── Segmented toggle (.view-toggle) ─────────────────────────────────────
   List/Calendar/Timeline, By Truck/By Driver, Type/Status, Week/Month — one
   control, used wherever a view is switched rather than filtered.

   It lived in tasks/index.php's inline <style>, so the moment the truck
   calendar grew a Week/Month pair the same markup rendered there as two bare
   blue links. Shared here instead of copied, or the two would drift. */
/* A segmented control: a recessed track, the chosen view raised out of it.
   Reads as ONE control with a state, not as three buttons in a row. */
.view-toggle {
    display: flex; gap: 2px; padding: 2px;
    background: var(--surface-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}
.view-toggle a {
    display: flex; align-items: center; gap: 6px; padding: 5px var(--space-3);
    border-radius: var(--radius-xs);
    font-size: var(--fs-sm); font-weight: var(--fw-medium); line-height: var(--control-lh);
    color: var(--text-muted); text-decoration: none; white-space: nowrap;
    transition: color var(--transition), background-color var(--transition), box-shadow var(--transition);
}
.view-toggle a:hover  { color: var(--text); text-decoration: none; }
.view-toggle a.active { color: var(--text); background: var(--surface); box-shadow: var(--shadow-sm), 0 0 0 1px var(--border); }
.view-toggle svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; }

/* ── Calendar event bars (.cal-ep) ────────────────────────────────────────
   Shared by the trip calendar, the truck calendar and the timeline so all
   three read as one product. A run spanning several days is ONE bar, named
   once at its start: drawn as separate pills it looked like several unrelated
   jobs.

   OUTLINE, not fill. Every bar is the same white card with black text; the
   status colour appears only as the border, the left spine and the dot. With
   a 19-truck fleet a tinted fill turned the month into stripes of pastel and
   the label was the first thing lost — coloured text on a coloured wash is
   the lowest-contrast combination on the page.

   The colour arrives as ONE custom property, `--ep`, set inline per bar. It
   was three separate inline declarations (background + color + shadow) at
   three call sites, so restyling meant editing PHP in three files and an
   inline `color` silently beat any rule here. Now the callers state the
   colour and this rule decides what is done with it. */
/* A clickable bar is a div with role=button, not a <button>: the run segments
   size themselves with the block auto-width formula (containing block minus
   margins), and the negative margins that join one bar across day cells are
   what make that come out right. A <button> takes an intrinsic content width
   instead, so a continuation segment rendered 11px wide — a column of stray
   outlines down the month. */
.cal-ep[role="button"] { cursor: pointer; font-family: inherit; text-align: left; }

.cal-ep {
    display: flex; align-items: center; gap: .3rem;
    /* Fixed integer height. Sized in rem the lane came out fractional, so each
       cell rounded its stack differently and a run drifted a pixel per lane —
       4px out by the tenth truck, and a hairline seam between day segments. */
    height: 20px; line-height: 1; box-sizing: border-box;
    margin-bottom: 2px; padding: 0 .4rem;
    border-radius: var(--radius-xs); font-size: var(--fs-xs); font-weight: 600;
    text-decoration: none; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis; max-width: 100%;
    cursor: pointer;

    background: var(--card);
    color: var(--text);
    border: 1px solid var(--ep, var(--border));
    /* The spine is what makes the status readable at a glance once the fill is
       gone — a 1px border alone is too quiet to scan a fleet by. */
    box-shadow: inset 3px 0 0 0 var(--ep, transparent);
    padding-left: calc(.4rem + 3px);
    transition: background-color 140ms cubic-bezier(.23,1,.32,1),
                border-color 140ms cubic-bezier(.23,1,.32,1);
}
/* Hover tints toward the status colour instead of brightening the fill —
   with a white bar, `filter: brightness()` did nothing visible at all. */
.cal-ep:hover {
    text-decoration: none;
    border-color: var(--ep, var(--text-muted));
    background: color-mix(in srgb, var(--ep, var(--text-muted)) 9%, var(--card));
}
.cal-ep-dot   { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; background: var(--ep, var(--text-muted)); }
.cal-ep-text  { overflow: hidden; text-overflow: ellipsis; color: var(--text); }

/* The legend is a key to the bars, so it is built the same way — outlined,
   neutral text, colour carried by the dot. Tinted pills next to outlined bars
   read as two different vocabularies for the same seven statuses. */
.cal-legend      { display: flex; gap: .4rem; flex-wrap: wrap; }
.cal-legend-item {
    display: inline-flex; align-items: center; gap: .35rem;
    font-size: var(--fs-xs); font-weight: 600;
    padding: .2rem .55rem; border-radius: var(--radius-pill);
    background: var(--card); color: var(--text-muted);
    border: 1px solid var(--border);
}
.cal-legend-dot  { width: 7px; height: 7px; border-radius: 50%; background: var(--ep); flex-shrink: 0; }

/* Multi-day run: square the joined edges and bleed into the cell padding so
   the segments meet across the day boundary. The side borders come off the
   middle segments too — with an outlined bar, leaving them draws a vertical
   rule at every midnight and a five-day run reads as five boxes again. */
.cal-ep-run             { border-radius: 0; border-left: none; border-right: none; box-shadow: none;
                          margin-left: -.35rem; margin-right: -.35rem; padding-left: .35rem; padding-right: .35rem; }
.cal-ep-run.cal-ep-start{ border-top-left-radius: var(--radius-xs);  border-bottom-left-radius: var(--radius-xs);  margin-left: 0;
                          border-left: 1px solid var(--ep, var(--border));
                          box-shadow: inset 3px 0 0 0 var(--ep, transparent);
                          padding-left: calc(.38rem + 3px); }
.cal-ep-run.cal-ep-end  { border-top-right-radius: var(--radius-xs); border-bottom-right-radius: var(--radius-xs); margin-right: 0;
                          border-right: 1px solid var(--ep, var(--border)); padding-right: .38rem; }
.cal-ep-run:not(.cal-ep-start) .cal-ep-dot  { display: none; }
.cal-ep-run:not(.cal-ep-start) .cal-ep-text { opacity: .45; font-weight: 500; }

/* "+N more" used to be plain text that counted hidden events and offered no way
   to see them. It is a button; the cell expands. */
.cal-ep-more {
    display: block; width: 100%; text-align: left;
    font-size: var(--fs-xs); color: var(--text-muted);
    padding: 1px 4px; margin-top: .1rem; border-radius: 3px;
    background: none; border: none; font-family: inherit; cursor: pointer;
}
.cal-ep-more:hover { color: var(--accent); background: rgba(37,99,235,.08); }
.cal-ep-hidden { display: none; }
.is-expanded .cal-ep-hidden { display: flex; }
.is-expanded .cal-ep-more   { color: var(--accent); }

/* Hover card. position:fixed because the day cell is overflow:hidden — an
   in-flow card would be clipped by the very cell it describes. */
.cal-tip {
    position: fixed; z-index: 500; pointer-events: none; max-width: 280px;
    padding: .6rem .7rem; background: var(--card);
    border: 1px solid var(--border); border-radius: var(--radius);
    box-shadow: var(--shadow);
    opacity: 0; transform: translateY(-3px);
    transition: opacity 130ms cubic-bezier(.23,1,.32,1), transform 130ms cubic-bezier(.23,1,.32,1);
}
.cal-tip.show      { opacity: 1; transform: translateY(0); }
.cal-tip-ref       { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-muted); letter-spacing: .3px; }
.cal-tip-title     { font-size: var(--fs-sm); font-weight: 600; color: var(--text); margin: .1rem 0 .35rem; }
.cal-tip-row       { display: flex; gap: .4rem; font-size: var(--fs-xs); line-height: 1.5; }
.cal-tip-key       { color: var(--text-muted); min-width: 62px; flex-shrink: 0; }
.cal-tip-val       { color: var(--text); }

@media (prefers-reduced-motion: reduce) {
    .cal-tip { transition: opacity 100ms linear; transform: none; }
    .cal-tip.show { transform: none; }
}

/* ── Confirm dialog (.rt-confirm) ─────────────────────────────────────────
   Replaces window.confirm(), which is OS-drawn, unstyleable, and shows the
   page's URL above the message. Native <dialog> because the browser already
   does the hard parts correctly: focus trap, Escape, inert background, and a
   ::backdrop — all of which a hand-rolled div would have to reimplement. */
.rt-confirm {
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--card); color: var(--text);
    padding: 0; max-width: 420px; width: calc(100vw - 2rem);
    box-shadow: var(--shadow-lg);
    /* A modal <dialog> is centred by the UA's own `margin: auto`. The global
       reset sets `margin: 0` on everything, which silently wins and pins the
       dialog to the top-left corner — so state it back explicitly. */
    margin: auto;
}
.rt-confirm::backdrop { background: rgba(8,15,28,.55); backdrop-filter: blur(2px); }
.rt-confirm-body  { padding: 1.25rem 1.35rem 1rem; }
.rt-confirm-icon  {
    width: 38px; height: 38px; border-radius: 50%; margin-bottom: .8rem;
    display: flex; align-items: center; justify-content: center;
    background: rgba(37,99,235,.12); color: var(--accent);
}
.rt-confirm-icon svg { width: 19px; height: 19px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.rt-confirm.is-danger .rt-confirm-icon { background: rgba(239,68,68,.12); color: var(--danger); }
.rt-confirm-title { font-size: var(--fs-md); font-weight: 600; margin: 0 0 .3rem; }
.rt-confirm-text  { font-size: var(--fs-sm); color: var(--text-muted); line-height: 1.55; margin: 0; white-space: pre-line; }
.rt-confirm-actions {
    display: flex; justify-content: flex-end; gap: .5rem;
    padding: .85rem 1.35rem 1.1rem;
}
.rt-confirm[open] { animation: rtConfirmIn 160ms cubic-bezier(.23,1,.32,1); }
@keyframes rtConfirmIn { from { opacity: 0; transform: scale(.97) translateY(-4px); } }
@media (prefers-reduced-motion: reduce) { .rt-confirm[open] { animation: none; } }



/* Continuous calendar runs.
   The bars bled by the cell padding but the cell was overflow:hidden and had a
   1px right border, so each day's segment was clipped just short of the next —
   a three-day trip still read as three separate bars. */
.month-grid .mcell { overflow: visible; }
/* max-width:100% on .cal-ep caps the box at the cell's content width, so the
   negative margins below were computed and then ignored — the bar stopped
   exactly at the padding edge and a three-day trip read as three separate bars.
   The run has to be allowed to outgrow its cell to bridge the gap. */
/* One extra pixel of bleed. A 7-column grid rarely divides into whole pixels,
   so an exact-fit bleed lands mid-pixel and leaves a hairline between day
   segments. Adjacent segments are the same colour, so the overlap is invisible
   while the seam was not. */
.cal-ep-run             { max-width: none; margin-left: calc(-.35rem - 2px); margin-right: calc(-.35rem - 2px); }
.cal-ep-run.cal-ep-start{ margin-left: 0; }
.cal-ep-run.cal-ep-end  { margin-right: 0; }

/* ── Time picker panel ────────────────────────────────────────────────────
   Same shell, spacing and type as .ui-cal-panel — a form shouldn't show the
   app's own calendar next to the OS's stock blue time list. */
.ui-time-input::-webkit-calendar-picker-indicator { display: none !important; }
.ui-time-input { cursor: pointer; }
.ui-time-input.is-open { border-color: var(--accent); box-shadow: var(--focus-ring); }

.ui-time-panel { padding: 10px; width: max-content; }
.ui-time-cols  { display: flex; gap: 8px; align-items: stretch; }

.ui-time-colwrap { display: flex; flex-direction: column; min-width: 0; }
.ui-time-colhead {
  font-size: var(--fs-xs); font-weight: 600;
  color: var(--text-muted); text-align: center; padding-bottom: 6px;
  border-bottom: 1px solid var(--border); margin-bottom: 5px;
}
.ui-time-col {
  display: flex; flex-direction: column; gap: 2px;
  /* Both lists are exactly twelve rows, so show all twelve: 12 x 32px plus the
     gaps. A scroll that only ever moves a little is pure friction, and hiding
     half the hours behind it is why the old sixty-row minute column was so
     awkward. Capped against the viewport so a short window still scrolls
     rather than the panel running off the screen. */
  max-height: min(410px, 46vh); overflow-y: auto; overscroll-behavior: contain;
  scrollbar-width: thin; scrollbar-color: var(--border) transparent;
  padding-right: 3px;
}
.ui-time-col::-webkit-scrollbar        { width: 6px; }
.ui-time-col::-webkit-scrollbar-track  { background: transparent; }
.ui-time-col::-webkit-scrollbar-thumb  { background: var(--border); border-radius: var(--radius-pill); }
.ui-time-col::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }

.ui-time-cell {
  flex: 0 0 auto; height: 32px; width: 3.25rem;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: none; color: var(--text);
  font: inherit; font-size: var(--fs-xs); font-variant-numeric: tabular-nums;
  cursor: pointer; padding: 0; transition: background-color .12s, color .12s;
}
.ui-time-cell:hover        { background: var(--card-hover); }
.ui-time-cell.is-selected  { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }

/* Two choices, so two buttons — not a column that looks like it scrolls. */
.ui-time-mer {
  display: flex; flex-direction: column; gap: 6px;
  padding-left: 10px; margin-left: 2px;
  /* Top-aligned with the first hour, not centred against the whole column —
     centred it floated in the middle of the panel attached to nothing. The
     offset clears the column headers. No divider rule: top-aligned, it stopped
     halfway down the panel and drew attention to its own end. The buttons
     carry their own borders, which is separation enough. */
  align-self: flex-start; padding-top: 27px;
}
.ui-time-merbtn {
  height: 38px; width: 3.5rem; border-radius: var(--radius);
  border: 1px solid var(--border); background: var(--card); color: var(--text-muted);
  font: inherit; font-size: var(--fs-xs); font-weight: 600; letter-spacing: .04em;
  cursor: pointer; transition: background-color .12s, color .12s, border-color .12s;
}
.ui-time-merbtn:hover       { border-color: var(--text-muted); color: var(--text); }
.ui-time-merbtn.is-selected { background: var(--accent); border-color: var(--accent); color: #fff; }

.ui-time-panel .ui-cal-foot { margin-top: 10px; padding-top: 9px; border-top: 1px solid var(--border); }

/* On a phone the three columns still fit, but the cells want a bigger target. */
@media (max-width: 480px) {
  .ui-time-cell   { width: 3rem; height: 36px; }
  .ui-time-merbtn { width: 3.1rem; height: 40px; }
  .ui-time-col    { max-height: 200px; }
}

/* An empty lane in a day cell. Same box as a bar but invisible — it holds the
   row so a run that shares a week with a shorter job stays on one line. */
.cal-ep-spacer {
    height: 20px;          /* must equal .cal-ep exactly, or lanes drift */
    margin-bottom: 2px;
    pointer-events: none;
}

/* The range control is wider than a plain select, so it gets two grid tracks
   in a filter bar rather than being squeezed into one. */
/* Only span two tracks when the grid actually has two — otherwise the span
   forces the panel wider than its container on a narrow screen. */
@media (min-width: 900px) { .filter-field-range { grid-column: span 2; } }
.filter-field-range .field-inline > label {
    display: block; font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); margin-bottom: .3rem;
}

/* ── Colour picker (controls.js: enhanceColor) ─────────────────────────────
   A swatch the size of a control that opens a palette in the app's own panel
   shell. The native <input type="color"> keeps the same look below, so with no
   script — or on a touch screen, which keeps the OS picker — the field reads
   the same and only the dialog differs. One rule for every colour field: the
   Job types and Maintenance types screens each carried their own. */
input[type="color"],
.ui-color-trigger {
  width: var(--control-h); height: var(--control-h);
  padding: 3px;
  border: 1px solid var(--control-border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
  transition: border-color var(--transition), box-shadow var(--transition), transform 120ms var(--ease-out);
}
input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
input[type="color"]::-webkit-color-swatch { border: 0; border-radius: var(--radius-xs); }
input[type="color"]::-moz-color-swatch    { border: 0; border-radius: var(--radius-xs); }
input[type="color"]:hover,
.ui-color-trigger:hover { border-color: var(--control-border-hover); }
input[type="color"]:focus-visible,
.ui-color-trigger:focus-visible,
.ui-color.is-open .ui-color-trigger { outline: none; border-color: var(--accent); box-shadow: var(--focus-ring); }
.ui-color-trigger:active { transform: scale(0.94); }

.ui-color { position: relative; display: inline-flex; vertical-align: middle; }
.ui-color-native {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  opacity: 0; pointer-events: none;
}
/* The fill, inset from the border so a pale colour still has an edge. */
.ui-color-trigger::before,
.ui-color-swatch,
.ui-color-preview {
  background: var(--c);
  box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.12);
}
.ui-color-trigger::before {
  content: '';
  display: block;
  width: 100%; height: 100%;
  border-radius: var(--radius-xs);
}

.ui-color-panel { padding: var(--space-3); }
.ui-color-title {
  margin: 0 0 var(--space-2);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  color: var(--text-muted);
}
.ui-color-grid {
  display: grid;
  grid-template-columns: repeat(6, 28px);
  gap: var(--space-2);
}
.ui-color-swatch {
  position: relative;
  width: 28px; height: 28px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: transform 120ms var(--ease-out), box-shadow 120ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .ui-color-swatch:hover { transform: scale(1.1); }
}
/* Selected: a gap ring in the swatch's own colour, and a tick. */
.ui-color-swatch.is-selected { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--c); }
.ui-color-swatch.is-selected::after {
  content: '';
  position: absolute;
  left: 10px; top: 6px;
  width: 6px; height: 11px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.ui-color-swatch:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent); }
/* Already another row's colour: a small mark, so two job types do not end up
   as the same bar on the calendar without anybody choosing that. */
.ui-color-swatch.is-used:not(.is-selected)::before {
  content: '';
  position: absolute;
  right: 3px; bottom: 3px;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.25);
}

.ui-color-foot {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
.ui-color-preview {
  flex: none;
  width: var(--control-h-sm); height: var(--control-h-sm);
  border-radius: var(--radius-sm);
}
.ui-color-panel .ui-color-hex {
  flex: 1 1 auto;
  width: 0; min-width: 0;
  height: var(--control-h-sm);
  padding: 0 var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  text-transform: uppercase;
}
.ui-color-panel .ui-color-hex.is-invalid { border-color: var(--danger); box-shadow: var(--focus-ring-danger); }
/* The free picker: any colour at all. A saturation x brightness area over the
   current hue, and a hue bar beneath it — the only two gradients in the app,
   because they ARE the control rather than decoration on one. */
.ui-color-free {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
.ui-color-sv {
  position: relative;
  height: 128px;
  border-radius: var(--radius-sm);
  background:
    linear-gradient(to top, #000, transparent),
    linear-gradient(to right, #fff, var(--hue, red));
  box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.12);
  cursor: crosshair;
  touch-action: none;
}
.ui-color-hue {
  position: relative;
  height: 12px;
  margin: 0 7px;              /* the thumb's half-width, so it never hangs off */
  border-radius: var(--radius-pill);
  background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
  box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.12);
  cursor: pointer;
  touch-action: none;
}
.ui-color-sv-thumb,
.ui-color-hue-thumb {
  position: absolute;
  width: 14px; height: 14px;
  border: 2px solid #fff;
  border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.35), 0 1px 3px rgba(15, 23, 42, 0.3);
  transform: translate(-50%, -50%);
  pointer-events: none;
}
.ui-color-sv-thumb  { background: var(--c); }
.ui-color-hue-thumb { top: 50%; background: transparent; }
.ui-color-sv:focus-visible,
.ui-color-hue:focus-visible { outline: none; box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.12), var(--focus-ring); }

.ui-color-drop { flex: none; }
.ui-color-drop svg { width: 15px; height: 15px; }
@media (prefers-reduced-motion: reduce) {
  .ui-color-swatch, .ui-color-trigger { transition: none; }
  .ui-color-swatch:hover, .ui-color-trigger:active { transform: none; }
}

/* ── Vertical rhythm ──────────────────────────────────────────────────────
   A card inside a grid is spaced by the grid's own gap; its margin would add a
   second helping. And the last section on a page shouldn't push the footer. */
.detail-grid > .card,
.stats-grid  > .card,
.form-grid   > .card { margin-bottom: 0; }
.main-content > :last-child { margin-bottom: 0; }

/* ── Responsive: page chrome ──────────────────────────────────────────────
   Headers pack a title, a view toggle and a primary action on one row. Below
   the desktop width that row was wider than the viewport and dragged the whole
   layout with it, so every page reported horizontal overflow. */
.page-header, .page-head { flex-wrap: wrap; }
.page-header > *, .page-head > * { min-width: 0; }

@media (max-width: 1100px) {
  .sub-nav, .user-toolbar { flex-wrap: wrap; gap: var(--space-2); }
}
/* One gutter per width class: 24px on a desktop, 16px from a tablet down. */
@media (max-width: 900px) {
  :root { --card-pad: var(--space-4); }
  .main-content { padding-left: var(--space-4); padding-right: var(--space-4); }
  .detail-grid { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
  .page-header, .page-head { align-items: flex-start; gap: var(--space-3); }
  .page-header .btn, .page-head .btn { flex: 0 1 auto; }
  .filter-grid { grid-template-columns: 1fr; }
  /* A month grid can't usefully compress to a phone; let it scroll as a unit
     rather than crushing seven columns into 390px. */
  .month-grid, .dow-header { min-width: 640px; }
  .trip-cal-card, .cal-card { overflow-x: auto; }
}

/* ── Calendar: show every lane ────────────────────────────────────────────
   A month cell shows three bars and hides the rest behind "+N more". With a
   10+ truck fleet booked on the same week that hides most of the fleet, and
   expanding one cell pushes its bars out of line with the same run in the next
   cell. This reveals every lane in every cell at once, so the rows stay
   aligned and a full week is readable. */
.month-grid.show-all .cal-ep-hidden { display: flex; }
.month-grid.show-all .cal-ep-more   { display: none; }

.cal-expand {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .34rem .7rem; border: 1px solid var(--border);
    border-radius: var(--radius-sm); background: var(--card);
    font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted);
    font-family: inherit; cursor: pointer;
}
/* [hidden] sets display:none in the UA sheet, which a class-level
   display:inline-flex silently beats — the button stayed on screen reading
   "Show all (0)" with nothing to reveal. */
.cal-expand[hidden]    { display: none; }
.cal-expand:hover      { color: var(--text); border-color: var(--text-muted); }
.cal-expand[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); background: var(--accent-dim); }
.cal-expand svg { width: 13px; height: 13px; stroke: currentColor; fill: none; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }

/* ── Table column sort + filter ───────────────────────────────────────────
   The pattern the OCR invoice app uses: every header carries a sort toggle and
   a filter button, and the popover lists the values actually present in that
   column. Filtering happens server-side over the whole set, not the visible
   page — a filter that only searched the 50 rows in front of you would be
   worse than none, because it looks like it searched everything. */
.th-cell {
    display: flex; align-items: center; gap: .3rem;
    justify-content: space-between; min-width: 0;
}
/* A number column's heading sits over its numbers, as th.num already says. */
th.num .th-cell { justify-content: flex-end; }
.th-sort {
    color: inherit; text-decoration: none; font: inherit;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.th-sort:hover      { color: var(--accent); text-decoration: none; }
.th-sort.is-sorted  { color: var(--accent); }

.th-filter {
    flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; padding: 0;
    border: none; border-radius: var(--radius-xs); background: none;
    color: var(--text-dim); cursor: pointer; opacity: .45;
}
th:hover .th-filter, .th-filter.is-active, .th-filter[aria-expanded="true"] { opacity: 1; }
.th-filter:hover    { background: var(--card-hover); color: var(--text); }
.th-filter.is-active{ color: var(--accent); opacity: 1; }
.th-filter svg      { width: 11px; height: 11px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linejoin: round; }

/* position:fixed — a table wrapper is overflow:auto and would clip an in-flow
   panel to a couple of rows. JS lifts these to the body and places them. */
.th-pop {
    position: fixed; z-index: 420; width: 250px;
    background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
    box-shadow: var(--shadow-lg); padding: 8px;
    font-weight: 400; text-transform: none; letter-spacing: 0;
}
.th-pop[hidden]     { display: none; }
.th-pop-title       { font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); margin: 2px 2px 6px; }
.th-pop-search      { width: 100%; margin-bottom: 6px; font-size: var(--fs-xs); padding: 5px 8px; }
.th-pop-list        { max-height: 230px; overflow-y: auto; margin: 0 -2px; }
.th-pop-item        { display: flex; align-items: center; gap: .45rem; padding: 4px 6px;
                      border-radius: var(--radius-xs); cursor: pointer; font-size: var(--fs-xs); }
.th-pop-item:hover  { background: var(--card-hover); }
.th-pop-item[hidden]{ display: none; }
.th-pop-item input  { flex: 0 0 auto; margin: 0; }
.th-pop-text        { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.th-pop-count       { flex: 0 0 auto; font-size: var(--fs-xs); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.th-pop-date        { display: flex; align-items: center; gap: .5rem; margin-bottom: 6px;
                      font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); }
.th-pop-date > span { flex: 0 0 34px; }
/* Vertical/right padding only — the base rule reserves padding-left for the
   calendar background icon, and a shorthand here would put the date under it. */
.th-pop-date input  { flex: 1 1 auto; min-width: 0; width: 100%; font-size: var(--fs-xs);
                      padding-top: 5px; padding-bottom: 5px; padding-right: 8px; }
.th-pop-actions     { display: flex; gap: 6px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); }
.th-pop-actions > * { flex: 1 1 0; justify-content: center; text-align: center; }

/* What is currently filtered, and a way to drop each one. Without this the
   only clue is a tinted funnel icon in a header you may have scrolled past. */
.th-chips      { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .75rem; }
.th-chip {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: 3px 8px 3px 10px; border-radius: var(--radius-pill);
    background: var(--accent-dim); border: 1px solid rgba(37,99,235,.28);
    color: var(--accent); font-size: var(--fs-xs); font-weight: 600; text-decoration: none;
}
.th-chip:hover { text-decoration: none; filter: brightness(1.08); }
.th-chip svg   { width: 11px; height: 11px; stroke: currentColor; fill: none; stroke-width: 2.4; stroke-linecap: round; }

/* Search over every column at once, for "I don't know which column this is in".

   ONE control, not two. The submit used to be a .btn-sm standing beside the
   field — 9px shorter than the input it belonged to, and a second thing to aim
   at for a single action. It sits inside the field now: magnifier on the left
   to say what the box is, the button on the right to run it. */
.table-search {
    position: relative; display: flex; align-items: center;
    flex: 1 1 220px; max-width: 320px; height: var(--control-h);
    background: var(--surface); border: 1px solid var(--control-border);
    border-radius: var(--radius-sm);
    transition: border-color var(--transition), box-shadow var(--transition);
}
/* :focus-within, so the ring belongs to the whole pill. Put on the input it
   would draw a second border inside the first. */
.table-search:hover { border-color: var(--control-border-hover); }
.table-search:focus-within { border-color: var(--accent); box-shadow: var(--focus-ring); }
.table-search input {
    flex: 1 1 auto; min-width: 0; height: 100%;
    padding: 0 .35rem 0 30px;
    border: none; background: none; border-radius: inherit;
}
.table-search input:focus { border: none; box-shadow: none; }
.table-search > svg {
    position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
    width: 13px; height: 13px; stroke: var(--text-muted); fill: none; stroke-width: 2; pointer-events: none;
}
.table-search-go {
    flex: none; display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: calc(var(--control-h) - 8px); margin-right: 3px;
    border: none; border-radius: calc(var(--radius-sm) - 2px);
    background: rgba(37,99,235,.08); color: var(--accent);
    cursor: pointer; transition: background var(--transition);
}
.table-search-go:hover { background: rgba(37,99,235,.18); }
.table-search-go svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 2.2; stroke-linecap: round; }

/* In a dialog the pill has a column to itself, so the toolbar's 320px ceiling —
   which exists to stop it eating a row shared with other controls — does not
   apply. */
.modal-body .table-search { max-width: none; margin-bottom: .6rem; }

.table-toolbar {
    display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
    margin-bottom: var(--stack-gap);
}

/* ── Month grid (days as cells) ───────────────────────────────────────────
   Read by the trip calendar AND the Calendar page. */
/* ── Calendar ──────────────────────────────────────────── */
.trip-cal-card { background:var(--card); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden; margin-bottom:var(--stack-gap); }
/* Three columns, not space-between. The month has to sit in the MIDDLE OF THE
   CARD, and with space-between it lands wherever the two side groups leave it
   — measured drifting ~200px between a calendar with a twelve-item legend and
   one with none, on the same page. `1fr auto 1fr` makes the side cells equal by
   construction, so the centre cell is centred whatever they contain.

   min-width: 0 lets a long legend wrap inside its own cell instead of pushing
   the month off centre, which is the failure this replaces. */
.cal-nav, .cal-toolbar {
    display: grid; grid-template-columns: 1fr auto 1fr;
    align-items: center; gap: .6rem;
    padding: .7rem 1.1rem; border-bottom: 1px solid var(--border);
}
.cal-nav > *, .cal-toolbar > * { min-width: 0; }
/* Three groups of controls share a third of the card each, so they sit tighter
   here than on an open toolbar — same height, same type, less side padding. */
.cal-nav .view-toggle a, .cal-toolbar .view-toggle a { padding-inline: 10px; }
.cal-nav .btn, .cal-toolbar .btn { padding-inline: var(--space-3); }
.cal-nav > :last-child, .cal-toolbar > :last-child { justify-self: end; }
.cal-nav h2 { font-size:var(--fs-sm); font-weight:600; margin:0; }
.dow-header { display:grid; grid-template-columns:repeat(7,1fr); background:var(--card); border-bottom:1px solid var(--border); }
.dow-header span { text-align:center; font-size:var(--fs-xs); font-weight:600; color:var(--text-muted); padding:.45rem 0; }
.month-grid { display:grid; grid-template-columns:repeat(7,1fr); grid-auto-rows:minmax(100px,auto); border-left:1px solid var(--border); border-top:1px solid var(--border); }
.mcell { border-right:1px solid var(--border); border-bottom:1px solid var(--border); padding:.3rem .35rem; overflow:hidden; }
.mcell.other-month { background:var(--surface-3); }
.mcell.is-today .day-num { background:var(--accent); color:#fff; border-radius:50%; width:22px; height:22px; display:flex; align-items:center; justify-content:center; }
.day-num { font-size:var(--fs-xs); font-weight:600; color:var(--text-muted); margin-bottom:.25rem; width:22px; height:22px; display:flex; align-items:center; justify-content:center; }

/* ── Timeline / resource calendar (rows × days) ───────────────────────────
   Read by the trip timeline AND the Calendar page. It lived in
   tasks/index.php's inline <style>, which is the same trap .view-toggle
   fell into: a second page drawing the same markup got no styles at all. */
/* ── Timeline (resource rows × days) ──────────────────────
   A trip is ONE grid item spanning grid-column from/to, so a run is
   continuous by construction — no per-day segments to bleed into each
   other, and no "+N more" because a row grows to fit its own lanes. */
.tl-scroll { overflow-x: auto; overflow-y: visible; }
/* width:max-content, or the row box is only as wide as the VIEWPORT while its
   track overflows past it — so the bottom border and any row background stop
   at the fold and everything right of it renders as blank white. Measured:
   2674px of content in a 1126px row. min-width keeps a near-empty week
   filling the card. */
.tl-row {
    display: flex; align-items: stretch; border-bottom: 1px solid var(--border);
    /* max-content, or the row box is only as wide as the VIEWPORT while its
       track overflows past it — the bottom border and any row background then
       stop at the fold and everything right of it renders as blank white.
       Measured: 2674px of content in a 1126px row. min-width keeps a short
       week filling the card, and resolves against .tl-scroll, whose width is
       the card's and therefore definite. */
    width: max-content; min-width: 100%;
}
.tl-row:last-child { border-bottom: none; }
.tl-row.is-idle .tl-label { opacity: .5; }

/* Sticky so the plate stays readable while the month scrolls under it —
   on a phone the 31 columns are far wider than the viewport. */
.tl-label {
    flex: 0 0 132px; position: sticky; left: 0; z-index: 2;
    display: flex; flex-direction: column; justify-content: center;
    padding: .45rem .7rem; background: var(--card);
    border-right: 1px solid var(--border); min-width: 0;
}
.tl-label strong { font-size: var(--fs-xs); font-weight: 600; color: var(--text);
                   overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tl-label span   { font-size: var(--fs-xs); color: var(--text-muted);
                   overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.tl-track {
    flex: 1 1 auto; position: relative;
    /* Wide enough for a customer name, not a day number. At 27px a
       one-day job showed "D." and the grid told you a truck was busy
       without saying who for — which is the one thing their spreadsheet
       cell does say. A month is then wider than the screen and scrolls,
       exactly as their sheet does across 75 printed pages, and the
       sticky plate column keeps the row identifiable while it does. */
    /* DEFINITE, not just a minimum. Under the row's max-content sizing a
       flexible track resolves to the widest BAR's text, so one long customer
       name made its row wider than its neighbours and the right edge came out
       ragged. A definite width makes every row the same length. */
    width: calc(var(--days) * 82px);
    min-width: calc(var(--days) * 82px);
    display: grid; grid-template-columns: repeat(var(--days), minmax(0, 1fr));
    grid-auto-rows: 20px; row-gap: 2px; padding: 5px 0;
    /* Day separators as a gradient rather than 31 empty cells per row. */
    background-image: repeating-linear-gradient(to right,
        var(--border) 0, var(--border) 1px,
        transparent 1px, transparent calc(100% / var(--days)));
}
.tl-head { border-bottom: 1px solid var(--border); background: var(--card); position: sticky; top: 0; z-index: 3; }
.tl-head .tl-label { font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); }
.tl-head .tl-track { grid-auto-rows: auto; padding: .3rem 0; background-image: none; }
.tl-day { display: flex; flex-direction: column; align-items: center; gap: 1px; padding: 0 1px; }
.tl-dow  { font-size: var(--fs-xs); color: var(--text-muted); }
.tl-dnum { font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.tl-day.is-weekend .tl-dow, .tl-day.is-weekend .tl-dnum { opacity: .45; }
.tl-day.is-today .tl-dnum {
    background: var(--accent); color: #0b1220; border-radius: 50%;
    width: 18px; height: 18px; display: flex; align-items: center; justify-content: center;
}

/* Absolute, not a grid item: grid-row:1/-1 only spans explicit rows and
   these rows are implicit, so the marker would collapse to one lane. */
.tl-now {
    position: absolute; top: 0; bottom: 0;
    left: calc((var(--i) - 1) * 100% / var(--days));
    width: calc(100% / var(--days));
    background: rgba(37,99,235,.09);
    border-left: 1px solid rgba(37,99,235,.5);
    pointer-events: none;
}

/* Beats .cal-ep's max-width:100% and margin-bottom — here the grid area
   is the width, and row-gap does the spacing. */
.tl-track .tl-bar { max-width: none; min-width: 0; margin-bottom: 0; position: relative; z-index: 1; }

/* A day the resource is off the road or off work. Hatched rather than
   solid, so at a glance it reads as an absence of work rather than another
   job — the whole reason their sheet keeps both in one grid. Drawn by the
   Calendar page; the trip timeline shows trips only. */
.tl-track .tl-block {
    background-image: repeating-linear-gradient(45deg,
        transparent 0 4px, color-mix(in srgb, var(--ep) 22%, transparent) 4px 8px);
}
.tl-track .tl-block .cal-ep-text { font-weight: 600; letter-spacing: .01em; }

.tl-foot {
    padding: .55rem 1.1rem; border-top: 1px solid var(--border);
    font-size: var(--fs-xs); color: var(--text-muted);
}

/* The nav packs month, paging, grouping and a seven-item legend on one
   row. Below this it shrinks them all instead, and every control wraps
   mid-word — so let it stack, month first. */
/* Below this the three columns cannot each hold their controls, so they stack —
   month first, because it is what the screen is about. */
@media (max-width: 760px) {
    .cal-nav, .cal-toolbar { grid-template-columns: 1fr; }
    .cal-nav > :last-child, .cal-toolbar > :last-child { justify-self: start; }
    .cal-nav .cal-jump, .cal-toolbar .cal-jump { order: -1; justify-content: flex-start; }
}
@media (max-width: 640px) { .tl-label { flex-basis: 100px; } }

/* ── Renewals due ──────────────────────────────────────
   The reminder list on the Truck Scheduler: road tax, insurance, service — each
   derived from the last completed one plus that type's interval. A list, not a
   card grid: these are read top to bottom and acted on one at a time, and the
   overdue ones have to be the first thing the eye lands on. */
.renew-card { padding: 0; overflow: hidden; }
.renew-card .card-header { padding: var(--card-pad) var(--card-pad) 0; }
.renew-list { list-style: none; margin: var(--space-3) 0 0; padding: 0; }
.renew-row {
  display: flex; align-items: baseline; flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  padding: 9px var(--card-pad);
  font-size: var(--fs-xs);
}
.renew-row + .renew-row { border-top: 1px solid var(--border); }
/* Overdue is the only state that earns colour here — a list where every row is
   coloured has no ranking, and ranking is the whole job of this panel. The
   ranking is carried by the tint, the red figures and the ordering (soonest
   first), NOT by a coloured rule down the left edge: a thick side border is the
   most recognisable tell of a generated interface and this sheet's own floor
   caps one at 1px, where it would be too faint to rank anything anyway. */
.renew-row.is-late { background: rgba(220,38,38,.05); }
.renew-what { font-weight: 600; }
.renew-when { color: var(--text); }
.renew-row.is-late .renew-when { color: var(--status-overdue); }
.renew-when strong { font-variant-numeric: tabular-nums; }
/* The provenance — "last done X, every N months" — is why the row is here at
   all, so it stays on the line rather than in a tooltip, but it recedes. */
.renew-last { margin-left: auto; color: var(--text-muted); font-size: var(--fs-xs); }
@media (max-width: 700px) {
  .renew-row { padding-left: calc(var(--space-4) - 3px); padding-right: var(--space-4); }
  .renew-last { margin-left: 0; flex-basis: 100%; }
}

/* ── The trip status log ───────────────────────────────
   One line per status change, oldest first, on a trip's own page. It replaced a
   snaking node path that got harder to read the more the trip had been
   corrected — which is exactly when somebody opens this card.

   The phase colour rides on a DOT before the phase word, off --st (the palette
   above) — the same 6px dot .badge::before already draws everywhere else in this
   app, so the log borrows the vocabulary rather than inventing one. Not a badge
   per row: seven stacked badges read as seven buttons. Not a coloured left rule
   either — a thick side border is the most recognisable tell of a generated
   interface. Timestamps are tabular so the column is a column. */
.st-log { list-style: none; margin: 0; padding: 0; }
.st-log-row {
  --st: var(--text-muted);
  display: flex; align-items: baseline; flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  padding: 7px var(--card-pad);
  font-size: var(--fs-xs); line-height: 1.45;
}
.st-log-row + .st-log-row { border-top: 1px solid var(--border); }
.st-log-when {
  flex: 0 0 auto; min-width: 8.5em;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted); font-size: var(--fs-xs);
}
.st-log-what {
  flex: 0 0 auto; font-weight: 600; color: var(--st);
  display: inline-flex; align-items: baseline; gap: 6px;
}
.st-log-what::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex-shrink: 0;
  /* Baseline-aligned flex puts a bare dot on the text baseline; nudge it to the
     optical middle of the lowercase, which is where .badge's own dot sits. */
  transform: translateY(-1px);
}
.st-log-who  { flex: 0 0 auto; color: var(--text-muted); }
/* The note is the part with something to say, so it takes the rest of the line
   and wraps rather than truncating — a cancellation reason cut off mid-sentence
   is the one entry you needed to read in full. */
.st-log-note { flex: 1 1 14em; color: var(--text); }
.st-log-note::before { content: '— '; color: var(--text-muted); }
@media (max-width: 560px) {
  .st-log-row { gap: 2px var(--space-2); padding-left: calc(var(--space-4) - 2px); padding-right: var(--space-4); }
  .st-log-when { flex-basis: 100%; }
}

/* The e-invoice opt-in that sits in the Send action bar. Deliberately looks
   like a decision and not like a setting: the warning is part of the label, so
   the consequence is read at the moment of ticking rather than in a help panel
   somebody opens once. It never carries a checked state on render — see the
   comment in finance/invoice-detail.php for why that is the point. */
.inv-einv-tick {
  display: flex; align-items: flex-start; gap: var(--space-2);
  margin-right: var(--space-3); cursor: pointer;
  font-size: var(--fs-xs); font-weight: 600; line-height: 1.3;
  /* Same inheritance trap as .act-split-opt: the bare `label` rule is written
     for a form-field caption and made this read "SUBMIT E-INVOICE TO LHDN TOO"
     in tracked caps next to a sentence-case button. It had been shipping that
     way; it is a sentence again. */
  text-transform: none; letter-spacing: normal;
}
.inv-einv-tick input { margin-top: 2px; flex-shrink: 0; accent-color: var(--status-overdue); }
.inv-einv-tick small {
  display: block; font-weight: 500; font-size: var(--fs-xs);
  color: var(--status-overdue); margin-top: 1px;
}
.inv-einv-tick:has(input:checked) { color: var(--status-overdue); }

/* ── The ledger strip ──────────────────────────────────
   A running balance said as the sum it is: opening + in − out = closing. It
   replaced four stat cards on the cashbook, which gave every term equal weight
   and hid the arithmetic between them behind four decorative icons. The
   operators are real type, not borders, and the total is the only term that
   reads as an answer. Numerals are tabular so the columns line up as figures
   rather than as words. Scrolls inside itself on a narrow screen — the sum has
   to stay on one line to read as a sum. */
.ledger-strip {
  display: flex; align-items: flex-end; gap: var(--space-4);
  flex-wrap: nowrap; overflow-x: auto;
  padding: var(--space-4) var(--card-pad);
  margin-bottom: var(--stack-gap);
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius);
}
.ledger-strip .ls-term { display: flex; flex-direction: column; gap: 2px; white-space: nowrap; }
.ledger-strip .ls-label {
  font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted);
}
.ledger-strip .ls-value {
  font-size: var(--fs-md); font-weight: 600;
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
}
.ledger-strip .ls-value.is-in  { color: var(--status-completed); }
.ledger-strip .ls-value.is-out { color: var(--status-overdue); }
.ledger-strip .ls-op {
  font-size: var(--fs-md); font-weight: 500; color: var(--text-muted);
  padding-bottom: 2px; user-select: none;
}
/* The answer, not another term: the operator before it separates, and the size
   step is what says which of the four you came to read. */
.ledger-strip .is-total { margin-left: auto; padding-left: var(--space-4); }
.ledger-strip .is-total .ls-value { font-size: var(--fs-lg); font-weight: 600; }
@media (max-width: 720px) {
  .ledger-strip { gap: var(--space-3); padding: var(--space-3) var(--space-4); }
  .ledger-strip .is-total { margin-left: 0; padding-left: 0; }
  .ledger-strip .is-total .ls-value { font-size: var(--fs-lg); }
}

/* ── Status chips (filter quick-select) ───────────────── */
.status-chips { display: flex; gap: .4rem; flex-wrap: wrap; }
/* A chip carries its phase's colour ALWAYS — as an outline — and fills in when
   it is the one selected. It used to be grey until clicked, so the row of
   filters said nothing about what it was filtering and the only coloured chip
   on screen was the one you had already chosen. --st comes from the phase
   palette above, so a chip is the same hue as the badges it selects.
   --st falls back to --border for a chip with no phase of its own. */
.status-chip {
    --st: var(--text-muted);
    display: inline-flex; align-items: center; gap: 4px;
    height: 28px; padding: 0 var(--space-3);
    border-radius: var(--radius-pill); font-size: var(--fs-xs); font-weight: var(--fw-medium);
    /* THE TEXT STAYS BLACK; the outline carries the colour (client, 2026-08-27:
       "for clean look the word color can stay as black"). Seven chips in seven
       coloured typefaces on one line is seven things asking to be read first,
       and the pale ones — amber on white — were the hardest to read of the lot.
       The same rule .cal-ep already follows: white fill, black text, the phase
       colour as the border and the dot. */
    border: 1.5px solid var(--st); color: var(--text); background: var(--surface);
    cursor: pointer; text-decoration: none;
    transition: background-color var(--transition), border-color var(--transition);
}
/* Except the count, which stays in the phase colour — it is the one part that
   is ABOUT the phase rather than naming it, and it is what the eye scans. */
.status-chip .chip-n { color: var(--st); opacity: 1; }
.status-chip:hover, .status-chip.active { text-decoration: none; }
/* The fill. color-mix keeps the tint tied to the one hue rather than repeating
   it as a second rgba() per phase — the drift this whole block was written to
   end. */
.status-chip.active { background: color-mix(in srgb, var(--st) 12%, transparent); }
.status-chip.chip-all { --st: var(--accent); }
/* Maintenance's scheduled / completed / rejected chips take their tones from
   the status system, like every other chip. */
/* The count rides inside the chip, so a chip is both the filter and how
   many it would leave — the OCR list reads the same way. */
.status-chip .chip-n { font-variant-numeric: tabular-nums; margin-left: .15rem; }

/* Chips first, search pushed to the end of the same line. It wraps on a
   narrow screen rather than scrolling: a filter row you have to scroll
   hides its own options. */
.trip-toolbar { flex-wrap: wrap; row-gap: .5rem; }
.trip-toolbar .status-chips { flex: 1 1 auto; }
.trip-toolbar .table-search { flex: 0 1 260px; margin-left: auto; }

/* ── Calendar window control (‹ July 2026 ▾ ›) ────────────────────────────
   The arrows flank the month they move. They used to sit at the far left of
   the toolbar, several controls away from the label that answered them, so
   "back one month" meant crossing the row to press a chevron and reading the
   result somewhere else. */
.cal-jump { display: flex; align-items: center; gap: .15rem; }
.cal-arrow {
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; border-radius: var(--radius-sm);
    color: var(--text-muted); text-decoration: none; flex: none;
}
.cal-arrow:hover { background: var(--card-hover); color: var(--text); text-decoration: none; }
.cal-arrow svg   { width: 15px; height: 15px; }

.cal-jump-wrap { position: relative; }
.cal-jump-btn {
    display: inline-flex; align-items: center; gap: .3rem;
    padding: 4px 8px; border: 1px solid transparent; border-radius: var(--radius-sm);
    background: none; font-family: inherit; font-size: var(--fs-sm); font-weight: 600;
    color: var(--text); cursor: pointer; white-space: nowrap;
}
.cal-jump-btn:hover { border-color: var(--border); background: var(--card-hover); }
.cal-jump-btn[aria-expanded="true"] { border-color: var(--accent); color: var(--accent); }
.cal-jump-btn svg { width: 13px; height: 13px; opacity: .55; }

/* Centred under the label rather than left-aligned to it: the label is the
   middle of the toolbar, and a panel hanging off one side of it reads as
   belonging to whatever control it covers. */
.cal-jump-panel {
    position: absolute; top: calc(100% + 6px); left: 50%; transform: translateX(-50%);
    z-index: 40; width: 232px; padding: .6rem;
    background: var(--card); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: var(--shadow);
}
.cal-jump-panel[hidden] { display: none; }
.cjp-year { display: flex; align-items: center; justify-content: space-between; margin-bottom: .5rem; }
.cjp-year span { font-size: var(--fs-sm); font-weight: 600; font-variant-numeric: tabular-nums; }
.cjp-step {
    width: 26px; height: 26px; border: none; border-radius: var(--radius-sm);
    background: none; color: var(--text-muted); font-size: var(--fs-md); line-height: 1;
    cursor: pointer; font-family: inherit;
}
.cjp-step:hover { background: var(--card-hover); color: var(--text); }
.cjp-months { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; }
.cjp-month {
    display: flex; align-items: center; justify-content: center;
    padding: 7px 0; border-radius: var(--radius-sm);
    font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); text-decoration: none;
}
.cjp-month:hover      { background: var(--card-hover); color: var(--text); text-decoration: none; }
.cjp-month.is-current { background: var(--accent); color: #fff; }

/* ── Grouped tree table (.ac-tree*) ───────────────────────────────────────
   Read by the AutoCount department console AND the truck fleet: a group row
   is a lorry type, its children are plates. Shared, not copied. */
/* ── Department tree-table ────────────────────────────────────────────────
   AutoCount's department hierarchy IS the fleet: parent = lorry type, child
   = plate. The table carries the nesting itself — a group row and its
   vehicle rows — rather than a tree pane beside a detail card repeating the
   same fields the columns already show. */
.ac-treetable td { vertical-align: middle; }
.ac-tcell { display: flex; align-items: center; gap: .5rem; min-width: 0; }
/* Children indent under their type, with a rail so the nesting survives
   scrolling past the group row. */
.ac-tcell.is-indented { padding-left: 30px; position: relative; }
.ac-tcell.is-indented::before {
    content: ''; position: absolute; left: 13px; top: -14px; bottom: -14px;
    border-left: 1px solid var(--border);
}

.ac-trow-toggle {
    width: 20px; height: 20px; flex-shrink: 0; padding: 0;
    border: none; background: none; border-radius: var(--radius-xs);
    display: grid; place-items: center; color: var(--text-dim);
    transition: background var(--transition), color var(--transition);
}
.ac-trow-toggle svg { width: 12px; height: 12px; transition: transform var(--transition); }
.ac-trow-toggle:hover { background: var(--accent-dim); color: var(--accent); }
.ac-trow-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ac-trow-toggle.is-spacer { pointer-events: none; }
.ac-trow.is-open .ac-trow-toggle svg { transform: rotate(90deg); }
/* Collapsed children are removed from flow, not faded — a half-hidden row
   in a table reads as a rendering fault. */
.ac-trow.is-child[hidden] { display: none; }

.ac-tcode { font-size: var(--fs-sm); color: var(--text); }
.ac-tcode.is-group { font-weight: 600; letter-spacing: .01em; }
.ac-trow.is-group > td { background: var(--bg); }
.ac-trow.is-archived td { opacity: .55; }

.ac-node-icon { width: 18px; height: 18px; flex-shrink: 0; display: grid; place-items: center; color: var(--rt-navy-500); }
.ac-node-icon svg { width: 16px; height: 16px; }
.ac-node-icon.is-truck { color: var(--text-dim); }

.ac-node-tally {
    font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted);
    background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-pill);
    padding: .1rem .42rem; flex-shrink: 0; font-variant-numeric: tabular-nums;
}

/* The tree's footer stands in for a pager: the table isn't paged (that would
   cut children from their parent), so this reports the shape instead. */
.ac-treefoot { display: flex; align-items: center; gap: 1.25rem; flex-wrap: wrap; }
.ac-treefoot-stat { font-size: var(--fs-xs); color: var(--text-muted); }
.ac-treefoot-stat strong { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }

@media (prefers-reduced-motion: reduce) {
    .ac-trow-toggle, .ac-trow-toggle svg { transition: none; } }

/* ── Two-step form (the truck wizard) ─────────────────────────────────────
   The dots are a position indicator, not navigation: you move with Next and
   Back, so they are aria-hidden and carry no click target. */
.wiz-steps { display: flex; gap: .5rem; margin-bottom: var(--field-gap); }
.wiz-step {
    flex: 1 1 0; position: relative; padding-top: .5rem;
    font-size: var(--fs-xs); font-weight: 600; color: var(--text-dim);
    border-top: 3px solid var(--border);
}
.wiz-step.is-on { color: var(--accent); border-top-color: var(--accent); }

/* ── AutoCount activity bell ──────────────────────────────────────────────
   The write log used to be a table at the foot of the Departments tab, so a
   write made from the Trucks page was recorded on a screen it had nothing to do
   with. This is where a person looks for "what just happened". */
.md-trigger { position: relative; }
.md-bell-badge {
    position: absolute; top: 2px; right: 2px; min-width: 15px; height: 15px;
    padding: 0 4px; border-radius: var(--radius-pill);
    background: var(--status-overdue, var(--danger)); color: #fff;
    font-size: var(--fs-xs); font-weight: 600; line-height: 15px; text-align: center;
}
.md-panel-log { width: 340px; max-height: 60vh; overflow-y: auto; }
.md-log { list-style: none; margin: 0; padding: 0; }
.md-log-item {
    display: flex; gap: .55rem; align-items: flex-start;
    padding: .55rem var(--card-pad); border-bottom: 1px solid var(--border);
}
.md-log-item:last-child { border-bottom: none; }
/* The dot carries the outcome, so the row needs no status word taking a column
   of its own in a 340px panel. */
.md-log-dot { width: 7px; height: 7px; border-radius: 50%; margin-top: 6px; flex: none; background: var(--text-dim); }
.md-log-item.is-done    .md-log-dot { background: var(--success); }
.md-log-item.is-error   .md-log-dot { background: var(--status-overdue, var(--danger)); }
.md-log-item.is-pending .md-log-dot { background: var(--warning); }
.md-log-body { min-width: 0; }
.md-log-line { font-size: var(--fs-xs); color: var(--text); display: flex; gap: .35rem; align-items: baseline; flex-wrap: wrap; }
.md-log-line code { font-size: var(--fs-xs); color: var(--text-muted); }
.md-log-meta { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 1px; }
.md-log-err  { font-size: var(--fs-xs); color: var(--status-overdue, var(--danger)); margin-top: 2px; word-break: break-word; }
/* Quiet: an empty log is not news, and a panel that shouts when it has
   nothing to say trains people to stop opening it. */
.md-log-empty { padding: 1.1rem var(--card-pad); font-size: var(--fs-xs); color: var(--text-dim); opacity: .6; }

/* ── Calendar summary dialog ──────────────────────────────────────────────
   A bar answers "what is this?" in place. Following a link to find out meant
   losing the month, the scope and both filters, and retracing them to get back. */
.cs-dot { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.modal-title .cs-dot { margin-right: .45rem; vertical-align: baseline; display: inline-block; }
.cs-what { font-size: var(--fs-sm); font-weight: 600; color: var(--text); margin: 0 0 .2rem; }
.cs-when { font-size: var(--fs-xs); color: var(--text-muted); margin: 0 0 var(--stack-gap); }

/* ── Bulk row selection ───────────────────────────────────────────────────
   The bar sits under the rows it acts on, inside the card, and only exists
   while something is ticked. Above the table it would be a permanent strip
   reading "0 selected", which is furniture. */
.bulk-cell { width: 34px; text-align: center; padding-right: 0 !important; }
.bulk-cell input[type="checkbox"] { width: 15px; height: 15px; margin: 0; accent-color: var(--accent); cursor: pointer; }
tr.is-selected > td { background: rgba(37,99,235,.05); }

.bulk-bar {
    display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
    padding: .55rem var(--card-pad);
    border-top: 1px solid var(--border);
    background: var(--card);
    position: sticky; bottom: 0; z-index: 2;
}
.bulk-bar[hidden] { display: none; }
/* Sitting INSIDE the pager row rather than being a bar of its own: the pager
   already supplies the line, the border and the background, so this drops all
   three and keeps only the controls. Used by the invoice list, where "3
   selected" and "1–50 of 120 invoices" belong on one line. */
.bulk-bar.is-inline {
    position: static;
    padding: 0;
    border-top: none;
    background: none;
    gap: .5rem;
}
.bulk-count {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: 3px 10px; border-radius: var(--radius-pill);
    background: rgba(37,99,235,.08); border: 1px solid rgba(37,99,235,.25);
    color: var(--accent); font-size: var(--fs-xs); font-weight: 600;
}
.bulk-count b { font-variant-numeric: tabular-nums; }
.bulk-clear {
    border: none; background: none; cursor: pointer; padding: 2px 4px;
    color: var(--text-dim); font-size: var(--fs-xs); line-height: 1;
}
.bulk-clear:hover { color: var(--text); }
.bulk-sep { width: 1px; height: 18px; background: var(--border); }
.bulk-bar .btn { height: var(--control-h); padding-block: 0; }

/* Editing one field straight from a list row.
   Deliberately NOT --control-h: that is the toolbar height, and 34px on every
   row of a nineteen-lorry table bulks the whole thing. A row control is sized
   to the row. The form wrapper carries no margin so the cell keeps the table's
   own rhythm rather than growing a gap the other cells do not have. */
.inline-pair { margin: 0; display: block; }
.inline-select {
  width: 100%;
  height: var(--control-h-sm);
  padding: 0 26px 0 8px;
  font-size: var(--fs-xs);
  line-height: calc(var(--control-h-sm) - 2px);
}
.inline-select + .ui-select-trigger,
.ui-select-trigger.is-inline { height: var(--control-h-sm); font-size: var(--fs-xs); }


/* ── One height for everything on a toolbar line ──────────────────────────
   Scoped to the toolbars rather than set on .btn-sm, because the same class
   is a row action inside a table, where 34px would bulk every row. A toolbar
   is where things sit side by side and have to line up. */
.table-toolbar .btn,
.table-toolbar select,
.table-toolbar .view-toggle,
.page-head-actions .btn,
.page-head-actions .view-toggle,
.cal-toolbar .btn,
.cal-toolbar select,
.cal-toolbar .view-toggle,
.cal-toolbar .cal-expand,
.cal-nav .btn,
.cal-nav select,
.cal-nav .view-toggle,
.cal-nav .cal-expand {
    height: var(--control-h);
}
/* The toggle is the parent; its links have to fill it or the box is taller
   than the thing you click. */
.table-toolbar .view-toggle a,
.page-head-actions .view-toggle a,
.cal-toolbar .view-toggle a,
.cal-nav .view-toggle a { height: 100%; padding-block: 0; }
/* Height replaces vertical padding — keeping both makes the label sit off
   centre in whichever control has the larger font. */
.table-toolbar .btn, .page-head-actions .btn,
.cal-toolbar .btn, .cal-nav .btn { padding-block: 0; }

/* Assign Truck / Outsourced lorry: the switch on the left, and ONE slot on the
   right that shows either the fleet select or a typed lorry number (client,
   2026-09-25, from a sketch). flex-end so the switch lines up with the CONTROL,
   not the caption above it; wraps under a phone-width column so neither half is
   squeezed. .arr-tick is a form-field tall here on purpose — it has to sit level
   with a 38px select, and its own padding lands it a few px off. */
.assign-row { display: flex; align-items: flex-end; gap: var(--field-gap, 16px); flex-wrap: wrap; }
.assign-row .arr-tick { flex: none; height: 38px; padding-block: 0; white-space: nowrap; }
.assign-field { flex: 1 1 220px; min-width: 0; }
.assign-field[hidden] { display: none; }

/* "+ New rate", under a line's variant select. A link in a button's clothes: it is
   a suggestion, not an action the row needs, so it must not compete with the
   line's own controls. [hidden] is toggled by rtChargeProduct(). */
.charge-newrate {
    display: inline-block; margin-top: 3px; padding: 0; border: 0; background: none;
    color: var(--accent); font: inherit; font-size: var(--fs-xs); font-weight: 600; cursor: pointer;
}
.charge-newrate:hover { text-decoration: underline; }
.charge-newrate[hidden] { display: none; }

/* THE TWO THAT ESCAPED, and the first is the .view-toggle trap again: the rule
   above sizes a bare `select`, and controls.js has since moved that select
   inside a .ui-select wrapper at opacity 0 — so it was sizing the one control
   on the row nobody can see. What a person looks at is .ui-select-trigger,
   which carries a FORM field's 38px min-height: right in a form, 4px too tall
   in a row of 34px buttons. Measured on the Schedule, where "All trips" and
   "All maintenance" stood visibly proud of Today / Week / Month beside them.

   .cal-jump-btn is the other: padding-only, so it came out at 29px — a 9px
   spread across one row. min-height as well as height, or the 38px wins. */
.table-toolbar .ui-select-trigger,
.page-head-actions .ui-select-trigger,
.cal-toolbar .ui-select-trigger,
.cal-nav .ui-select-trigger,
.table-toolbar .cal-jump-btn,
.page-head-actions .cal-jump-btn,
.cal-toolbar .cal-jump-btn,
.cal-nav .cal-jump-btn {
    height: var(--control-h);
    min-height: var(--control-h);
    padding-block: 0;
}
.table-toolbar select, .cal-toolbar select, .cal-nav select { padding-block: 0; }
.table-count { font-size: var(--fs-xs); color: var(--text-muted); margin-left: auto; }

/* ── Trip classification fields ───────────────────────────────────────────── */
.field-hint { display: block; margin-top: .3rem; font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.35; }

.check-row { display: flex; gap: 1.25rem; flex-wrap: wrap; }
.check-inline {
    display: inline-flex; align-items: flex-start; gap: .5rem;
    font-size: var(--fs-sm); font-weight: 600; color: var(--text); cursor: pointer;
}
.check-inline input { margin: 2px 0 0; flex: 0 0 auto; }
.check-inline small { display: block; font-weight: 400; font-size: var(--fs-xs); color: var(--text-muted); }

/* The sentence this trip will print as on the exported schedule. Their whole
   operation reads as those lines, so "will this look right on next month's
   sheet" is a more useful question while typing than "are six fields set". */
.sentence-preview {
    display: flex; flex-direction: column; gap: .3rem;
    padding: .7rem .85rem; border-radius: var(--radius-sm);
    background: var(--card-hover); border: 1px dashed var(--border);
}
.sentence-label {
    font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted);
}
.sentence-preview code {
    font-family: var(--font-mono);
    font-size: var(--fs-xs); line-height: 1.5; color: var(--text); word-break: break-word;
}

/* A leave application whose dates already have trips assigned to that driver.
   Approving it is still allowed — the office may intend to reassign — but they
   should not have to cross-check the trip list to find out. */
.leave-clash {
    display: inline-flex; align-items: center; gap: .3rem;
    margin-top: .25rem; padding: 2px 7px; border-radius: var(--radius-pill);
    background: rgba(245,158,11,.12); border: 1px solid rgba(245,158,11,.32);
    color: var(--warning-text); font-size: var(--fs-xs); font-weight: 600; white-space: nowrap; cursor: help;
}
.leave-clash svg { width: 11px; height: 11px; stroke: currentColor; fill: none; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }

/* Leave: the running total and balance while applying. Server-side
   rtLeaveBalanceError() is the control; this only means they find out before
   submitting rather than after. */
.leave-calc {
    display: flex; align-items: center; justify-content: space-between; gap: .75rem;
    flex-wrap: wrap; margin: -.25rem 0 1rem;
    padding: .6rem .8rem; border-radius: var(--radius-sm);
    background: var(--card-hover); border: 1px dashed var(--border);
    font-size: var(--fs-xs); color: var(--text);
}
.leave-calc[hidden]   { display: none; }
.leave-calc strong    { font-size: var(--fs-sm); }
.leave-calc-bal       { font-size: var(--fs-xs); color: var(--text-muted); font-weight: 600; }
.leave-calc-bal.is-over { color: var(--danger); }

/* Who approved a leave application, under its status pill. */
.decided-by { margin-top: .2rem; font-size: var(--fs-xs); color: var(--text-muted); white-space: nowrap; }

/* ── Leave balance cards (driver's own view) ──────────────────────────────
   A driver previously had nowhere to see their balance — they could apply and
   be refused for a cap they had never been shown. One card per type that has a
   cap; a type with no cap is simply absent rather than showing "0 of 0". */
.lv-balance {
    display: grid; gap: .75rem; margin-bottom: 1rem;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
}
.lv-card {
    position: relative; overflow: hidden;
    padding: .9rem 1rem 1rem; border-radius: var(--radius);
    background: var(--card); border: 1px solid var(--border);
    box-shadow: inset 3px 0 0 0 var(--lv, var(--accent));
}
.lv-card-code {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 34px; height: 24px; padding: 0 .45rem; border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--lv) 14%, transparent);
    color: var(--lv); font-size: var(--fs-xs); font-weight: 600; letter-spacing: .04em;
}
.lv-card-name { margin-top: .45rem; font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); }
.lv-bar       { margin: .6rem 0 .55rem; height: 5px; border-radius: var(--radius-pill); background: var(--border); overflow: hidden; }
.lv-bar span  { display: block; height: 100%; border-radius: var(--radius-pill); background: var(--lv); }
.lv-card-nums { display: flex; gap: 1.1rem; margin: 0; }
.lv-card-nums div { display: flex; flex-direction: column; gap: .1rem; }
.lv-card-nums dt  { font-size: var(--fs-xs); color: var(--text-muted); }
.lv-card-nums dd  { margin: 0; font-size: var(--fs-md); font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }


.lv-counts { display: flex; gap: .6rem; flex-wrap: wrap; margin-bottom: 1rem; }
.lv-count {
    display: flex; align-items: center; gap: .5rem;
    padding: .45rem .8rem; border-radius: var(--radius-pill);
    background: var(--card); border: 1px solid var(--border);
}
.lv-count strong { font-size: var(--fs-md); font-variant-numeric: tabular-nums; }

@media (max-width: 480px) {
    .lv-balance { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
    .lv-card-nums dd { font-size: var(--fs-md); }
}

/* Link to a leave attachment. Never a direct file URL — payroll/leave-doc.php
   checks the viewer is the applicant or an approver before opening it. */
.lv-doc { display: inline-flex; align-items: center; gap: .3rem; margin-top: .25rem;
          font-size: var(--fs-xs); font-weight: 600; color: var(--accent); text-decoration: none; }
.lv-doc:hover { text-decoration: underline; }
.lv-doc svg { width: 11px; height: 11px; stroke: currentColor; fill: none; stroke-width: 2; }

/* ── Leave application review ─────────────────────────────────────────────
   The approver sees what they are deciding on — reason, attachment and the
   trips it would strand — instead of saying yes to a line in a table. */
.lvv-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; }
.lvv-type   { font-size: var(--fs-md); font-weight: 600; color: var(--text); }
.lvv-driver { font-size: var(--fs-xs); color: var(--text-muted); margin-top: .1rem; }

.lvv-grid {
    display: grid; grid-template-columns: repeat(4, 1fr); gap: .75rem 1rem; margin: 0 0 1rem;
    padding: .85rem 1rem; border-radius: var(--radius-sm);
    background: var(--card-hover); border: 1px solid var(--border);
}
.lvv-grid div { min-width: 0; }
.lvv-grid dt  { font-size: var(--fs-xs); color: var(--text-muted); }
.lvv-grid dd  { margin: .15rem 0 0; font-size: var(--fs-sm); font-weight: 600; color: var(--text); }
@media (max-width: 560px) { .lvv-grid { grid-template-columns: repeat(2, 1fr); } }

.lvv-block { margin-bottom: 1rem; }
.lvv-label { font-size: var(--fs-xs); font-weight: 600;
             color: var(--text-muted); margin-bottom: .35rem; }
.lvv-reason { margin: 0; font-size: var(--fs-sm); line-height: 1.55; color: var(--text); white-space: pre-wrap; }
.lvv-decided { margin: 0; font-size: var(--fs-sm); line-height: 1.5; color: var(--text); }
.lvv-clash-list { margin-top: .4rem; font-size: var(--fs-xs); line-height: 1.6; font-variant-numeric: tabular-nums; }

/* ── File input ───────────────────────────────────────────────────────────
   The native control renders an OS button whose text and shape can't be styled
   and matches nothing else on the form. The real <input> is kept and covers the
   field, so it still submits and still works with no JavaScript; .file-ui is
   what you actually see. */
.file-field { position: relative; display: block; }
.file-field input[type="file"] {
    position: absolute; inset: 0; width: 100%; height: 100%;
    opacity: 0; cursor: pointer; z-index: 1;
}
.file-ui {
    display: flex; align-items: center; gap: .6rem;
    padding: .55rem .6rem; border-radius: var(--radius-sm);
    border: 1.5px dashed var(--border); background: var(--card);
    transition: border-color .15s, background-color .15s;
}
.file-field:hover .file-ui,
.file-field.is-over .file-ui { border-color: var(--accent); background: var(--accent-dim); }
.file-field.has-file .file-ui { border-style: solid; border-color: var(--border); }

.file-btn {
    flex: 0 0 auto; display: inline-flex; align-items: center; gap: .35rem;
    padding: .35rem .7rem; border-radius: var(--radius-sm);
    background: var(--surface); border: 1px solid var(--control-border);
    font-size: var(--fs-xs); font-weight: 600; color: var(--text); white-space: nowrap;
}
.file-btn svg { width: 12px; height: 12px; }
.file-name {
    flex: 1 1 auto; min-width: 0; font-size: var(--fs-xs); color: var(--text-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.file-field.has-file .file-name { color: var(--text); font-weight: 600; }
.file-clear {
    flex: 0 0 auto; position: relative; z-index: 2;   /* above the input overlay */
    width: 22px; height: 22px; border-radius: 50%; border: none;
    background: var(--card-hover); color: var(--text-muted);
    font-size: var(--fs-md); line-height: 1; cursor: pointer;
}
.file-clear:hover { background: rgba(239,68,68,.12); color: var(--danger); }

/* ── Arrival confirmation: what happened at this stop ─────────────────────
   Asked as the driver arrives, at the place it happened.

   Real toggles, not checkboxes: the native input is taken out of the flow and
   the pill itself is the control, with a dot that slides and fills when it is
   on. A checkbox reads as "one of a list to work down"; this reads as "is this
   on or off", which is the question being asked at a loading bay. */
.arr-costs {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(11.5rem, 1fr));
    gap: .5rem; margin-bottom: .8rem;
}
.arr-tick {
    display: flex; align-items: center; gap: .55rem; cursor: pointer; margin: 0;
    padding: .6rem .8rem; border: 1.5px solid var(--border); border-radius: var(--radius);
    font-size: var(--fs-sm); background: var(--card); user-select: none;
    transition: border-color .15s, background-color .15s, box-shadow .15s;
}
.arr-tick:hover { border-color: var(--accent); }
/* Off-screen rather than display:none — a hidden input keeps keyboard focus and
   still announces its state to a screen reader. */
.arr-tick input {
    position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; pointer-events: none;
}
/* A toggle's own text is not a field caption. The bare `label` rule at the top
   of this file is written for one — block, .76rem, muted, UPPERCASE, tracked —
   and .arr-tick is a <label>, so every tick in the app has been rendering
   "BONDED GOODS" / "LABOUR FEE" in tracked caps beside sentence-case fields.
   Third instance of the same inheritance trap, after .act-split-opt and
   .inv-einv-tick. */
.arr-lbl { text-transform: none; letter-spacing: normal; }

.arr-dot {
    flex: none; width: 34px; height: 19px; border-radius: var(--radius-pill);
    background: var(--border); position: relative; transition: background-color .18s;
}
.arr-dot::after {
    content: ""; position: absolute; top: 2px; left: 2px; width: 15px; height: 15px;
    border-radius: 50%; background: #fff; transition: transform .18s;
    box-shadow: var(--shadow-sm);
}
/* THE STATE COMES FROM THE CHECKBOX, not only from a class.
   .is-on is toggled by page-specific JS (tasks/detail.php has the handler), so
   this control was inert anywhere that JS did not run — the real input flipped
   underneath and nothing on screen moved, because the input itself is visually
   hidden. It read exactly like a toggle that cannot be clicked. Reusing the
   markup on the trip form is what surfaced it.

   :has() makes the checked state the source of truth and the class an optional
   extra, so the toggle now works with no script at all. Both selectors are kept:
   detail.php sets .is-on optimistically before its request comes back. */
.arr-tick.is-on,
.arr-tick:has(input:checked) { border-color: var(--accent); background: var(--accent-dim); }
.arr-tick.is-on .arr-lbl,
.arr-tick:has(input:checked) .arr-lbl { font-weight: 600; }
.arr-tick.is-on .arr-dot,
.arr-tick:has(input:checked) .arr-dot { background: var(--accent); }
.arr-tick.is-on .arr-dot::after,
.arr-tick:has(input:checked) .arr-dot::after { transform: translateX(15px); }
.arr-tick input:focus-visible + .arr-dot { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The full-size tick above is sized for a driver's thumb at a loading bay.
   Beside a .btn-sm in the Trip costs header, that read as a different,
   larger control next to a small button rather than a matched pair — sized
   down to the same ~28px line height instead of shrinking .arr-tick itself,
   which would have shrunk the arrival ticks too. */
.arr-tick-sm { padding: 4px 10px; gap: .4rem; border-width: 1px; border-radius: var(--radius-sm); font-size: var(--fs-xs); }
.arr-tick-sm .arr-dot { width: 26px; height: 14px; }
.arr-tick-sm .arr-dot::after { width: 10px; height: 10px; }
.arr-tick-sm.is-on .arr-dot::after,
.arr-tick-sm:has(input:checked) .arr-dot::after { transform: translateX(12px); }

/* Closed until its toggle opens it. `hidden` alone lost to the display rule
   that used to live here, so the waiting times showed on every arrival whatever
   was ticked — the class is what decides now. */
.arr-reveal { display: none; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
.arr-reveal.is-open { display: flex; }
.arr-reveal-lbl {
    font-size: var(--fs-xs); font-weight: 600;
    color: var(--text-muted); min-width: 7.5rem;
}
.arr-reveal input[type="time"] { width: 7.5rem; }
.arr-reveal input[type="file"] { max-width: 14rem; font-size: var(--fs-xs); }
.arr-dash { color: var(--text-muted); }
.arr-hint { font-size: var(--fs-xs); color: var(--text-muted); }

.cost-add {
    display: flex; align-items: center; gap: .45rem; flex-wrap: wrap;
    padding: var(--space-3, 12px) var(--card-pad, 24px); border-top: 1px solid var(--border);
}
.cost-add select, .cost-add input[type="text"] { min-width: 8rem; }
.cost-add input[type="text"] { flex: 1 1 8rem; }

/* ── Trip costs panel ─────────────────────────────────────────────────────
   One row per expense type, ticked or not. The client asked for these as
   checkboxes and their paper trip report has one box per column per row, so
   the screen and the document agree by construction.

   An unticked row keeps its fields in the DOM but dims them — they are what
   you are about to fill in, and hiding them makes the panel jump as you tick
   down the list. They stay focusable, so a keyboard user can tab straight to
   an amount and the tick follows. */
.cost-row {
    display: grid; grid-template-columns: 12.5rem 1fr; gap: var(--space-3, 12px);
    align-items: center; padding: .55rem var(--card-pad, 24px);
    border-bottom: 1px solid var(--rule, var(--border));
}
.cost-row:last-of-type { border-bottom: 0; }
.cost-tick { display: flex; align-items: center; gap: .5rem; cursor: pointer; margin: 0; }
.cost-tick input { width: 16px; height: 16px; flex: none; cursor: pointer; }
.cost-name { font-size: var(--fs-sm); font-weight: 600; }
.cost-fields {
    display: flex; flex-wrap: wrap; align-items: center; gap: .45rem;
    opacity: .42; transition: opacity .15s;
}
.cost-row.is-on .cost-fields { opacity: 1; }
.cost-amt { display: flex; align-items: center; gap: .35rem; }
.cost-cur { font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); min-width: 1.9rem; }
.cost-amt input { width: 7.5rem; text-align: right; font-variant-numeric: tabular-nums; }
.cost-wait { display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
.cost-wait input[type="time"] { width: 7rem; }
.cost-wait select { width: 11rem; }
.cost-dash { color: var(--text-muted); }
.cost-note { flex: 1 1 9rem; min-width: 7rem; }
.cost-doc { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.cost-doc input[type="file"] { max-width: 14rem; font-size: var(--fs-xs); }
.cost-req, .cost-hint { font-size: var(--fs-xs); color: var(--text-muted); }
.cost-haddoc { font-size: var(--fs-xs); }
.cost-foot {
    display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
    gap: var(--space-3, 12px); padding: var(--space-3, 12px) var(--card-pad, 24px);
    border-top: 1px solid var(--border);
}
.cost-sum { font-size: var(--fs-xs); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.cost-sum strong { color: var(--text); }

/* Waiting is ticked at the stop it happened at, so it lives in the Route table
   beside that stop's proof — the times only appear once the box is ticked,
   because an untouched route of six drops should read as six addresses, not as
   twelve empty clock fields. */
.wait-cell { display: flex; flex-direction: column; gap: .3rem; align-items: flex-start; }
.wait-tick { display: flex; align-items: center; gap: .4rem; font-size: var(--fs-xs); cursor: pointer; margin: 0; }
.wait-tick input { width: 15px; height: 15px; cursor: pointer; }
.wait-times, .wait-amt { display: none; align-items: center; gap: .3rem; }
.wait-cell.is-on .wait-times,
.wait-cell.is-on .wait-amt { display: flex; }

/* TWO TIMES AND A TOTAL ON ONE LINE, inside a table column that also has to
   carry six other columns. At the body font size the field was too narrow for
   its own value — "01:49 AM" rendered as "01:49 A", because 36px of that 6.4rem
   is the clock icon's inset — and the "1 h" total behind it wrapped to two lines
   reading "1" over "h", which is a number split from its unit.

   Smaller type and a tighter icon inset buy the value the room it needs without
   widening the column past what the Route table can give it. nowrap is the half
   that matters: a duration is one token, and a column can always be narrower
   than somebody's screen. */
.wait-times { white-space: nowrap; }
.wait-times input[type="time"] {
    width: 6.5rem;
    font-size: var(--fs-xs);
    padding-left: 27px;
    background-position: left 8px center;
    background-size: 13px;
}
.wait-times .cost-hint { white-space: nowrap; }
.wait-amt input { width: 6.4rem; text-align: right; font-variant-numeric: tabular-nums; }
@media (max-width: 780px) {
    .cost-row { grid-template-columns: 1fr; gap: .4rem; }
    .cost-fields { opacity: 1; }
}

/* ── Trip money queue (Payroll → Trip costs) ──────────────────────────────
   One block per trip: what it cost, what came off the driver's float, what was
   claimed against it. Grouped by trip rather than by kind, because "which trip
   did this claim come from" is the question being asked. */
.tm-trip { padding: 0; overflow: hidden; margin-bottom: var(--stack-gap); }
.tm-head { flex-wrap: wrap; gap: .5rem var(--space-3, 12px); align-items: baseline; }
.tm-route { font-size: var(--fs-sm); font-weight: 600; margin-left: .5rem; }
.tm-who   { font-size: var(--fs-xs); color: var(--text-muted); }
.tm-sec   { border-top: 1px solid var(--border); }
.tm-sec-t {
    margin: 0; padding: .55rem var(--card-pad, 24px) .1rem;
    font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted);
}
/* The section heading already says what the columns are, so the rows carry no
   header of their own — three tables of two words each would be more chrome
   than content. */
.tm-table td { border-bottom: 0; padding-top: .4rem; padding-bottom: .4rem; }
.tm-table tr + tr td { border-top: 1px solid var(--rule-2, var(--border)); }
.tm-foot {
    display: flex; align-items: center; justify-content: space-between;
    flex-wrap: wrap; gap: var(--space-3, 12px); padding-top: var(--space-3, 12px);
}

/* ── Upload drop zone ─────────────────────────────────────────────────────
   Click-or-drag panel for the two places a file is attached to a trip: the
   expense supporting document and the proof of delivery.

   It lived inline in tasks/detail.php, styling one hardcoded id, so the POD
   upload beside it fell back to a bare <input type="file"> and the same job
   looked like two different controls on one page. Here it belongs to any
   .upload-drop, which is what let the second one adopt it. Same reason
   .tl-* moved out of tasks/index.php: a style that only exists on the page
   that first needed it renders as nothing the day a second page draws it. */
.upload-drop {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: .2rem; padding: 1.4rem 1rem; width: 100%;
    border: 2px dashed var(--border); border-radius: var(--radius);
    background: var(--bg); cursor: pointer; text-align: center;
    transition: border-color .18s, background .18s;
}
.upload-drop:hover,
.upload-drop.drag-over { border-color: var(--accent); background: rgba(37,99,235,.04); }
.upload-drop.has-file  { border-color: var(--success); background: #f0fdf4; }
.upload-drop.has-file svg { color: var(--success); }
.upload-drop.has-file .upload-drop-label { color: var(--success-text); font-weight: 600; }
.upload-drop-label { font-size: var(--fs-sm); color: var(--text-muted); margin-top: .35rem; }
.upload-drop-hint  { font-size: var(--fs-xs); color: var(--text-dim); }

/* Leave-balance card variants: a type with no cap has nothing to remain, so it
   reports what has been TAKEN rather than inventing a remainder. */
.lv-card-top  { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
.lv-card-tag  { font-size: var(--fs-xs); font-weight: 600;
                color: var(--text-muted); border: 1px solid var(--border); border-radius: var(--radius-pill); padding: 1px 6px; }
.lv-card.is-uncapped { box-shadow: inset 3px 0 0 0 var(--border); }
.lv-card.is-uncapped .lv-card-code { background: var(--card-hover); color: var(--text-muted); }
.lv-bar.is-flat span { width: 100%; background: var(--border); }
.lv-card-nums dd.lv-none { font-size: var(--fs-sm); font-weight: 600; color: var(--text-muted); }

.leave-calc-days { display: inline-flex; align-items: center; gap: .45rem; }
.leave-calc-days svg { width: 14px; height: 14px; stroke: var(--text-muted); flex-shrink: 0; }

/* The number inside the running total, now that it is one text node. */
.leave-calc-days #lvDaysText { font-weight: 400; }
.leave-calc-days #lvDaysText::first-letter { font-weight: 600; }

/* One line in place of the 24-cell permission matrix that used to sit on every
   user's page. It answered a question about the role, not the person. */
.role-summary {
    margin: 0 0 1.25rem; padding: .7rem .95rem;
    border-radius: var(--radius-sm);
    background: var(--card-hover); border: 1px solid var(--border);
    font-size: var(--fs-xs); line-height: 1.55; color: var(--text-muted);
}
.role-summary strong { color: var(--text); }

/* ── Master Data menu (header) ─────────────────────────────────────────────
   The six AutoCount lists, one click from any page. A grid of tiles rather
   than a text menu: the six are siblings with no order worth reading, and a
   tile carries the same mark and hue the AutoCount console uses for that
   entity — so what you clicked here is recognisable when you land there. */
.md-menu { position: relative; }

.md-trigger {
  display: flex; align-items: center; justify-content: center;
  /* Top-bar icons are the one place a button carries no edge at rest: the bar
     is their frame and the glyph is the affordance, as in every app bar. They
     share the toolbar's height and get a visible hover and focus. */
  width: var(--control-h); height: var(--control-h); padding: 0;
  background: none; border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--text-muted); cursor: pointer;
  transition: background var(--transition), color var(--transition), border-color var(--transition);
}
.md-trigger svg { width: 18px; height: 18px; }
.md-trigger:hover { background: var(--accent-dim); color: var(--accent); }
.md-menu.open .md-trigger { background: var(--accent-dim); color: var(--accent); border-color: var(--accent); }

.md-panel {
  position: absolute; top: calc(100% + 10px); right: 0; z-index: 60;
  width: 320px; padding: 14px;
  background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
.md-panel[hidden] { display: none; }

.md-panel-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: .6rem;
  padding-bottom: 12px; margin-bottom: 12px; border-bottom: 1px solid var(--border);
}
.md-panel-title { font-size: var(--fs-sm); font-weight: 600; color: var(--text); }
.md-panel-sub {
  font-size: var(--fs-xs); font-weight: 600;
  color: var(--text-dim); margin-top: 2px;
}
.md-chip {
  display: inline-flex; align-items: center; gap: .35rem; flex-shrink: 0;
  font-size: var(--fs-xs); font-weight: 600; color: var(--accent);
  background: var(--accent-dim); border: 1px solid var(--border);
  border-radius: var(--radius-pill); padding: .15rem .5rem;
}
.md-chip-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); }

.md-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }

.md-tile { position: relative; border-radius: var(--radius); transition: background var(--transition); }
.md-tile:hover { background: var(--bg); }

.md-tile-link {
  display: flex; flex-direction: column; align-items: center; gap: .4rem;
  padding: 10px 4px; border-radius: var(--radius);
  color: var(--text-muted); text-decoration: none;
}
/* The tile is a target, not prose — the global link underline reads as a
   defect at this size and cuts through the label's descenders. */
.md-tile-link:hover, .md-tile-link:focus { text-decoration: none; }

/* Sync sits ON the tile, revealed on hover: the tile's own job is "open this
   list", and a second permanent button per tile would make six of them compete
   with the one that syncs all six.

   LEFT, because the right corner belongs to .md-tile-state — the trigger and
   the result of pressing it cannot share a corner. */
.md-tile-sync {
  position: absolute; top: 3px; left: 3px;
  width: 22px; height: 22px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; cursor: pointer;
  background: var(--card); border: 1px solid var(--border); color: var(--text-muted);
  box-shadow: var(--shadow-sm);
  opacity: 0; transform: scale(.86);
  transition: opacity var(--transition), transform var(--transition),
              color var(--transition), border-color var(--transition);
}
.md-tile-sync svg { width: 11px; height: 11px; }
.md-tile:hover .md-tile-sync,
.md-tile-sync:focus-visible { opacity: 1; transform: scale(1); }
.md-tile-sync:hover { color: var(--md-hue); border-color: var(--md-hue); }

/* Touch has no hover, so the control would never be reachable there. */
@media (hover: none) {
  .md-tile-sync { opacity: 1; transform: none; }
}

/* While one list is pulling, its own tile spins and the rest stop offering. */
.md-tile.is-working .md-tile-sync { opacity: 1; transform: none; color: var(--md-hue); }
.md-tile.is-working .md-tile-sync svg { animation: mdSpin .85s linear infinite; }
.md-menu.is-working .md-tile:not(.is-working) .md-tile-sync { opacity: 0; pointer-events: none; }
/* ── Did that sync? ─────────────────────────────────────────────────────────
   A tick or a cross in the tile's right corner, ALWAYS visible (no hover), so
   the answer is on screen without pressing anything again. Server-rendered
   from rt_ac_sync_state and set live by header.php when a run finishes; it
   expires with RT_AC_SYNC_RECENT, because a badge that never goes away would
   claim a mirror pulled last Tuesday is fresh.

   No [data-state] means nothing has run recently — the badge is not drawn at
   all rather than drawn neutral, so a quiet tile stays quiet. */
.md-tile-state {
  position: absolute; top: 3px; right: 3px;
  width: 18px; height: 18px;
  display: none; align-items: center; justify-content: center;
  border-radius: 50%;
  background: var(--card); border: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
}
.md-tile-state[data-state] { display: flex; }
.md-tile-state svg { width: 10px; height: 10px; display: none; }
.md-tile-state[data-state="OK"]    { color: var(--status-completed); border-color: rgba(22, 163, 74, .35); }
.md-tile-state[data-state="ERROR"] { color: var(--status-overdue);   border-color: rgba(220, 38, 38, .35); }
.md-tile-state[data-state="BUSY"]  { color: var(--accent);           border-color: rgba(37, 99, 235, .35); }
.md-tile-state[data-state="OK"]    .md-state-ok   { display: block; }
.md-tile-state[data-state="ERROR"] .md-state-err  { display: block; }
.md-tile-state[data-state="BUSY"]  .md-state-busy { display: block; animation: mdSpin .85s linear infinite; }

.md-tile.is-failed .md-tile-icon { box-shadow: inset 0 0 0 1.5px var(--status-overdue); }
.md-tile-icon {
  display: flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: var(--radius);
  background: var(--md-tint); color: var(--md-hue);
  transition: box-shadow var(--transition);
}
.md-tile-icon svg { width: 19px; height: 19px; }
.md-tile-label {
  font-size: var(--fs-xs); font-weight: 600; line-height: 1.25;
  color: var(--text); text-align: center;
}

.md-note { font-size: var(--fs-xs); color: var(--text-dim); margin: 12px 0 0; text-align: center; }
.md-panel-foot { margin-top: 10px; }
.md-panel-foot .btn { width: 100%; justify-content: center; }
.md-menu.is-working .md-sync svg { animation: mdSpin .85s linear infinite; }
@keyframes mdSpin { to { transform: rotate(360deg); } }

@media (max-width: 480px) {
  .md-panel { width: calc(100vw - 24px); right: -8px; }
}
@media (prefers-reduced-motion: reduce) {
  .md-menu.is-working .md-sync svg { animation: none; }
  .md-tile.is-working .md-tile-sync svg { animation: none; }
  .md-tile-state[data-state="BUSY"] .md-state-busy { animation: none; }
}

.topbar-right { margin-left: auto; }

/* ── App shell: grouped nav, identity at the foot ──────────────────────────
   The sidebar answers two questions that used to be answered in two places:
   where can I go, and who am I signed in as. Putting the second at the bottom
   of the first is the convention every operator already knows, and it frees
   the top bar to be a top bar. */

/* The nav, its groups, the identity block and the top bar's mark are all
   in the Sidebar and Topbar sections at the top of this file. */

/* Writes need a token this app cannot fetch itself, so its absence is stated
   where the button would have been — a missing control with no explanation
   reads as a bug in the page rather than a machine being off. */
.ac-write-off {
  display: inline-flex; align-items: center; gap: .35rem;
  font-size: var(--fs-xs); font-weight: 600;
  color: var(--status-pending); background: rgba(217,119,6,.09);
  border: 1px solid rgba(217,119,6,.22); border-radius: var(--radius-pill); padding: .25rem .65rem;
}
.ac-write-off svg { width: 13px; height: 13px; }

/* The Master Data panel's title is a link now — the AutoCount console has more
   in it than the six tiles, and this is the only way there. */
a.md-panel-title { color: var(--text); text-decoration: none; display: inline-flex; align-items: center; gap: .3rem; }
a.md-panel-title::after { content: "›"; color: var(--text-dim); font-size: var(--fs-md); line-height: 1; }
a.md-panel-title:hover { color: var(--accent); text-decoration: none; }
a.md-panel-title:hover::after { color: var(--accent); }

/* "Whose lorry is this?" — two cards rather than a select, because the answer
   decides whether a cost centre appears in the client's chart of accounts, and
   that consequence belongs next to the choice rather than in a tooltip. */
.own-choice { display: grid; gap: 8px; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
.own-choice label {
  display: flex; align-items: flex-start; gap: .6rem; cursor: pointer;
  padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--card); transition: border-color var(--transition), background var(--transition);
}
.own-choice label:hover { border-color: var(--accent); background: var(--accent-dim); }
.own-choice input { width: auto; margin: 3px 0 0; flex-shrink: 0; }
.own-choice label:has(input:checked) { border-color: var(--accent); background: var(--accent-dim); }
.own-choice label:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.own-choice strong { display: block; font-size: var(--fs-sm); font-weight: 600; color: var(--text); }
.own-choice small  { display: block; font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.45; margin-top: 2px; }

/* A backdrop click on a half-filled form is ignored rather than obeyed — this
   says so, instead of leaving the click looking broken. */
@keyframes modalNudge {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-5px); }
  75%      { transform: translateX(5px); }
}
.modal-nudge { animation: modalNudge .18s ease-in-out 1; }
@media (prefers-reduced-motion: reduce) { .modal-nudge { animation: none; } }

/* An empty state that teaches rather than shrugs. Measure held near prose width
   so it reads as an explanation, not as a dialog. */
.cfg-empty { max-width: 62ch; padding: var(--space-4) 0; }
.cfg-empty h3 {
  font-size: var(--fs-md);
  font-weight: 600;
  margin-bottom: var(--space-3);
}
.cfg-empty p {
  color: var(--text-muted);
  font-size: var(--fs-sm);
  line-height: 1.65;
  margin-bottom: var(--field-gap);
}
.cfg-ready {
  list-style: none;
  padding: var(--space-4);
  margin: 0 0 var(--field-gap);
  background: var(--bg);
  border-radius: var(--radius);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-8);
}
.cfg-ready li { font-size: var(--fs-xs); color: var(--text-muted); }
.cfg-ready strong { color: var(--text); font-size: var(--fs-md); margin-right: 4px; }

/* A reveal that carries a drop zone stacks instead of sitting on one line —
   the panel is 100% wide by design and would otherwise squeeze its label. */
.arr-reveal-col { flex-direction: column; align-items: stretch; gap: .35rem; }
.arr-reveal-col .arr-reveal-lbl { min-width: 0; }

/* ── Priced-by-the-point boxes (Payroll → Trip costs dialog) ──────────────
   A titled box per place, because that is how the driver reported them: they
   ticked at a stop, and an approver reads them the same way. The title bar
   carries the place name, so the question "where did this cost come from" is
   answered by the box it sits in rather than a column repeated on every row. */
.pt-box { border: 1.5px solid var(--text); border-radius: var(--radius-sm); margin-bottom: var(--space-3, 12px); overflow: hidden; }
.pt-box-t {
    background: var(--text); color: var(--card);
    padding: .35rem .7rem; font-size: var(--fs-xs); font-weight: 600;
}
.pt-box-b { padding: .2rem .7rem .5rem; }
.pt-row {
    display: flex; align-items: center; gap: .6rem;
    padding: .4rem 0; border-bottom: 1px solid var(--rule-2, var(--border));
}
.pt-row:last-child { border-bottom: 0; }
.pt-lbl { flex: 0 0 6.5rem; font-size: var(--fs-xs); font-weight: 600; }
.pt-det { flex: 1 1 auto; font-size: var(--fs-xs); color: var(--text-muted); min-width: 0; }
.pt-amt { flex: 0 0 7.5rem; text-align: right; font-variant-numeric: tabular-nums; }

/* ── Trip Details + Trip costs: matched, fixed-height boxes that scroll ───
   Both sit in the same row, and either one's content can run long — a dozen
   reported costs, or a trip with detour stops and a delay reason. Fixed
   height (not max-height) on BOTH means they always match regardless of
   which side has more to say, with the longer one scrolling inside itself
   rather than stretching the row: Costs' header (Document tick and Add a
   cost included) and its Save row stay put, and Trip Details' own heading
   stays put too, which is what makes the scroll obviously a scroll rather
   than the two cards drifting out of line. */
#payroll, #tripDetails {
    display: flex; flex-direction: column;
    height: min(60vh, 31.25rem);
}
#tripDetails .detail-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* flex: 1 only does anything on an element whose DIRECT parent is a flex
   container. .table-wrapper's real parent is the <form> around it, not
   #payroll — so this rule alone was a no-op: the form kept its natural
   height, and everything past the card's fixed height (the Save row, the
   Add button below it) spilled out uncontained rather than scrolling. The
   form has to be a flex column too, so the flex-ness actually reaches its
   child.

   Scoped to .cost-panel specifically, not a bare "#payroll > form" — the
   Document tick used to be a second direct-child <form> here (.cost-doc-row,
   now folded into the card header instead) and the untargeted selector
   caught it too: flex-direction: column stacked its toggle and its hint
   text instead of the single tidy row it was asking for. Left scoped this
   way even with only one direct-child form again — the same trap is one
   sibling form away from reopening. */
#payroll > .cost-panel {
    display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto;
}
#payroll .table-wrapper { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* The heading row has to stay readable while the rows move under it.
   Padding and font-size are here too, not in the shared th/td rule below —
   #payroll thead th outranks #payroll .table-wrapper th on specificity (a
   class beats an extra type selector), so a font-size set over there would
   have silently lost to whatever this rule left unset. */
#payroll thead th {
    position: sticky; top: 0; z-index: 1;
    background: var(--card); box-shadow: inset 0 -1px 0 var(--border);
    font-size: var(--fs-xs); padding: 6px 8px;
}
/* Half of even a laptop viewport still isn't much once badges' white-space:
   nowrap is added up across three columns — tightened here only, so the
   rest of the app's tables keep their normal size. Measured against a real
   laptop window: a stacked card ran 1221px wide, so a 2-column half is
   ~605px after the grid gap — this table needs to live inside that, not
   the ~780px it wanted before tightening. */
#payroll .table-wrapper td { padding: 6px 8px; font-size: var(--fs-xs); }
#payroll .table-wrapper .badge { padding: 2px 7px; font-size: var(--fs-xs); gap: 3px; }
/* No separate breakpoint needed for this pairing any more — tightened to
   fit a real half column, the generic .detail-grid rule above (900px)
   already stacks it exactly where a half column stops being enough, and
   .table-wrapper's own overflow-x is the fallback below that. */

/* ── Moveable, resizable table columns ────────────────────────────────────
   "trip list table column can be adjustable and move and arrange" (client,
   2026-08-26). The server renders ONE order — its default — and app.js
   reorders the rendered cells per browser. See rtTableColumns().

   The grip is a separate target rather than the whole header, because the
   header already holds a sort link and a filter button and making the cell
   itself draggable makes both of those awkward to click. */
/* The whole header cell is the handle — see the note in tasks/index.php. A 12px
   invisible strip was a target you had to already know about. */
.is-movecols th[data-col] {
    position: relative; padding-left: 16px; padding-right: 14px;
    cursor: grab;
}
.is-movecols th[data-col]:active { cursor: grabbing; }
/* The sort link keeps its own pointer: inside a grabbable cell, the one thing
   that is still a click has to say so. */
.is-movecols th[data-col] .th-sort,
.is-movecols th[data-col] .th-filter { cursor: pointer; }

/* The AFFORDANCE, not the target. Always faintly there so a column reads as
   movable before anyone hovers it — invisible until hover is how the whole
   feature went unnoticed. */
.th-grip {
    position: absolute; left: 4px; top: 50%; transform: translateY(-50%);
    width: 8px; height: 14px;
    opacity: .3; transition: opacity .12s ease;
    pointer-events: none;          /* the <th> under it carries the drag */
    /* Two columns of dots — the universal "pick this up" mark, drawn rather
       than shipped as a glyph so it inherits nothing from the font stack. */
    background-image: radial-gradient(currentColor 1.1px, transparent 1.1px);
    background-size: 4px 4px; color: var(--text-muted);
}
th:hover > .th-grip { opacity: .85; }

.th-resize {
    position: absolute; right: 0; top: 0; bottom: 0; width: 9px;
    cursor: col-resize; touch-action: none;
}
.th-resize::after {
    content: ''; position: absolute; right: 4px; top: 25%; bottom: 25%;
    width: 1px; background: var(--border); opacity: 0; transition: opacity .12s ease;
}
th:hover > .th-resize::after, .th-resize:hover::after { opacity: 1; }

.is-movecols th.is-dragging { opacity: .45; }
.is-movecols th.is-drop     { box-shadow: inset 2px 0 0 var(--accent); }
/* A drag that selects the header text instead of moving it reads as broken. */
.is-movecols.is-resizing    { user-select: none; cursor: col-resize; }

/* Only once a width has actually been set — see pin() for why both halves are
   needed. width:max-content lets the pinned columns add up to more than the
   card, and .table-wrapper is already overflow:auto, so it scrolls. */
.data-table.is-fixed,
.is-movecols.is-fixed,
.inv-items-table.is-fixed { table-layout: fixed; width: max-content; min-width: 100%; }
.inv-items-table.is-fixed th[data-col] { overflow: hidden; }
.data-table.is-fixed td:not(.col-actions),
.data-table.is-fixed th[data-col] { overflow: hidden; }
.data-table.is-fixed td:not(.col-actions) { text-overflow: ellipsis; }
/* A column dragged narrower than its words cuts them off — "ADAMM…" — instead
   of wrapping every cell into a tower (2026-10-09). An ellipsis needs one line
   AND a clipping box, so nowrap goes on the cell and the clip on the blocks
   inside it too: a second line (an email under a name) is a block of its own.
   The full text is a hover away — app.js sets the title on a cut-off cell. */
/* A resized moveable table fills its card: width 100%, with a floor app.js sets
   to the sum of its columns. The LAST column is left unpinned and takes the
   slack, so a narrowed column stays narrow and nothing leaves a gap. */
.is-movecols.is-fixed { width: 100%; }
.is-movecols thead th:last-child > .th-resize { display: none; }
.is-movecols.is-fixed td[data-col],
.is-movecols.is-fixed th[data-col] { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.is-movecols.is-fixed td[data-col] > div,
.is-movecols.is-fixed td[data-col] > div > div,
.is-movecols.is-fixed td[data-col] > div > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.is-movecols.is-fixed th[data-col] .th-cell { min-width: 0; }
.is-movecols.is-fixed th[data-col] .th-sort { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A pill, a chip or a badge truncates INSIDE its own shape — "Manage…" in a whole
   pill — rather than being sliced at the cell edge with a stray "…" after it.
   Controls (buttons, selects, inputs) are never ellipsized; the cell just clips.
   Badges and status tags are flex boxes with a dot drawn before them, so they are
   left to the cell's own ellipsis, which already ends them cleanly. */
.is-movecols.is-fixed td[data-col] :is(.role-pill, .code-chip) {
  display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; vertical-align: middle;
}
.is-movecols.is-fixed td[data-col]:has(.role-pill, .code-chip, button, select, input, .ui-select) {
  text-overflow: clip;
}

/* ── Trip list cells ──────────────────────────────────────────────────────
   These were inline styles on the hand-written <td>s. The cells are built from
   one loop now (see $tripColumns in tasks/index.php), so a per-column look has
   to be a class — which is the better place for it anyway. */
.cell-customer, .cell-depart, .cell-arrive, .cell-created { font-size: var(--fs-xs); }
.cell-truck            { font-size: var(--fs-xs); }
.cell-depart, .cell-arrive { color: var(--text-muted); }
.cell-actual           { color: var(--text); }
.cell-sub              { font-size: var(--fs-xs); color: var(--text-muted); }
.route-arrow           { color: var(--accent); margin: 0 3px; }
.route-count           { font-size: var(--fs-xs); margin-top: .15rem; }
/* The status select is the phase, so it reads as one — the arrows in its
   options already say which way each choice moves the trip. */
.inline-status         { min-width: 9.5rem; }

/* ── Public holidays on the Schedule ──────────────────────────────────────
   THE SCHEDULE ONLY, of the four calendars — it is the one that answers "is
   this truck free on Thursday", and a day customs and the customer are shut is
   that kind of answer. Drawn as a DAY marking, never as a bar: a holiday does
   not enter the lane packer, so it can never push a trip into another row or
   count towards a cell's "+N more".

   Red, and only red, because it is the one thing on this grid that is not a
   booking — every bar colour means a job, a workshop day or a leave day. */
.mcell.is-holiday { background: rgba(217, 48, 37, .06); }
.mcell.is-holiday .day-num { color: var(--danger); font-weight: 600; }
.mcell-hol {
    font-size: var(--fs-xs); font-weight: 600; line-height: 1.25;
    color: var(--danger); background: #fdeceb;
    border-radius: var(--radius-xs); padding: 1px 4px; margin-bottom: 2px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A day column is too narrow for a name, so the timeline tints and the note
   above the grid spells it out. is-weekend may apply to the same column — a
   holiday falling on a Sunday is legitimately both. */
.tl-day.is-holiday { background: rgba(217, 48, 37, .07); border-radius: var(--radius-xs); }
.tl-day.is-holiday .tl-dow, .tl-day.is-holiday .tl-dnum { color: var(--danger); opacity: 1; font-weight: 600; }

.cal-legend-item.is-holiday .cal-legend-dot { background: var(--danger); }
.cal-holiday-note {
    display: flex; flex-wrap: wrap; gap: .35rem .9rem;
    font-size: var(--fs-xs); color: #b3261e;
    background: #fdeceb; border: 1px solid rgba(217, 48, 37, .18);
    border-radius: var(--radius); padding: .4rem .6rem; margin-bottom: var(--stack-gap);
}

/* Settings → Holidays: the add row is date + name + button on one line. */
.hol-add { display: flex; gap: var(--space-3); align-items: flex-end; flex-wrap: wrap; }
.hol-add .form-group { margin: 0; }
.hol-add .hol-name   { flex: 1 1 18rem; }

/* ── A split action: the option and the act it modifies, as one control ──────
   "Direct to AutoCount" + "Approve invoice". Set apart, a tick beside a button
   is a second thing to notice and the commonest way an option gets pressed past
   without being read — and this one decides whether a real document lands in a
   real account book. One outline says the choice belongs to the press.

   Green from --status-completed, the app's own approved colour, not the primary
   blue: blue means "the main action here", and this colour is naming the
   OUTCOME. The two halves are one hue at two weights, which is what makes them
   read as halves rather than as two controls that happen to touch.

   EVERY METRIC IS .btn's, so this sits in a row of buttons without being a
   second type scale: 0.85rem / 600, 8px 16px, 7px gap, --radius-sm, 15px icon.
   font-family is not set — `button { font-family: inherit }` at the top of this
   file already carries it, and repeating it here would be a second place to
   forget when the face changes. */
.act-split {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--status-completed);
  border-radius: var(--radius-sm);
  /* Clips the halves to the shared radius so the seam is a straight line and
     the outer corners are one curve. The focus ring is drawn on THIS element
     (:focus-within below), never on a child, or overflow would slice it off. */
  overflow: hidden;
  background: var(--card);
  /* Offset and blur, not a zero-offset halo — it sits on a sticky bar and has
     to lift off the content scrolling under it. */
  box-shadow: var(--shadow-sm);
}
.act-split:focus-within {
  outline: 2px solid var(--status-completed);
  outline-offset: 2px;
}

/* The option half. Pale, so the solid half is plainly the one that acts. */
.act-split-opt {
  display: inline-flex; align-items: center; gap: var(--space-2);
  /* 6 + 18 + 6, inside the wrapper's 1px border = --control-h, like .btn */
  padding: 6px var(--space-4);
  margin: 0;
  cursor: pointer;
  font-size: var(--fs-sm); font-weight: 600; line-height: var(--control-lh);
  /* The bare `label` rule near the top of this file is written for a FORM FIELD
     caption — block, with its own colour and a bottom margin (and, until the
     2026-09 redesign, uppercase and tracked: this once read "DIRECT TO
     AUTOCOUNT" in small caps beside a sentence-case button). Both halves take
     .btn's metrics — --fs-sm/600 on a 20px line — so the only difference across
     the seam is colour. */
  text-transform: none;
  /* var(--success-text) on this tint is the pair .alert-success already ships — comfortably
     past 4.5:1, and tinted from the hue rather than greyed. */
  color: var(--success-text);
  background: rgba(22, 163, 74, .08);
  user-select: none;
  transition: background var(--transition);
}
.act-split-opt:hover { background: rgba(22, 163, 74, .15); }
.act-split-opt input {
  width: 15px; height: 15px; margin: 0; flex-shrink: 0;
  accent-color: var(--status-completed);
  cursor: pointer;
}
/* Ticked, the option stops being a suggestion — it now describes what the press
   will do, so it firms up rather than staying a whisper beside a loud button. */
.act-split-opt:has(input:checked) { background: rgba(22, 163, 74, .2); color: #14532d; }
/* The ring belongs to the whole control (:focus-within); an inner default would
   be a second ring, in the wrong place, clipped by the overflow above. */
.act-split-opt input:focus-visible { outline: none; }

/* The acting half. Its own element rather than .btn, because .btn carries a
   radius and a press-scale that would both redraw the seam this control exists
   to hide — a half that shrinks on :active opens a gap down the middle. It
   darkens instead, which is the same feedback without moving the join. */
.act-split-go {
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: 6px var(--space-4);
  border: 0;
  /* The seam: one hairline off the shared hue, so the halves stay joined rather
     than becoming two buttons that happen to touch. */
  border-left: 1px solid rgba(255, 255, 255, .35);
  background: var(--status-completed);
  color: #fff;
  font-size: var(--fs-sm); font-weight: 600; line-height: var(--control-lh);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--transition);
}
.act-split-go:hover  { background: var(--success-text); }
.act-split-go:active { background: #14532d; }
.act-split-go:focus-visible { outline: none; }   /* the wrapper carries it */
.act-split-go svg { width: 15px; height: 15px; flex-shrink: 0; }

/* The bar wraps on a narrow screen; the control must not be split by the wrap
   it is designed to survive. */
@media (max-width: 640px) {
  .act-split { width: 100%; }
  .act-split-go { margin-left: auto; }
}

/* ── Console legs on the trip form ────────────────────────────────────────
   Another customer's load on the SAME run — its own pick-up, drop-off and
   charge, billed on its own invoice. A block rather than more rows in the two
   route lists, because the three fields only mean anything together: a console
   pick-up with nobody to bill is not a leg, it is a stray address. */
.rt-consoles:empty { display: none; }
.rt-console {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--card-hover);
  padding: var(--space-3); margin-bottom: var(--space-2);
}
.rt-console-head {
  display: flex; align-items: center; gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.rt-console-n {
  font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted);
}
.rt-console-head .rt-stop-x { margin-left: auto; }
.rt-console-body { display: grid; gap: var(--space-2); grid-template-columns: 1fr; }
@media (min-width: 720px) { .rt-console-body { grid-template-columns: 1fr 1fr 1fr; } }
.rt-console-f { display: flex; flex-direction: column; gap: 4px; margin: 0; }
/* The bare `label` rule is a form-field caption (block, --fs-sm/500, label
   colour; uppercase and tracked before the 2026-09 redesign). Here the label
   wraps a whole field, so its own text is a caption but the control inside must
   not inherit its styling. Same trap as .act-split-opt and .inv-einv-tick. */
.rt-console-f > span {
  font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted);
  text-transform: none; letter-spacing: normal;
}
.rt-console-add { margin-bottom: var(--space-2); }

/* "Bills to" only exists once the trip actually has a leg — a column reading
   "Main trip" on every row of every trip is a column nobody reads. */
.rt-charges[data-legs="0"] .charge-leg { display: none; }

/* ── The explanation a field needs, behind an icon ────────────────────────
   "all this description i think no need, maybe can have like info icon and
   mouse over and show, will be more clean and clear" (client, 2026-08-27).

   The trip form carried five permanent sentences under five fields. Each was
   true and each was read once; after that they are furniture that pushes the
   next field down the page on every visit. The text is kept — it answers a real
   question — it just stops being asked out loud. See rtFieldInfo(). */
.field-info { position: relative; display: inline-flex; vertical-align: middle; margin-left: 5px; }
.field-info-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 15px; height: 15px; padding: 0;
    border: 0; background: none; cursor: help;
    color: var(--text-muted); opacity: .55;
    transition: opacity .12s ease, color .12s ease;
}
.field-info-btn svg { width: 15px; height: 15px; }
.field-info-btn:hover, .field-info-btn:focus-visible { opacity: 1; color: var(--accent); }
.field-info-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 50%; }

/* position:fixed, for the same reason .th-pop is. A field's explanation lives
   inside .inv-dialog-body, which is overflow-y:auto — so an in-flow panel is
   CLIPPED at the scroll container's edge, and on the first field of a dialog the
   tip opened upward straight into that edge and lost its top half.

   Fixed takes it out of the clip entirely; rtFieldInfoPlace() supplies the
   coordinates on hover and flips it below when there is no room above. Left as
   absolute-with-no-JS it still appears, just possibly clipped — a tooltip that
   needs a script to EXIST is worse than one that needs a script to be tidy. */
.field-info-pop {
    position: fixed; top: 0; left: 0;
    /* ABOVE the shell. The sidebar and sticky header sit at 100–200, so a lower
       value painted the tip under them and the first words vanished behind the
       nav. 950 clears every chrome layer and still sits under a modal. */
    z-index: 950; width: max-content; max-width: 15rem;
    padding: 7px 10px; border-radius: var(--radius-sm);
    background: var(--text); color: #fff;
    /* The bare `label` rule reaches in here too — the pop sits inside a <label>
       on most of its uses. Fourth instance of that inheritance trap, after
       .act-split-opt, .inv-einv-tick and .arr-lbl. */
    font-size: var(--fs-xs); font-weight: 500; line-height: 1.45;
    text-transform: none; letter-spacing: normal; text-align: left;
    /* `thead th` sets white-space: nowrap, and a tip rendered from a column
       heading INHERITS it — so the text laid out on one line, max-width clamped
       the dark box to 15rem, and the sentence ran out of it across the page.
       Fifth instance of a bare element rule reaching into this component, after
       .act-split-opt, .inv-einv-tick, .arr-lbl and the font rules above. It also
       broke the placement: rtFieldInfoPlace() clamps on offsetWidth, which
       reported the clamped 15rem while the visible text was three times that,
       so the edge check had nothing to catch. */
    white-space: normal;
    box-shadow: var(--shadow);
    opacity: 0; visibility: hidden;
    transition: opacity .12s ease, transform .12s ease, visibility .12s;
    pointer-events: none;          /* never eats a click meant for the field */
}
/* Driven by rtFieldInfoPlace(), which reparents the tip to <body> — where the
   :hover rule below can no longer reach it. Both paths are kept: no JS, the tip
   stays put and shows on hover; with JS, it is portalled and this class shows
   it. */
.field-info-pop.is-shown { opacity: 1; visibility: visible; }

.field-info-pop::after {
    content: ''; position: absolute; top: 100%; left: 12px;
    border: 4px solid transparent; border-top-color: var(--text);
}
/* Flipped below the icon when there is no room above — the tail points up. */
.field-info-pop.is-below::after {
    top: auto; bottom: 100%;
    border-top-color: transparent; border-bottom-color: var(--text);
}
/* Hover AND focus — tabbing to the icon has to show it, or the explanation is
   mouse-only. :focus-within covers the button inside. */
.field-info:hover .field-info-pop,
.field-info:focus-within .field-info-pop {
    opacity: 1; visibility: visible; transform: translateY(0);
}
/* Near the left edge of a form column the centred pop would be clipped. */
@media (max-width: 560px) { .field-info-pop { max-width: 13rem; } }

/* A button and its explanation are one thing, so they sit on one line. The icon
   was appended after the button and the surrounding grid put it in the next
   cell — an orphan ⓘ floating in the middle of the row, attached to nothing.
   Named for what it does, not for the console it was first written for. */
.btn-with-info { display: inline-flex; align-items: center; gap: 2px; }

/* ── Console legs ─────────────────────────────────────────────────────────
   Another customer's load on the SAME run — its own pick-up, drop-off, charges
   and invoice. Added from the trip list and the trip page rather than the trip
   form: a console is normally decided after the run is booked. */
.console-cell { gap: 5px; padding-left: 6px; padding-right: 8px; }
.console-cell.is-add { color: var(--text-muted); }
.console-cell.is-add:hover { color: var(--accent); }

/* The legs a trip already has, along the top of the dialog, so you can move
   between them without closing it. */
.console-legs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: var(--space-3); }
.console-legs[hidden] { display: none; }
.console-leg-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 10px; border: 1px solid var(--control-border); border-radius: var(--radius-pill);
  background: var(--surface); cursor: pointer;
  font-size: var(--fs-xs); font-weight: 600; color: var(--text);
}
.console-leg-chip span { font-weight: 500; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.console-leg-chip:hover { border-color: var(--accent); }
.console-leg-chip.is-current { border-color: var(--accent); background: rgba(37,99,235,.07); color: var(--accent); }
.console-leg-chip.is-current span { color: inherit; }
.console-leg-chip.is-add { border-style: dashed; color: var(--text-muted); }

/* The trip page's own section. A console bills separately, so its charges are
   their own block rather than more rows in the trip's — reading them as one
   list is how somebody invoices the wrong customer. */
.console-block {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: var(--space-3); margin-bottom: var(--space-2);
}
.console-block-head { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); }
.console-block-cust { font-weight: 600; font-size: var(--fs-sm); }
.console-block-route { font-size: var(--fs-xs); color: var(--text-muted); }
.console-block-total { margin-left: auto; font-weight: 600; font-variant-numeric: tabular-nums; }
.console-block .data-table { font-size: var(--fs-xs); }

/* ── Charges, grouped by what they bill ───────────────────────────────────
   A console leg is a separate document for a different customer, so its lines
   sit under their own band with their own invoice reference and their own
   subtotal. Listed among the main job's they read as one bill, which is how
   somebody invoices the wrong account.

   ONE TABLE, not one per group: separate tables size their columns
   independently, so Variant and Amount did not line up between the groups — and
   a column of money you cannot scan straight down defeats the card. */
.charge-band > th {
  padding: var(--space-3) var(--card-pad) var(--space-2);
  background: var(--card-hover);
  border-top: 1px solid var(--border);
  /* The band is a heading, not a column head — it must not inherit the
     uppercase tracking .data-table th carries, nor <th>'s centred default,
     which floated the group name into the middle of the row. */
  text-align: left; text-transform: none; font-weight: 400;
}
/* The first band closes up against the column header above it. */
.charge-table tbody tr.charge-band:first-child > th,
.charge-table tbody tr.charge-band:first-child > td { border-top: 0; }

.charge-band > th > * { vertical-align: middle; }
.charge-band-name  { font-weight: 600; font-size: var(--fs-sm); color: var(--text); margin-right: .5rem; }
.charge-band-route { font-size: var(--fs-xs); color: var(--text-muted); margin-right: .5rem; }
.charge-band-meta  { font-size: var(--fs-xs); color: var(--text-muted); }
.charge-band-edit  { margin-left: .5rem; vertical-align: baseline; }

.charge-band-sum {
  padding: var(--space-3) var(--card-pad) var(--space-2);
  background: var(--card-hover);
  border-top: 1px solid var(--border);
  text-align: right; font-weight: 600; font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.charge-none td { color: var(--text-muted); font-size: var(--fs-xs); font-style: italic; }

/* Every figure in the same rhythm — proportional digits make a money column
   ragged, and this card is read by scanning one. */
.charge-num    { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.charge-amount { font-weight: 600; }

.charge-table tfoot td {
  border-top: 2px solid var(--border);
  padding-top: var(--space-3); padding-bottom: var(--space-3);
}
.charge-total-label { text-align: right; font-weight: 600; font-size: var(--fs-sm); }
.charge-total       { font-size: var(--fs-md); font-weight: 600; }

/* A REAL gap between groups. Padding cannot make one: the band carries a
   background, so a bigger padding-top only grows the tinted area — which is why
   20px of it read as no change at all. An untinted spacer row is the only thing
   that shows as a break between two documents. */
.charge-gap td {
  height: var(--space-5); padding: 0;
  border: 0; background: var(--card);
}

/* The driver's commission rate card, beside the box it is a guide for. Read-only
   and in the sheet's own words — "RM90-RM100" is a range somebody reads while
   typing, not a figure to copy. */
.pt-card { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 8px; font-size: var(--fs-xs); color: var(--text-muted); }
.pt-card strong { color: var(--text); font-weight: 600; }

/* ── A trip's commission ─────────────────────────────────────────────────────
   Set with the trip, not at month end (client, 2026-08-27): by then nobody
   remembers which run had the extra hour. Its own block under the costs and
   outside their form — those are what the trip COST, this is what the driver
   EARNED on it, and one Save for both would fold an unrelated tick into
   approving a commission. */
/* ── Commission: the lines on the left, the rate card on the right ───────────
   Its own card since 2026-09-02 — it was the foot of Trip costs and squeezed
   that card's table into a scrolling strip showing two rows of six.

   Two columns because they are two different things: the LEFT is the record
   being built, the RIGHT is the reference read while building it. The record
   leads, since it is what the card is for and what somebody returns to check. */
.tc-comm { padding: var(--card-pad); }
.tc-comm-scheme { font-size: var(--fs-xs); color: var(--text-muted); }
.tc-comm-scheme.is-none { font-style: italic; }

.cm-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 340px;
    gap: var(--space-8);
    align-items: start;
}
/* The caption idiom this app already uses for a field label — matched rather
   than reinvented, so the two columns read as one form. */
.cm-cap {
    display: block; margin: 0 0 var(--space-3);
    font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted);
}
.cm-lbl {
    display: block; margin-bottom: 5px;
    font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted);
}

/* ── The lines ─────────────────────────────────────────────────────────────
   A ledger, not cards: name on the left, figure on the right, a rule between
   rows and a heavier one under the total. It has to add up down the column. */
.cm-items { list-style: none; margin: 0 0 0; padding: 0; }
.cm-item {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto 28px;
    align-items: center;
    gap: var(--space-3);
    padding: 8px 0;
    border-bottom: 1px solid var(--border);
}
.cm-item-cat { font-size: var(--fs-sm); font-weight: 600; color: var(--text); }
.cm-item-amt { font-size: var(--fs-sm); font-variant-numeric: tabular-nums; color: var(--text); }
.cm-item-del { display: flex; justify-content: flex-end; }
/* Quiet until wanted: removing a line is a correction, not one of the two
   things this panel is for. */
.cm-x {
    display: inline-flex; align-items: center; justify-content: center;
    width: 24px; height: 24px; padding: 0;
    background: none; border: 1px solid transparent; border-radius: var(--radius-sm);
    color: var(--text-muted); cursor: pointer;
    transition: color var(--transition), border-color var(--transition);
}
.cm-x svg { width: 13px; height: 13px; }
.cm-x:hover { color: var(--danger-text); border-color: #fecaca; }
.cm-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.cm-total {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: var(--space-3);
    padding: 10px 0 0;
    border-top: 2px solid var(--text);
    margin-top: -1px;               /* the row rule above it is redundant */
}
.cm-total span {
    font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted);
}
.cm-total strong { font-size: var(--fs-md); font-variant-numeric: tabular-nums; color: var(--text); }
.cm-none { margin: 0; font-size: var(--fs-xs); color: var(--text-muted); }

/* The add row: what for, how much, add — in the order the decision is made. */
.cm-add {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 150px auto;
    gap: var(--space-3);
    align-items: end;
    margin-top: var(--space-5);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
}
.cm-amt { display: flex; align-items: center; gap: var(--space-2); }
.cm-amt > span { font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); }
.cm-amt .form-control {
    flex: 1; min-width: 0; height: var(--control-h);
    font-variant-numeric: tabular-nums; font-size: var(--fs-sm); font-weight: 600;
}
.cm-add select { height: var(--control-h); }

/* ── Suggested ─────────────────────────────────────────────────────────────
   The same ledger shape as the lines, on a tint so it reads as proposed, not
   recorded: category, an editable figure, Add. Disappears as each is added. */
.cm-sug {
    margin: 0 0 var(--space-5);
    padding: var(--space-3) var(--space-4) var(--space-2);
    background: var(--accent-dim);
    border: 1px dashed rgba(37, 99, 235, .35);
    border-radius: var(--radius-sm);
}
.cm-sug-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.cm-sug-head .cm-cap { margin: 0; color: var(--accent); }
.cm-sug-list { list-style: none; margin: var(--space-2) 0 0; padding: 0; }
.cm-sug-item {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 150px auto;
    align-items: center;
    gap: var(--space-3);
    padding: 6px 0;
}
.cm-sug-item + .cm-sug-item { border-top: 1px solid rgba(37, 99, 235, .15); }
.cm-sug-cat { font-size: var(--fs-sm); font-weight: 600; color: var(--text); }
.cm-sug-cat small { display: block; font-size: var(--fs-xs); font-weight: 400; color: var(--text-muted); }
.cm-sug-amt .form-control { background: var(--card); }
@media (max-width: 560px) {
    .cm-sug-item { grid-template-columns: minmax(0, 1fr) auto; }
    .cm-sug-item .cm-sug-amt { grid-column: 1; }
}
.cm-save { height: var(--control-h); }
.cm-err  { margin: var(--space-4) 0 0; }

/* ── The rate card ─────────────────────────────────────────────────────────
   IT SCROLLS. Thirteen categories is taller than the form beside it, so left
   unbounded it set the height of the whole card and the lines floated at the
   top of a lot of white. Capped, it is a reference panel you run down while
   the thing you are building stays put. */
.cm-rates { min-width: 0; }
.cm-scroll {
    max-height: 320px;
    overflow-y: auto;
    /* An inset rule rather than a box: the panel is part of the card, and a
       bordered container inside a card is a box inside a box. */
    border-top: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
    /* Room for the scrollbar so the figures do not sit under it. */
    padding-right: var(--space-2);
    /* Says "there is more" without a shadow or a fade: the last row is cut by
       the container edge, which is the honest cue. */
    overscroll-behavior: contain;
}
.cm-list { list-style: none; margin: 0; padding: 0; }
.cm-row {
    display: flex; align-items: center; justify-content: space-between;
    flex-wrap: wrap;
    gap: 4px var(--space-3);
    padding: 6px 0;
    border-bottom: 1px solid var(--border);
}
.cm-row:last-child { border-bottom: none; }
.cm-vals { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.cm-open { font-size: var(--fs-xs); color: var(--text-muted); font-style: italic; }

/* The NAME picks the category and leaves the amount alone; a FIGURE picks both.
   Two targets on one row because they are two intentions — and a rate the sheet
   never put a number to would otherwise be unclickable on the very card where
   it is most likely to be typed by hand. */
.cm-cat {
    font: inherit; font-size: var(--fs-xs); font-weight: 600;
    color: var(--text); background: none; border: 0;
    padding: 2px 0; margin: 0; text-align: left;
    min-width: 0; cursor: pointer;
    border-bottom: 1px dashed transparent;
    transition: color var(--transition), border-color var(--transition);
}
.cm-cat:hover { color: var(--accent); border-bottom-color: currentColor; }
.cm-cat:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* A figure you can press. Every one of these appears verbatim on the driver's
   own card — a range offers its two ends and nothing between them, because the
   middle would be a number this app invented. */
.cm-val {
    font: inherit; font-size: var(--fs-xs); font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--text); background: var(--card);
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    padding: 3px 9px; cursor: pointer; white-space: nowrap;
    transition: border-color var(--transition), background var(--transition), color var(--transition);
}
.cm-val:hover { border-color: var(--accent); color: var(--accent); }
.cm-val:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Chosen — the figure now in the add row. Typing lets go of it by itself. */
.cm-val[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: #fff; }

.cm-empty { margin: 0; font-size: var(--fs-xs); line-height: 1.55; color: var(--text-muted); }
.cm-empty a { white-space: nowrap; }

@media (max-width: 980px) {
    /* Stacked, the record stays first — it is what the card is for, and the
       rate card is a reference you drop to when you need it. */
    .cm-grid { grid-template-columns: 1fr; gap: var(--space-6); }
    .cm-add  { grid-template-columns: minmax(0, 1fr) 130px auto; }
    .cm-scroll { max-height: 260px; }
}
@media (max-width: 560px) {
    /* Three controls on one line is 90px each on a phone. */
    .cm-add { grid-template-columns: 1fr; }
    .cm-save { justify-self: start; padding-left: var(--space-6); padding-right: var(--space-6); }
}

/* ── What the month adds up to (Payroll) ─────────────────────────────────────
   Four facts on the page that pays for them. Not tiles-as-controls: the pending
   counts live on the tabs below as badges you can press, which is why the four
   that used to stand here were removed. These are read, not clicked. */
.pay-stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: var(--stack-gap); margin-bottom: var(--stack-gap);
}
.pay-stat {
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 14px 16px;
  display: flex; flex-direction: column; gap: 2px; min-width: 0;
}
.pay-stat-n { font-size: var(--fs-xl); font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; line-height: 1.1; }
.pay-stat-l {
  font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted);
  display: inline-flex; align-items: center; gap: 5px;
}
/* A breakdown, where the split IS the figure — a total with no split answers
   nothing the trip count has not already. */
.pay-stat-split { display: flex; flex-wrap: wrap; gap: 2px 12px; margin-top: 4px; font-size: var(--fs-xs); color: var(--text-muted); }
.pay-stat-split strong { color: var(--text); font-variant-numeric: tabular-nums; }
/* Phases are a list, not one number, so they get a line rather than a fifth
   tile — a tile would give the shortest phase the weight of the whole count. */
.pay-stats-status {
  display: flex; flex-wrap: wrap; gap: 4px 16px;
  margin: calc(-1 * var(--space-1)) 0 var(--stack-gap);
  font-size: var(--fs-xs); color: var(--text-muted);
}
.pay-stats-status strong { color: var(--text); font-variant-numeric: tabular-nums; }

/* ── Renewals coming due (Truck Scheduler) ───────────────────────────────────
   On the page that books them rather than as a notification: a reminder you
   cannot act on is a nag, and every row here is one click from the thing it is
   warning about. */
.due-strip {
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-sm);
  padding: var(--space-3) var(--space-4); margin-bottom: var(--stack-gap);
}
.due-strip-h {
  font-size: var(--fs-xs); font-weight: 600;
  color: var(--text-muted); display: flex; align-items: center; gap: 6px; margin-bottom: 8px;
}
.due-strip-h strong { color: var(--text); font-size: var(--fs-sm); letter-spacing: 0; }
.due-list { display: flex; flex-wrap: wrap; gap: 8px; }
.due-item {
  display: inline-flex; align-items: baseline; gap: 8px;
  padding: 6px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg); font-size: var(--fs-xs); color: var(--text); text-decoration: none;
  transition: border-color .12s, background .12s;
}
a.due-item:hover { border-color: var(--text-muted); background: var(--card-hover); text-decoration: none; }
.due-plate { font-weight: 600; }
.due-what { color: var(--text-muted); }
.due-when { font-size: var(--fs-xs); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.due-when em { font-style: normal; font-weight: 600; color: var(--text); margin-left: 4px; }
/* Overdue reads differently from merely close — the same distinction the trip
   overrun makes, in the same colour. */
.due-item.is-late { border-color: var(--status-cancelled, var(--danger)); }
.due-item.is-late .due-when em { color: var(--status-cancelled, var(--danger)); }

/* The receipt behind a standby-cash movement, on the row it belongs to. Served
   through payroll/pc-doc.php, which asks who is looking. */
.pc-doc {
  display: inline-flex; align-items: center; gap: 4px; margin-left: 8px;
  font-size: var(--fs-xs); color: var(--accent); text-decoration: none; white-space: nowrap;
}
.pc-doc:hover { text-decoration: underline; }

/* ── Icon-only row actions (approve / reject) ────────────────
   A decision taken FROM the row, for a table that already shows everything the
   decision needs — the leave list carries the type, the dates, the days, the
   reason and the certificate link. They open the same remarks modal the text
   buttons do, so this is a shorter path to the decision and not a way round it.

   Square and small, because two of them sit in a cell beside a badge and a
   Document link; anything button-sized would make the row about them. */
.btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-h-sm);
  height: var(--control-h-sm);
  padding: 0;
  border: 1px solid var(--control-border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
  transition: background var(--transition), border-color var(--transition), color var(--transition);
}
.btn-icon svg { width: 15px; height: 15px; }

/* Colour arrives on hover, not at rest: a row of permanently green and red
   chips reads as a status, which is the thing directly beside them. */
.btn-icon.btn-approve { color: var(--status-completed); }
.btn-icon.btn-approve:hover,
.btn-icon.btn-approve:focus-visible {
  background: var(--success-bg);
  border-color: var(--success);
}
.btn-icon.btn-reject { color: var(--status-overdue); }
.btn-icon.btn-reject:hover,
.btn-icon.btn-reject:focus-visible {
  background: var(--danger-bg);
  border-color: var(--danger);
}

/* An amount being corrected inside the payroll accordion. Narrow and right-set,
   because the column is a money column and the figure has to stay comparable
   down the table while it is editable. */
.dp-amt {
  width: 92px;
  text-align: right;
  font-variant-numeric: tabular-nums;
  padding: 4px 8px;
  font-size: var(--fs-sm);
}

/* ── Dashboard: the week board ────────────────────────────────────────────
   Seven day columns read across like a station's timetable board, one row per
   thing worth counting; the day you pick fills the page below (index.php,
   components/dashboard.php). The board is one card edge to edge — the column
   rules ARE the design, so the card's own padding steps aside for them.

   Two marks and only two. The PICKED day is filled navy, the shell's own
   "where you are" colour. TODAY carries a gold rule on top, the same gold that
   marks the active sidebar item — gold says "here", in time as in the nav.
   Zero fades to a dash so the busy days stand out; a count whose MEANING is
   bad takes the status colour for that meaning, never for its size. */
.wb { padding: 0; overflow: hidden; }
.wb-header {
  padding: var(--space-4) var(--card-pad) var(--space-3);
  margin-bottom: 0;
  justify-content: flex-start;
}
.wb-header .card-title { margin: 0; }
.wb-range { color: var(--text-muted); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.wb-link { margin-left: auto; }

.wb-scroll { overflow-x: auto; overscroll-behavior-x: contain; }
.wb-grid {
  --wb-head-h: 64px;
  --wb-row-h:  40px;
  --wb-label-w: 132px;
  display: grid;
  grid-template-columns: var(--wb-label-w) minmax(0, 1fr);
  min-width: calc(var(--wb-label-w) + 7 * 60px);
}
.wb-labels {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-right: 1px solid var(--border);
  /* Sticky so a phone scrolling the days sideways still knows what each row counts. */
  position: sticky;
  left: 0;
  z-index: 1;
}
.wb-corner { height: var(--wb-head-h); border-bottom: 1px solid var(--border); }
.wb-label {
  height: var(--wb-row-h);
  display: flex;
  align-items: center;
  padding: 0 var(--space-4) 0 var(--card-pad);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text-label);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.wb-label:last-child, .wb-cell:last-child { border-bottom: 0; }

.wb-days { display: grid; grid-template-columns: repeat(7, minmax(60px, 1fr)); }
.wb-day {
  position: relative;
  display: flex;
  flex-direction: column;
  color: var(--text);
  text-decoration: none;
  border-right: 1px solid var(--border);
  transition: background var(--transition);
}
.wb-day:last-child { border-right: 0; }
.wb-day:hover { background: var(--surface-2); text-decoration: none; }
.wb-day:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.wb-head {
  height: var(--wb-head-h);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  border-bottom: 1px solid var(--border);
  transition: background var(--transition), color var(--transition);
}
.wb-dow { font-size: var(--fs-xs); font-weight: var(--fw-medium); color: var(--text-muted); }
.wb-date {
  font-size: var(--fs-lg);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-tight);
  font-variant-numeric: tabular-nums;
}
.wb-date small {
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
  margin-left: 3px;
}
.wb-day.is-today .wb-head::before {
  content: '';
  position: absolute;
  top: 0; left: 8px; right: 8px;
  height: 3px;
  border-radius: 0 0 var(--radius-xs) var(--radius-xs);
  background: var(--rt-gold-500);
}
.wb-day.is-on .wb-head { background: var(--brand); color: #fff; }
.wb-day.is-on .wb-dow,
.wb-day.is-on .wb-date small { color: var(--rt-navy-200); }
.wb-day.is-on .wb-cell { background: var(--brand-tint); }
.wb-day.is-on:hover { background: transparent; }

.wb-cell {
  height: var(--wb-row-h);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
  border-bottom: 1px solid var(--border);
  transition: background var(--transition);
}
.wb-cell.is-zero { color: var(--text-dim); font-weight: var(--fw-regular); }
.wb-cell.is-quiet { color: var(--text-label); font-weight: var(--fw-regular); }
.wb-cell[data-tone="warning"] { color: var(--warning-text); }
.wb-cell[data-tone="danger"]  { color: var(--danger-text); }

.wb-panel:not([hidden]) { animation: wb-in 180ms var(--ease-out); }
@keyframes wb-in { from { opacity: 0; transform: translateY(4px); } }
.wb-panel-sub { margin-top: 2px; font-size: var(--fs-sm); color: var(--text-muted); }
.wb-panel .card-header { margin-bottom: var(--space-3); }
.wb-sub-title {
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--text-label);
  margin: var(--space-5) 0 var(--space-2);
}
.wb-sub-title:first-of-type { margin-top: 0; }
.wb-empty { color: var(--text-muted); font-size: var(--fs-sm); padding: var(--space-2) 0; }
.wb-cont { margin-top: var(--space-3); font-size: var(--fs-sm); color: var(--text-muted); }
.wb-cont-go { appearance: none; background: none; border: 0; padding: 0; margin-left: var(--space-1); font: inherit; cursor: pointer; }
.wb-when { white-space: nowrap; font-variant-numeric: tabular-nums; }
/* The one cell allowed to wrap: a route is the longest thing on the row, and
   letting it break keeps the status — the thing you scan for — on screen. */
.wb-trips td.wb-route { white-space: normal; min-width: 150px; }
.wb-arrow { color: var(--accent); }
.wb-trips th, .wb-trips td { white-space: nowrap; }
.wb-plate { font-weight: var(--fw-semibold); }
.wb-who { display: block; font-size: var(--fs-xs); color: var(--text-muted); }
.wb-inv { display: inline-flex; align-items: center; gap: var(--space-2); margin-right: var(--space-2); white-space: nowrap; }

/* Off the road: hatched, like the absence bars on the calendars, so a day
   nobody can work never reads as one more job. */
.wb-off { list-style: none; display: grid; gap: var(--space-1); }
.wb-off-row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  background-image: repeating-linear-gradient(45deg,
      transparent 0 6px, color-mix(in srgb, var(--neutral) 9%, transparent) 6px 12px);
}
.wb-off-who { font-weight: var(--fw-semibold); }
.wb-off-why { color: var(--text-muted); }

@media (max-width: 640px) {
  .wb-grid { --wb-label-w: 104px; --wb-head-h: 56px; --wb-row-h: 38px; }
  .wb-label { padding-left: var(--space-4); }
  .wb-header { flex-wrap: wrap; row-gap: var(--space-1); }
}
@media (max-width: 640px) {
  /* A seven-column table on a phone shows three columns and hides the lorry
     and the status — the two things Management checks from it. Each trip
     becomes one stacked row instead: reference and status, then who and
     where, then the lorry and when it leaves. */
  .wb-panel .table-wrapper { overflow: visible; border: 0; }
  .wb-trips thead { display: none; }
  .wb-trips, .wb-trips tbody { display: block; }
  .wb-trips tbody tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "trip status" "cust cust" "route route" "lorry when";
    gap: 2px var(--space-3);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
  }
  .wb-trips tbody tr:last-child { border-bottom: 0; }
  .wb-trips tbody td { display: block; padding: 0; border: 0; white-space: normal; background: none; }
  .wb-trips td.wb-trip   { grid-area: trip; }
  .wb-trips td.wb-status { grid-area: status; justify-self: end; }
  .wb-trips td.wb-cust   { grid-area: cust; font-weight: var(--fw-medium); }
  .wb-trips td.wb-route  { grid-area: route; min-width: 0; color: var(--text-muted); }
  .wb-trips td.wb-lorry  { grid-area: lorry; }
  .wb-trips td.wb-when   { grid-area: when; justify-self: end; align-self: end; font-size: var(--fs-xs); color: var(--text-muted); }
  .wb-trips td.wb-lorry .wb-who { display: inline; margin-left: var(--space-2); }
}
@media (prefers-reduced-motion: reduce) {
  .wb-panel:not([hidden]) { animation: none; }
}

/* ── Dashboard: the counts ────────────────────────────────────────────────
   Fleet status and Needs attention, the counts the office has always opened
   this page for (client, 2026-10-07: "keep these also as in original"). Every
   count is a DOOR: a button that opens its list in the tabbed card below, or a
   link to its page. The number is text-coloured like every KPI in the app; the
   icon tile carries the tone, amber when the count is somebody's job. */
.kpi-group-title {
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  margin: var(--space-5) 0 var(--space-3);
}
.page-head + .kpi-group-title { margin-top: var(--space-2); }
.kpi-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: var(--stack-gap);
  margin-bottom: var(--stack-gap);
}
.kpi {
  appearance: none;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  text-align: left;
  padding: var(--space-3) var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  color: var(--text);
  font: inherit;
  cursor: pointer;
  text-decoration: none;
  transition: border-color var(--transition), box-shadow var(--transition), transform var(--transition);
}
.kpi:hover { border-color: var(--border-strong); box-shadow: var(--shadow); transform: translateY(-1px); text-decoration: none; }
.kpi:active { transform: translateY(0); }
.kpi:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
div.kpi { cursor: default; }
div.kpi:hover { border-color: var(--border); box-shadow: var(--shadow-sm); transform: none; }
.kpi.is-open { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand), var(--shadow-sm); }
.kpi-text { display: flex; flex-direction: column; min-width: 0; }
.kpi-label { font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--text-label); line-height: var(--lh-tight); }
.kpi-icon {
  width: 30px; height: 30px; flex-shrink: 0;
  display: grid; place-items: center;
  border-radius: var(--radius-sm);
  background: var(--neutral-bg); color: var(--neutral-text);
}
.kpi-icon svg { width: 16px; height: 16px; }
.kpi-icon.is-info    { background: var(--info-bg);      color: var(--info-text); }
.kpi-icon.is-pickup  { background: var(--st-pickup-bg); color: var(--st-pickup-text); }
.kpi-icon.is-success { background: var(--success-bg);   color: var(--success-text); }
.kpi-icon.is-warning { background: var(--warning-bg);   color: var(--warning-text); }
.kpi-icon.is-danger  { background: var(--danger-bg);    color: var(--danger-text); }
.kpi-value { font-size: var(--fs-xl); font-weight: var(--fw-bold); line-height: var(--lh-tight); min-width: 1ch; }
.kpi-sub { margin-top: 2px; font-size: var(--fs-xs); color: var(--text-muted); }

/* ── The lists, one tabbed card ─────────────────────────────────────────── */
.act { padding: 0; overflow: hidden; }
.act, #maint { scroll-margin-top: calc(var(--topbar-h) + var(--space-4)); }
/* The baseline is an INSET SHADOW, not a border the tabs overlap with a -1px
   margin: that overlap made the content 1px taller than the strip, and a strip
   that scrolls sideways (overflow-x) scrolls vertically too, so a scrollbar
   appeared for the one pixel. The active tab's own 2px line paints over the
   shadow, which sits on the strip's background. */
.act-tabs {
  display: flex;
  gap: var(--space-1);
  padding: 0 var(--card-pad);
  box-shadow: inset 0 -1px 0 var(--border);
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
}
.act-tab {
  appearance: none;
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-3) var(--space-3);
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--transition), border-color var(--transition);
}
.act-tab:hover { color: var(--text); }
.act-tab.is-on { color: var(--text); border-bottom-color: var(--brand); }
.act-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.act-count {
  display: inline-grid; place-items: center;
  min-width: 20px; height: 20px; padding: 0 6px;
  border-radius: var(--radius-pill);
  background: var(--surface-3);
  color: var(--text-label);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
}
.act-count.is-zero { color: var(--text-dim); font-weight: var(--fw-regular); }
.act-tab.is-on .act-count { background: var(--brand); color: #fff; }
.act-panel { padding: var(--space-4) var(--card-pad) var(--card-pad); }
.act-panel:not([hidden]) { animation: wb-in 180ms var(--ease-out); }
.act-foot { margin-top: var(--space-3); text-align: right; font-size: var(--fs-sm); }
.act-pending td { vertical-align: middle; }
.txt-danger { color: var(--danger-text); }

/* ── Trends ──────────────────────────────────────────────────────────────
   Charts drawn by assets/charts.js from the figures the server allowed the
   role. The marks follow the dataviz rules: one baseline, bars no thicker than
   24px with a rounded data end, 2px lines with a 10% wash, hairline grids,
   navy for a single series with the current period emphasised, job types in
   the calendar's own colours. Values in text tokens, never in series colour. */
.tr { margin-top: var(--space-6); }
.tr-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap;
  margin-bottom: var(--space-3);
}
.tr-heading { font-size: var(--fs-lg); font-weight: var(--fw-semibold); }
.tr-bar .view-toggle { height: var(--control-h); align-items: stretch; }
.tr-range { margin: 0; display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); }
.tr-range:has(.ui-range.is-enhanced) .tr-go { display: none; }
@media (max-width: 640px) {
  .tr-range { flex: 1 1 100%; }
  .tr-range .field-inline { flex: 1 1 100%; min-width: 0; }
}
.tr-range .field-inline { margin: 0; }
.tr-range .field-inline > label {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.tr-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--stack-gap); }
.tr-empty-all {
  display: none;
  padding: var(--space-6) var(--card-pad);
  background: var(--surface); border: 1px dashed var(--border-strong); border-radius: var(--radius);
  text-align: center; font-size: var(--fs-sm); color: var(--text-muted);
}
.tr-empty-all button { margin-left: var(--space-2); }
.tr.is-empty-range .tr-empty-all { display: block; }
.tr.is-empty-range .tr-grid { display: none; }
.tr-card { margin: 0; display: flex; flex-direction: column; min-width: 0; }
.tr-card.is-wide { grid-column: span 2; }
.tr-card.is-full { grid-column: 1 / -1; }
.tr-head { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-3); margin-bottom: var(--space-4); }
.tr-title { font-size: var(--fs-md); font-weight: var(--fw-semibold); }
.tr-sub { margin-top: 2px; font-size: var(--fs-xs); color: var(--text-muted); }
.tr-figure { display: flex; align-items: center; gap: var(--space-2); flex-shrink: 0; }
.tr-total { font-size: var(--fs-lg); font-weight: var(--fw-semibold); line-height: var(--lh-tight); }
.tr-delta {
  display: inline-flex; align-items: center; gap: 3px;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  background: var(--neutral-bg); color: var(--neutral-text);
  font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  white-space: nowrap;
}
.tr-delta svg { width: 10px; height: 10px; }
.tr-delta.is-up   { background: var(--success-bg); color: var(--success-text); }
.tr-delta.is-down { background: var(--danger-bg);  color: var(--danger-text); }

.tr-plot { position: relative; flex: 1; border-radius: var(--radius-sm); }
.tr-plot:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
.tr-svg { display: block; width: 100%; height: auto; overflow: visible; }
.tr-gl { stroke: var(--border); stroke-width: 1; }
.tr-ytick, .tr-xtick { font-family: var(--font-sans); font-size: var(--fs-xs); fill: var(--text-dim); font-variant-numeric: tabular-nums; }
.tr-xtick.is-cur { fill: var(--text); font-weight: var(--fw-semibold); }
.tr-vlabel { font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: var(--fw-semibold); fill: var(--text); }
/* Both steps clear 3:1 against white, the non-text contrast floor: context
   bars navy-400, the emphasised one navy-800. */
.tr-col { fill: var(--rt-navy-400); transition: fill 120ms ease; }
.tr-col.is-cur, .tr-col.is-hot { fill: var(--rt-navy-800); }
.tr-svg.has-hot .tr-col.is-cur:not(.is-hot) { fill: var(--rt-navy-400); }
.tr-hit { fill: transparent; }
.tr-area { fill: var(--rt-navy-500); fill-opacity: .10; }
.tr-line { fill: none; stroke: var(--rt-navy-500); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.tr-dot { fill: var(--rt-navy-500); stroke: var(--surface); stroke-width: 2; }
.tr-cross { stroke: var(--border-strong); stroke-width: 1; }
.tr-tip {
  position: absolute; z-index: 5; pointer-events: none;
  display: flex; flex-direction: column; gap: 1px;
  padding: 6px 10px;
  background: var(--brand); color: #fff;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  white-space: nowrap;
}
.tr-tip[hidden] { display: none; }
.tr-tip strong { font-size: var(--fs-sm); font-weight: var(--fw-semibold); }
.tr-tip span { font-size: var(--fs-xs); color: var(--rt-navy-200); }

.tr-donut { display: flex; align-items: center; gap: var(--space-5); flex-wrap: wrap; }
.tr-donut-dial { position: relative; width: 168px; height: 168px; flex-shrink: 0; margin-inline: auto; }
.tr-donut-dial svg { display: block; }
.tr-donut-mid {
  position: absolute; inset: 0; pointer-events: none;
  display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center;
}
.tr-donut-mid strong { font-size: var(--fs-lg); font-weight: var(--fw-semibold); line-height: var(--lh-tight); }
.tr-donut-mid span { max-width: 92px; font-size: var(--fs-xs); color: var(--text-muted); }
.tr-arc { transition: stroke-width 150ms ease, opacity 150ms ease; }
.tr-arc.is-hot { stroke-width: 26; }
.tr-arc.is-dim { opacity: .35; }

.tr-legend { list-style: none; display: grid; gap: var(--space-1); flex: 1; min-width: 160px; }
.tr-legend li {
  display: grid; grid-template-columns: 10px minmax(0, 1fr) auto auto; align-items: center;
  gap: var(--space-2); padding: 3px var(--space-1);
  border-radius: var(--radius-xs);
  font-size: var(--fs-sm);
}
.tr-legend li.is-hot { background: var(--surface-2); }
.tr-legend-l { color: var(--text-label); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tr-legend-v { font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }
.tr-legend-p { min-width: 34px; text-align: right; font-size: var(--fs-xs); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.tr-sw { width: 10px; height: 10px; border-radius: 3px; background: var(--neutral); }
.tr-sw.is-neutral, .tr-stack-seg.is-neutral { background: var(--neutral); }
.tr-sw.is-warning, .tr-stack-seg.is-warning { background: var(--warning); }
.tr-sw.is-success, .tr-stack-seg.is-success { background: var(--success); }
.tr-sw.is-synced,  .tr-stack-seg.is-synced  { background: var(--synced); }

.tr-rank { list-style: none; display: grid; gap: var(--space-3); }
.tr-rank li { display: grid; grid-template-columns: minmax(0, 8rem) minmax(0, 1fr) auto; align-items: center; gap: var(--space-3); font-size: var(--fs-sm); }
.tr-rank-l { color: var(--text-label); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tr-rank-track { height: 8px; background: var(--surface-3); border-radius: 0 4px 4px 0; overflow: hidden; }
.tr-rank-fill { display: block; height: 100%; background: var(--rt-navy-400); border-radius: 0 4px 4px 0; transition: width 380ms var(--ease-out); }
.tr-rank li.is-lead .tr-rank-fill { background: var(--rt-navy-800); }
.tr-rank-v { min-width: 2.5rem; text-align: right; font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }

.tr-stack { display: flex; gap: 2px; height: 14px; margin: var(--space-2) 0 var(--space-4); }
.tr-stack-seg { min-width: 4px; }
.tr-stack-seg:first-child { border-radius: 4px 0 0 4px; }
.tr-stack-seg:last-child  { border-radius: 0 4px 4px 0; }
.tr-stack-seg:only-child  { border-radius: 4px; }

.tr-meter-v { font-size: var(--fs-xl); font-weight: var(--fw-bold); line-height: var(--lh-tight); }
.tr-meter-track { height: 10px; margin: var(--space-3) 0 var(--space-2); background: var(--brand-tint); border-radius: 5px; overflow: hidden; }
.tr-meter-fill { height: 100%; background: var(--rt-navy-500); border-radius: 0 5px 5px 0; transition: width 380ms var(--ease-out); }
.tr-meter.is-low .tr-meter-track { background: var(--warning-bg); }
.tr-meter.is-low .tr-meter-fill  { background: var(--warning); }
.tr-meter-note { font-size: var(--fs-sm); color: var(--text-muted); }
.tr-none { padding: var(--space-6) 0; text-align: center; font-size: var(--fs-sm); color: var(--text-muted); }
.tr-more { margin-top: var(--space-2); font-size: var(--fs-xs); color: var(--text-muted); }

.tr-table { margin-top: var(--space-3); padding-top: var(--space-2); border-top: 1px solid var(--border); }
.tr-table summary {
  display: inline-flex; align-items: center; gap: 6px;
  list-style: none; cursor: pointer;
  font-size: var(--fs-xs); font-weight: var(--fw-medium); color: var(--text-muted);
}
.tr-table summary::-webkit-details-marker { display: none; }
.tr-table summary::after {
  content: ''; width: 5px; height: 5px;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg); transition: transform var(--transition);
}
.tr-table[open] summary::after { transform: rotate(45deg); }
.tr-table summary:hover { color: var(--text); }
.tr-table-body { max-height: 220px; overflow: auto; }
.tr-tbl { width: 100%; margin-top: var(--space-2); border-collapse: collapse; font-size: var(--fs-xs); }
.tr-tbl th { padding: 3px 0; text-align: left; font-weight: var(--fw-medium); color: var(--text-label); }
.tr-tbl td { padding: 3px 0; text-align: right; font-variant-numeric: tabular-nums; }
.tr-tbl tr + tr { border-top: 1px solid var(--border); }

/* One authored moment: a new period grows its bars from the baseline. */
.tr-plot.is-entering .tr-col { animation: tr-grow 380ms var(--ease-out) both; transform-box: fill-box; transform-origin: bottom; }
.tr-plot.is-entering .tr-line, .tr-plot.is-entering .tr-area, .tr-plot.is-entering .tr-donut { animation: wb-in 300ms var(--ease-out); }
@keyframes tr-grow { from { transform: scaleY(.1); } }

@media (max-width: 1100px) {
  .tr-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .tr-card.is-wide { grid-column: 1 / -1; }
}
@media (max-width: 640px) {
  .kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); }
  .kpi { padding: var(--space-3); gap: var(--space-2); }
  .kpi-icon { width: 26px; height: 26px; }
  .act-tabs { flex-wrap: wrap; overflow: visible; gap: 0; padding: var(--space-1) var(--space-3) 0; border-bottom: 0; }
  .act-tab { padding: var(--space-2); margin-bottom: 0; border-bottom: 1px solid var(--border); }
  .act-tab.is-on { border-bottom: 2px solid var(--brand); }
  .tr-grid { grid-template-columns: minmax(0, 1fr); }
  .tr-rank li { grid-template-columns: minmax(0, 6rem) minmax(0, 1fr) auto; }
}
@media (prefers-reduced-motion: reduce) {
  .kpi, .kpi:hover { transition: none; transform: none; }
  .act-panel:not([hidden]), .tr-plot.is-entering .tr-col, .tr-plot.is-entering .tr-line,
  .tr-plot.is-entering .tr-area, .tr-plot.is-entering .tr-donut { animation: none; }
  .tr-rank-fill, .tr-meter-fill { transition: none; }
}

/* ── "View as" (components/role-switch.php) — a developer's local switch ─────
   A drop-up in the sidebar footer; where there is no sidebar it floats top-right. Gold
   while viewing as someone else, so it is never forgotten that this is not
   your own view. */
.role-switch { position: relative; margin: 0 0 var(--space-1); }
.role-switch > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 6px;
  padding: 6px var(--space-3); border-radius: var(--radius-sm); font-size: var(--fs-xs);
  color: var(--rt-navy-300); border: 1px dashed rgba(255, 255, 255, 0.18); }
.role-switch > summary::-webkit-details-marker { display: none; }
.role-switch > summary strong { color: #fff; font-weight: 600; }
.role-switch > summary svg { margin-left: auto; }
.role-switch.is-viewing > summary { border-color: var(--rt-gold-500); color: var(--rt-gold-500); }
.role-switch-menu { position: absolute; left: 0; right: 0; bottom: calc(100% + 4px); z-index: 960;
  display: flex; flex-direction: column; padding: 4px; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); }
.role-switch-menu button { all: unset; box-sizing: border-box; padding: 7px 10px; border-radius: var(--radius-xs);
  font-size: var(--fs-sm); color: var(--text); cursor: pointer; }
.role-switch-menu button span { color: var(--text-muted); font-size: var(--fs-xs); }
.role-switch-menu button:hover, .role-switch-menu button:focus-visible { background: var(--surface-3); }
.role-switch-menu button.is-on { font-weight: 600; color: var(--accent); }
.role-switch-note { margin: 4px 10px 2px; font-size: var(--fs-xs); color: var(--text-muted); }
/* Pages with no sidebar (the driver app, "Access denied"): top-right, opening downward. */
.role-switch-float { position: fixed; top: 10px; right: 10px; z-index: 970; margin: 0; }
.role-switch-float > summary { background: var(--brand); border-style: solid; }
.role-switch-float .role-switch-menu { bottom: auto; top: calc(100% + 4px); left: auto; min-width: 180px; }

/* ── Printing an app page ─────────────────────────────────────────────────
   A report printed from inside the app shell (the leave report) drops the
   sidebar, the top bar and whatever it marks .no-print — its filters and
   buttons. The client's paper documents carry their own print CSS. */
@media print {
  .sidebar, .topbar, .no-print { display: none !important; }
  .main-wrapper { margin: 0 !important; }
  .main-content { padding: 0 !important; }
}

/* ── Documents for the driver (tasks/detail.php) ───────────────────────── */
.tdoc-add { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); margin-top: var(--space-3); }
.tdoc-add .upload-drop { flex: 1 1 320px; }

/* ── Price check (finance/price-check.php) ─────────────────────────────────
   One card per invoice waiting for approval: its lines beside the rate keyed on
   the trip, a line priced differently tinted, and the two answers below. */
.pc-sub { margin-top: 2px; font-size: var(--fs-sm); color: var(--text-muted); }
.pc-total { font-size: var(--fs-lg); font-weight: var(--fw-semibold); white-space: nowrap; }
.pc-head-right { display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-1); }
.pc-tick, .pc-batch-all { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--text); cursor: pointer; }
.pc-batch { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) var(--card-pad); }
.pc-card tr.pc-differs td { background: var(--warning-bg); }
.pc-warn { margin-top: var(--space-3); font-size: var(--fs-sm); color: var(--warning-text); }
.pc-actions { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--space-3); margin-top: var(--space-4); }
.pc-match { margin: 0; }
.pc-query > summary { list-style: none; cursor: pointer; }
.pc-query > summary::-webkit-details-marker { display: none; }
.pc-query[open] > summary { margin-bottom: var(--space-3); }
.pc-query form { display: grid; gap: var(--space-2); min-width: min(420px, 100%); }
.pc-query label { font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--text-label); }
.pc-query .btn { justify-self: start; }
.order-lorry { font-size: var(--fs-sm); color: var(--text-label); }

/* ── Touch fields: 16px, last in the file on purpose ──────────────────────
   Below 16px iOS Safari zooms the whole page the moment a field is focused,
   and does not zoom back out. Width alone is not the test — a phone in
   landscape is wider than 600px and zooms all the same — so a coarse pointer
   qualifies too. !important because a platform constraint has to beat every
   component that sizes its own field (a qty cell, a search box in a panel);
   this is the one place in the file that is allowed to. */
@media (max-width: 600px), (pointer: coarse) {
  input:not(:where([type="checkbox"], [type="radio"], [type="submit"], [type="button"],
                   [type="reset"], [type="file"], [type="range"], [type="color"],
                   [type="image"], [type="hidden"])),
  select, textarea { font-size: var(--fs-input-touch) !important; }
}
