/* =====================================================================
   JAIHIND CARRIERS PRIVATE LIMITED — minimalist futuristic theme
   Design principles baked in:
   - Light mode is the reference. Dark mode is its exact inverse.
   - One accent color. Amber/green appear ONLY as status.
   - Hairline borders. No shadows-as-decoration. No gradients.
   - Typography carries the personality, not color.
   The default colour scheme is 'jcpl', the company's own navy and orange
   (see the JCPL block below); the others stay on offer in the picker.
   ===================================================================== */

:root {
  --sans: -apple-system, "Inter", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "SFMono-Regular", "IBM Plex Mono", Menlo, Consolas, monospace;
  /* The brand MARK colour: the top bar's rule, the keyboard focus ring, the
     dashboard tile's hover corner and the sign-in card's line. It is the accent
     itself in every scheme except JCPL, where the accent (links, buttons) is the
     logo's navy and this is the logo's orange chevron. Kept as its own token so
     orange marks the brand without every link and button turning orange. */
  --accent-2: var(--accent);
}

/* ---------------------------------------------------------------- light (default) */
:root, [data-theme="light"] {
  --bg:            #FAFAFB;
  --bg-elevated:   #FFFFFF;
  --panel:         #FFFFFF;
  --panel-quiet:   #F4F5F7;
  --border:        #E5E7EB;
  --border-strong: #D1D5DB;
  --text:          #111318;
  --text-dim:      #6B7280;
  --text-faint:    #9CA3AF;

  --accent:        #4F46E5;   /* electric indigo — the ONE accent */
  --accent-ink:    #FFFFFF;
  --accent-quiet:  #EEF2FF;
  --accent-line:   #C7D2FE;

  --status-pending: #B45309;  /* amber, only for status */
  --status-pending-bg: #FEF3C7;
  --status-ok:      #047857;  /* emerald, only for status */
  --status-ok-bg:   #D1FAE5;
  --status-danger:  #B91C1C;
  --status-danger-bg: #FEE2E2;
}

/* ---------------------------------------------------------------- dark */
[data-theme="dark"] {
  --bg:            #0B0C0F;
  --bg-elevated:   #131519;
  --panel:         #131519;
  --panel-quiet:   #1A1D23;
  --border:        #23272F;
  --border-strong: #333944;
  --text:          #F3F4F6;
  --text-dim:      #9CA3AF;
  --text-faint:    #6B7280;

  --accent:        #818CF8;   /* same indigo, lighter step for dark bg */
  --accent-ink:    #0B0C0F;
  --accent-quiet:  #1E1B3B;
  --accent-line:   #3730A3;

  --status-pending: #FBBF24;
  --status-pending-bg: #3B2C0B;
  --status-ok:      #34D399;
  --status-ok-bg:   #0E2A20;
  --status-danger:  #F87171;
  --status-danger-bg: #2A1010;
}

/* ---------------------------------------------------------------------
   Color schemes — swap the accent only. Status colors (amber/green/red)
   never change with scheme; they mean something (pending/complete/
   danger) and staying constant keeps that meaning legible regardless
   of which accent the person picked. Indigo is the default and needs
   no override block since it's what :root already defines.
   --------------------------------------------------------------------- */

/* Emerald */
[data-theme="light"][data-scheme="emerald"] { --accent: #059669; --accent-ink: #FFFFFF; --accent-quiet: #ECFDF5; --accent-line: #6EE7B7; }
[data-theme="dark"][data-scheme="emerald"]  { --accent: #34D399; --accent-ink: #052e22; --accent-quiet: #0f2a20; --accent-line: #0f766e; }

/* Amber */
[data-theme="light"][data-scheme="amber"] { --accent: #D97706; --accent-ink: #FFFFFF; --accent-quiet: #FFFBEB; --accent-line: #FCD34D; }
[data-theme="dark"][data-scheme="amber"]  { --accent: #FBBF24; --accent-ink: #2b1c05; --accent-quiet: #2b1c05; --accent-line: #92400e; }

/* Rose */
[data-theme="light"][data-scheme="rose"] { --accent: #BE123C; --accent-ink: #FFFFFF; --accent-quiet: #FFF1F2; --accent-line: #FDA4AF; }
[data-theme="dark"][data-scheme="rose"]  { --accent: #FB7185; --accent-ink: #3a0712; --accent-quiet: #2a0a10; --accent-line: #9f1239; }

/* Sky */
[data-theme="light"][data-scheme="sky"] { --accent: #0284C7; --accent-ink: #FFFFFF; --accent-quiet: #F0F9FF; --accent-line: #7DD3FC; }
[data-theme="dark"][data-scheme="sky"]  { --accent: #38BDF8; --accent-ink: #062331; --accent-quiet: #062331; --accent-line: #0369A1; }

/* Claude — warm cream paper with a terracotta accent. Unlike the four
   schemes above (which only swap the accent), this one also shifts
   background/panel/text tones for a genuinely different warm feel. */
[data-theme="light"][data-scheme="claude"] {
  --bg: #FAF9F5;
  --bg-elevated: #FFFFFF;
  --panel: #FFFFFF;
  --panel-quiet: #F4F0E8;
  --border: #E8E2D5;
  --border-strong: #D9D0BE;
  --text: #2B2721;
  --text-dim: #6B6459;
  --text-faint: #9C9484;
  --accent: #CC785C;
  --accent-ink: #FFFFFF;
  --accent-quiet: #F5E6DE;
  --accent-line: #E3B7A4;
}
[data-theme="dark"][data-scheme="claude"] {
  --bg: #1A1714;
  --bg-elevated: #211D19;
  --panel: #211D19;
  --panel-quiet: #29241F;
  --border: #3A332C;
  --border-strong: #4A4137;
  --text: #F0EBE2;
  --text-dim: #B0A79A;
  --text-faint: #7D7468;
  --accent: #E0906F;
  --accent-ink: #1A1714;
  --accent-quiet: #3A241C;
  --accent-line: #6B4433;
}

/* JCPL — Jaihind Carriers Private Limited, the DEFAULT scheme (11 Oct 2026).
   Every value is a token of the company's own website (navy #0E2351 family,
   orange #EB6F22 family), so the app, the website and the logo agree.
   Light: navy-tinted paper; navy-700 for links and buttons (11:1 on white,
   and clearly blue beside the near-black body text); the logo orange is kept
   for brand marks only, through --accent-2. Dark: the website footer's deep
   navy, with orange as the accent — navy cannot carry contrast on navy. */
[data-theme="light"][data-scheme="jcpl"] {
  --bg: #F5F7FB;
  --bg-elevated: #FFFFFF;
  --panel: #FFFFFF;
  --panel-quiet: #F3F6FB;
  --border: #E2E8F0;
  --border-strong: #D3DCE8;
  --text: #0F1B33;
  --text-dim: #5D6B7F;
  --text-faint: #8A97AB;
  --accent: #1A3A7C;
  --accent-ink: #FFFFFF;
  --accent-quiet: #E6ECF6;
  --accent-line: #C9D4E8;
  --accent-2: #EB6F22;
}
[data-theme="dark"][data-scheme="jcpl"] {
  --bg: #071430;
  --bg-elevated: #0A1A3D;
  --panel: #0A1A3D;
  --panel-quiet: #0F2149;
  --border: #1B2D57;
  --border-strong: #2A4072;
  --text: #F3F6FB;
  --text-dim: #9FB0CF;
  --text-faint: #6D7FA3;
  --accent: #EB6F22;
  --accent-ink: #071430;
  --accent-quiet: #132C63;
  --accent-line: #A3420A;
  --accent-2: #EB6F22;
}

/* ---------------------------------------------------------------------
   Two additional full THEME MODES (not just accent swaps like the
   schemes above) — picked from the theme popover alongside Light/Dark.
   Each defines its own complete bg/panel/border/text token set, plus a
   default accent below; the 6 accent-scheme swatches also apply here
   (see the theme+scheme combination rules further down) exactly like
   they do for Light/Dark.
   --------------------------------------------------------------------- */

/* Sepia — warm parchment paper, low glare for long stretches of reading
   invoices/ledgers. Warm-brown by default; also takes any accent scheme. */
[data-theme="sepia"] {
  --bg:            #F3E9D2;
  --bg-elevated:   #FBF5E7;
  --panel:         #FBF5E7;
  --panel-quiet:   #ECDFC0;
  --border:        #DBCA9F;
  --border-strong: #C6B27E;
  --text:          #3A2E1C;
  --text-dim:      #6B5A3B;
  --text-faint:    #998660;

  --accent:        #8B5E2B;
  --accent-ink:    #FFFFFF;
  --accent-quiet:  #EADFC2;
  --accent-line:   #C9A567;

  --status-pending: #92400E;
  --status-pending-bg: #F6DFAE;
  --status-ok:      #14532D;
  --status-ok-bg:   #D3E9D3;
  --status-danger:  #7F1D1D;
  --status-danger-bg: #F0CFCB;
}

/* High Contrast — pure black/white with a strong blue accent, for
   maximum legibility (bright light, low vision, projector screens). */
[data-theme="contrast"] {
  --bg:            #FFFFFF;
  --bg-elevated:   #FFFFFF;
  --panel:         #FFFFFF;
  --panel-quiet:   #ECECEC;
  --border:        #000000;
  --border-strong: #000000;
  --text:          #000000;
  --text-dim:      #1A1A1A;
  --text-faint:    #3D3D3D;

  --accent:        #0000CC;
  --accent-ink:    #FFFFFF;
  --accent-quiet:  #DCDCFF;
  --accent-line:   #0000CC;

  --status-pending: #7A4B00;
  --status-pending-bg: #FFE8B0;
  --status-ok:      #005A20;
  --status-ok-bg:   #C8F5D6;
  --status-danger:  #B30000;
  --status-danger-bg: #FFD1D1;
}

/* The 6 accent-color swatches (Indigo/Emerald/Amber/Rose/Sky/Claude) also
   apply on top of Sepia and Contrast, not just Light/Dark — both have
   light/pale page backgrounds, so the same accent hex values already
   tuned for the Light theme read fine here too. Without these, picking
   a color scheme while on Sepia or Contrast silently did nothing, since
   no rule matched that specific theme+scheme pairing. */
[data-theme="sepia"][data-scheme="indigo"],
[data-theme="contrast"][data-scheme="indigo"] { --accent: #4F46E5; --accent-ink: #FFFFFF; --accent-quiet: #EEF2FF; --accent-line: #C7D2FE; }

[data-theme="sepia"][data-scheme="emerald"],
[data-theme="contrast"][data-scheme="emerald"] { --accent: #059669; --accent-ink: #FFFFFF; --accent-quiet: #ECFDF5; --accent-line: #6EE7B7; }

[data-theme="sepia"][data-scheme="amber"],
[data-theme="contrast"][data-scheme="amber"] { --accent: #D97706; --accent-ink: #FFFFFF; --accent-quiet: #FFFBEB; --accent-line: #FCD34D; }

[data-theme="sepia"][data-scheme="rose"],
[data-theme="contrast"][data-scheme="rose"] { --accent: #BE123C; --accent-ink: #FFFFFF; --accent-quiet: #FFF1F2; --accent-line: #FDA4AF; }

[data-theme="sepia"][data-scheme="sky"],
[data-theme="contrast"][data-scheme="sky"] { --accent: #0284C7; --accent-ink: #FFFFFF; --accent-quiet: #F0F9FF; --accent-line: #7DD3FC; }

[data-theme="sepia"][data-scheme="claude"],
[data-theme="contrast"][data-scheme="claude"] { --accent: #CC785C; --accent-ink: #FFFFFF; --accent-quiet: #F5E6DE; --accent-line: #E3B7A4; }

/* JCPL on Sepia / High contrast: the light navy accent, and the darker
   orange-600 for brand marks (the focus ring must still read on white). */
[data-theme="sepia"][data-scheme="jcpl"],
[data-theme="contrast"][data-scheme="jcpl"] { --accent: #1A3A7C; --accent-ink: #FFFFFF; --accent-quiet: #E6ECF6; --accent-line: #C9D4E8; --accent-2: #C4500C; }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}

@keyframes fadeInUp {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes pulseDot {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}
@keyframes slideDown {
  from { opacity: 0; transform: translateY(-6px); }
  to { opacity: 1; transform: translateY(0); }
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: 17.5px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  transition: background 0.2s ease, color 0.2s ease;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }

:focus-visible {
  outline: 2px solid var(--accent-2);
  outline-offset: 2px;
  border-radius: 4px;
}

/* Typography — one family, careful sizing and tracking do the work */
h1 { font-size: 1.5rem; font-weight: 600; margin: 0 0 4px; letter-spacing: -0.015em; }
h2 { font-size: 0.85rem; font-weight: 500; color: var(--text-dim); margin: 0 0 14px; letter-spacing: 0.02em; }

/* Tiny uppercase labels are the ONLY all-caps element in the design */
.label, label {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-dim);
}
.mono { font-family: var(--mono); }

/* ---------------------------------------------------------------- topbar */
.topbar {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px 18px;
  flex-wrap: wrap;
  padding: 14px 32px;
  background: var(--bg-elevated);
  border-bottom: 1px solid var(--border);
}
/* Brand + menu/back controls: a fixed cluster that never shrinks.
   Row 1 holds the brand cluster (left) and the account/theme controls
   (right); the module nav drops to its own full-width row beneath, so
   neither the brand nor the account controls ever get shoved around by
   the number of module shortcuts. */
.topbar-left { display: flex; align-items: center; gap: 14px; flex-shrink: 0; order: 1; }
.topbar::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: -1px; height: 2px;
  background: var(--accent-2);
  opacity: 0.55;
}
/* Company colours: the orange rule at full strength, as on the website's nav. */
[data-scheme="jcpl"] .topbar::after { opacity: 1; height: 3px; }
.brand-mark {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--text);
  font-weight: 600;
  font-size: 1.05rem;
  letter-spacing: -0.01em;
  line-height: 0;
}
/* The logo (partials/brand-logo.ejs). 40px tall keeps "JAIHIND CARRIERS"
   under the mark readable while adding only ~13px to the bar. Exactly one
   ink shows: the white one is made for dark backgrounds. */
.brand-logo { height: 40px; width: auto; display: block; }
.logo-ink-dark { display: none; }
[data-theme="dark"] .logo-ink-light { display: none; }
[data-theme="dark"] .logo-ink-dark { display: block; }
.brand-mark:hover { text-decoration: none; }
.topbar .who {
  color: var(--text-dim);
  font-size: 0.85rem;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 14px;
  flex-wrap: wrap;
  flex-shrink: 0;
  order: 2;
  margin-left: auto;
}
/* Module shortcuts in the top bar — one-click jumps between LR/Memo/
   Billing/etc. without going back to Dashboard first. Filtered by
   per-user permission on the server (res.locals.navModules). Sits in
   the middle, taking the slack between the brand cluster (left) and the
   account/theme controls (right), and wraps its own chips when tight. */
.topbar-nav {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
  order: 3;
  flex-basis: 100%;
  justify-content: flex-start;
  min-width: 0;
  margin-top: 4px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}
.topbar-nav-link {
  color: var(--text-dim);
  font-size: 0.85rem;
  text-decoration: none;
  padding: 4px 10px;
  border-radius: 6px;
  border: 1px solid transparent;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.topbar-nav-link:hover {
  color: var(--accent);
  background: var(--accent-quiet);
  border-color: var(--accent-line);
  text-decoration: none;
}
.topbar .who a { color: var(--text-dim); }
.topbar .who a:hover { color: var(--accent); text-decoration: none; }
.topbar .who .mono { color: var(--text); }
/* THE NOTIFICATIONS LINK (23 Oct 2026): a bell, the word, and a red count while
   a truck needs attention - the alerts moved off the dashboard to it. */
.topbar .who .notif-link { display: inline-flex; align-items: center; gap: 4px; }
.notif-bell { width: 16px; height: 16px; flex: 0 0 auto; }
.topbar .who .notif-link.has { color: var(--status-danger); font-weight: 600; }
.notif-count {
  display: inline-block; min-width: 18px; padding: 0 5px; border-radius: 9px;
  background: var(--status-danger); color: #fff; font-size: 0.75rem; font-weight: 700;
  line-height: 18px; text-align: center;
}
.topbar form { display: inline; }
.topbar button.link {
  background: none; border: none; color: var(--text-dim); cursor: pointer;
  font-size: 0.85rem; padding: 0; font-family: var(--sans);
}
.topbar button.link:hover { color: var(--accent); }

/* Theme toggle — minimal circle, one line.
   Border uses --text-dim (not --border-strong): the hairline border tokens
   are too close to --bg/--bg-elevated (≈1.4:1 contrast) to read as a
   clickable boundary on their own — fine for passive dividers, not enough
   for a control someone needs to spot and click. --text-dim clears 4.5:1+
   against the page background in every theme/scheme. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px; height: 32px; padding: 0;
  border-radius: 50%;
  border: 1px solid var(--text-dim);
  background: var(--panel-quiet);
  color: var(--text-dim);
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease, transform 0.2s ease;
}
.theme-toggle:hover { border-color: var(--accent); color: var(--accent); transform: rotate(15deg); }
/* flex-shrink: 0 — without it, an SVG with no width/height attributes
   sitting in an inline-flex button collapses to 0 width (height renders
   fine, width doesn't) because flex items default to min-width:auto,
   and an unsized SVG's automatic minimum width resolves to 0. This is
   what made the icon look "empty" — the button and its border were
   visible, the icon inside had a real box height but zero width. */
.theme-toggle svg { width: 14px; height: 14px; flex-shrink: 0; transition: transform 0.3s ease; }

/* animation-fill-mode is deliberately NOT "both"/"forwards" (16 Aug 2026,
   found while live-verifying sticky table headers). "forwards" holds the
   animation's `to` keyframe forever after it finishes — here that's
   `transform: translateY(0)`, which LOOKS like a no-op but is not: any
   computed transform other than the literal keyword `none` permanently
   makes its element a new containing block, which silently breaks
   `position: sticky` for every descendant, on every page, since .container
   wraps the entire content area everywhere in the app. This was sitting
   underneath every part of Phase 1's sticky-header work and made it
   inert regardless of how correct the sticky rule itself was — confirmed
   live: a table's own <th> reported `position: sticky` correctly and
   still tracked the page scroll 1:1, because its containing-block chain
   was pinned by this transform above it. Dropping the fill-mode back to
   the default (`none`) costs nothing visually — the `to` keyframe
   (opacity:1, translateY(0)) already matches the element's un-animated
   base state exactly, and there is no animation-delay for "backwards" to
   have covered either — so the fade-in on page load looks identical, it
   just stops leaving a transform behind once it's done. .stub below had
   the same bug for the same reason and got the same fix. */
.container { max-width: 1080px; margin: 0 auto; padding: 40px 32px 64px; animation: fadeInUp 0.35s ease; }
.container.narrow { max-width: 440px; }
/* Data-table screens (see partials/header.ejs). Wide enough to give a
   12-column table real room on a 1265px monitor, still capped so an ultrawide
   display doesn't stretch rows to unreadable lengths. */
.container.wide { max-width: 1480px; }

/* ---------------------------------------------------------------- sidebar (setup/admin) */
.sidebar-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px; height: 32px; padding: 0;
  border-radius: 6px;
  border: 1px solid var(--text-dim);
  background: var(--panel-quiet);
  color: var(--text-dim);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.sidebar-toggle:hover { background: var(--panel-quiet); color: var(--text); }
.sidebar-toggle svg { width: 18px; height: 18px; flex-shrink: 0; }

.sidebar-overlay {
  position: fixed; inset: 0;
  background: rgba(10, 12, 15, 0.35);
  opacity: 0; pointer-events: none;
  transition: opacity 0.2s ease;
  z-index: 40;
}
.sidebar-overlay.open { opacity: 1; pointer-events: auto; }

.sidebar-panel {
  position: fixed; top: 0; left: 0; bottom: 0;
  width: 280px;
  background: var(--bg-elevated);
  border-right: 1px solid var(--border);
  transform: translateX(-100%);
  transition: transform 0.22s ease;
  z-index: 50;
  display: flex;
  flex-direction: column;
}
.sidebar-panel.open { transform: translateX(0); }
.sidebar-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 18px 18px 14px;
  font-weight: 600;
  font-size: 0.95rem;
  border-bottom: 1px solid var(--border);
}
.sidebar-close {
  background: none; border: none; color: var(--text-dim); cursor: pointer;
  width: 28px; height: 28px; padding: 0; display: inline-flex; align-items: center; justify-content: center;
  border-radius: 6px;
}
.sidebar-close:hover { background: var(--panel-quiet); color: var(--text); }
.sidebar-close svg { width: 15px; height: 15px; flex-shrink: 0; }
.sidebar-list { padding: 10px; overflow-y: auto; }
.sidebar-loading { color: var(--text-faint); font-size: 0.85rem; padding: 12px; }
.sidebar-item {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px;
  padding: 11px 12px;
  border-radius: 6px;
  color: var(--text);
  font-size: 0.92rem;
  transition: background 0.12s ease;
}
a.sidebar-item:hover { background: var(--panel-quiet); text-decoration: none; color: var(--accent); }
.sidebar-item.disabled { color: var(--text-faint); }
.sidebar-item .soon { font-size: 0.68rem; color: var(--status-pending); text-transform: uppercase; letter-spacing: 0.04em; }

/* ---------------------------------------------------------------- color scheme picker */
.scheme-picker { position: relative; display: inline-flex; }
.scheme-popover {
  position: absolute; top: calc(100% + 8px); right: 0;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px;
  display: none;
  gap: 8px;
  box-shadow: 0 10px 30px -10px rgba(0,0,0,0.25);
  z-index: 60;
}
.scheme-popover.open { display: flex; }
.scheme-swatch {
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--sw);
  cursor: pointer;
  border: 2px solid transparent;
  transition: transform 0.12s ease, border-color 0.12s ease;
}
.scheme-swatch:hover { transform: scale(1.15); }
.scheme-swatch.active { border-color: var(--text); }

/* ---------------------------------------------------------------- theme picker (Light/Dark/Sepia/Contrast) */
.theme-popover-list { flex-direction: column; min-width: 150px; gap: 2px; }
.theme-option {
  display: flex; align-items: center; gap: 9px;
  padding: 7px 10px; border-radius: 6px; border: none; background: none;
  color: var(--text); font-size: 0.85rem; text-align: left; cursor: pointer; width: 100%;
}
.theme-option:hover { background: var(--panel-quiet); }
.theme-option.active { color: var(--accent); font-weight: 600; }
.theme-dot { width: 11px; height: 11px; border-radius: 50%; flex-shrink: 0; border: 1px solid var(--border-strong); background: var(--td); }

/* ---------------------------------------------------------------- panels */
.stub {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 18px 20px;
  /* Same transform-breaks-sticky fix as .container above, and the same
     reasoning: no visual difference, just stops pinning a transform. */
  animation: fadeInUp 0.3s ease;
}
.stat-card { border-top: 3px solid var(--accent-line); }

/* ---------------------------------------------------------------- module grid
   Square hexagonal-ish tiles: rounded-corner squares with a diagonal
   accent stripe in one corner. The stripe is what lifts these out of
   plain rectangles without needing an actual polygon clip-path (which
   would break the "click to reveal description" popover inside). */
.module-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 14px;
  margin-top: 12px;
}
.module-card {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  aspect-ratio: 1 / 1;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 18px 16px 14px;
  color: var(--text);
  position: relative;
  overflow: hidden;
  transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
/* Diagonal accent flag in the top-left corner — the "signature" that
   replaces the plain-rectangle look with something more crafted. */
.module-card::before {
  content: "";
  position: absolute;
  top: -18px; left: -18px;
  width: 36px; height: 36px;
  background: var(--accent-line);
  transform: rotate(45deg);
  transition: background 0.15s ease;
}
.module-card:hover {
  border-color: var(--accent);
  text-decoration: none;
  transform: translateY(-2px);
  box-shadow: 0 10px 22px -10px var(--accent-line);
}
.module-card:hover::before { background: var(--accent-2); }
.module-card:hover .module-title::after { transform: scaleX(1); }

.module-card .module-title {
  font-size: 0.95rem;
  font-weight: 600;
  margin: 0 0 4px;
  padding-right: 20px; /* clear the "i" button */
  color: var(--text);
  position: relative;
  display: inline-block;
  letter-spacing: -0.005em;
  line-height: 1.25;
}
.module-card .module-title::after {
  content: "";
  position: absolute;
  left: 0; width: 100%; bottom: -3px;
  height: 1px;
  background: var(--accent-2);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.2s ease;
}
.module-card .access-row {
  display: flex; gap: 12px; align-items: center;
  font-size: 0.8rem;
  color: var(--text-faint);
  margin-top: auto;
}
.module-card .access-row .coming {
  margin-left: auto;
  color: var(--status-pending);
}

/* "i" info button — smaller, tighter footprint */
.module-card .module-info {
  position: absolute;
  top: 8px; right: 8px;
  width: 16px; height: 16px;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  background: var(--panel-quiet);
  color: var(--text-dim);
  font-family: Georgia, "Times New Roman", serif;
  font-style: italic;
  font-size: 0.7rem;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  display: flex; align-items: center; justify-content: center;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
  z-index: 2;
}
.module-card .module-info:hover,
.module-card .module-info.active {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-quiet);
}
.module-card .module-desc-pop {
  display: none;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px dashed var(--border-strong);
  color: var(--text-dim);
  font-size: 0.8rem;
  line-height: 1.35;
}
.module-card .module-desc-pop.open { display: block; }

/* ---------------------------------------------------------------- forms */
label { display: block; margin: 14px 0 5px; }
label:first-child { margin-top: 0; }

input[type=text], input[type=password], input[type=email], input[type=number], input[type=date],
input[type=tel], input[type=datetime-local], select, textarea {
  width: 100%;
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  color: var(--text);
  padding: 10px 12px;
  font-size: 0.93rem;
  font-family: var(--sans);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-quiet);
}
input[type=checkbox] { width: auto; accent-color: var(--accent); }

/* Native number-input spin buttons add visual clutter without much
   value for currency/weight entry — hidden everywhere. Inputs use
   step="any" so this doesn't block typing arbitrary decimals. */
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
input[type=number] { -moz-appearance: textfield; }

button, .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  color: var(--accent-ink);
  border: 1px solid var(--accent);
  border-radius: 6px;
  padding: 9px 16px;
  font-weight: 500;
  font-size: 0.94rem;
  cursor: pointer;
  font-family: var(--sans);
  transition: background 0.12s ease, border-color 0.12s ease;
}
button:hover, .btn:hover { background: var(--text); border-color: var(--text); color: var(--bg); text-decoration: none; transform: translateY(-1px); }
button:active, .btn:active { transform: translateY(0) scale(0.97); }
button.secondary, .btn.secondary {
  background: transparent;
  color: var(--text);
  border: 1px solid var(--border-strong);
}
button.secondary:hover, .btn.secondary:hover {
  background: var(--panel-quiet);
  border-color: var(--text-dim);
  color: var(--text);
}

/* TWO COMPACT BUTTON SIZES, shared (17 Aug 2026). Reported against CA
   Reports' "Export CSV", which measured 40px tall around a 13.7px font while
   the Apply button beside it was 28px and the A−/A/A+ controls 26px.

   THE CAUSE IS ALWAYS THE SAME and it is worth recognising on sight: a
   button given a smaller font-size INLINE, with the base rule's `padding:
   9px 16px` left untouched. Shrinking the text does not shrink the box, so
   the label ends up floating in a button sized for a much bigger one. Seven
   buttons across the app were built that way. It is the same shape of bug as
   the phone-tier `.hint-toggle` inflation (P15) — padding, not the declared
   size, deciding the box.

   So the sizes are NAMED and carry their own padding, rather than each call
   site inventing a font-size:
     .btn-sm    ~28px — matches .filter-bar's submit and .pager-btn. For an
                 action in a toolbar or a table cell.
     .btn-tiny  ~21px — for several actions stacked inside ONE table row.
   `.btn-tiny` is not new: it was defined in billTracker/index.ejs's own
   page-local <style> and therefore silently did nothing on any other screen
   that tried to use it. Moved here so it works everywhere and there is one
   definition of what "tiny" means.

   Ratio to aim for if a third size is ever added: box height / font-size
   around 1.9-2.0, which is what every already-tightened control in this app
   measures. The base button sits at 2.7 deliberately — it is the page's
   primary action, not a row action. */
button.btn-sm, .btn.btn-sm {
  padding: 4px 10px; font-size: 0.82rem; line-height: 1.4;
}
button.btn-tiny, .btn.btn-tiny {
  padding: 2px 7px; font-size: 0.74rem; line-height: 1.35;
}
button.danger { background: var(--status-danger); border-color: var(--status-danger); color: white; }
/* Dark theme's danger red is a light step (#F87171); white on it read 2.8:1 (1 Nov 2026). */
[data-theme="dark"] button.danger { color: #1A0606; }
/* button.link is the same thing under another name (1 Nov 2026): thirteen
   buttons across the app - Stop the link, Track, Remove, Hide/Show, Tally
   rules' Save/Delete - were written class="link", but the only rule for it
   was scoped to .topbar, so each one drew as a filled orange button (a red
   "Remove" on orange read at 1.1:1). The top bar's own rule below still wins
   there. An inline color (danger) keeps working on top of this. */
button.link-btn, button.link {
  background: none; border: none; color: var(--accent);
  cursor: pointer; padding: 0; font: inherit;
  font-size: inherit;
  text-transform: none; letter-spacing: normal;
}
button.link-btn:hover, button.link:hover { text-decoration: underline; text-underline-offset: 3px; }

.form-error {
  background: var(--status-danger-bg);
  border: 1px solid var(--status-danger);
  color: var(--status-danger);
  padding: 10px 14px;
  border-radius: 6px;
  font-size: 0.88rem;
  margin-bottom: 18px;
  animation: slideDown 0.25s ease both;
}
/* The confirmation counterpart to .form-error — same box, the ok colour.
   Added with the two-factor screens, which are the first place in the app that
   needed to say "that worked" rather than only "that failed". Uses the reserved
   status palette, which is exactly what that palette is for (unlike a chart
   series — see the note in billTracker/_dashboard.ejs). */
.form-success {
  background: var(--status-ok-bg);
  border: 1px solid var(--status-ok);
  color: var(--status-ok);
  padding: 10px 14px;
  border-radius: 6px;
  font-size: 0.88rem;
  margin-bottom: 18px;
  animation: slideDown 0.25s ease both;
}
.form-hint { color: var(--text-dim); font-size: 0.84rem; margin-top: 6px; }

/* SAVE DRAFT (public/js/draft-save.js). The bar announces a saved draft at the
   top of the form and waits — deliberately NOT the ok colour, because nothing has
   been saved to the business: this is a scratch copy in this browser, and dressing
   it as a success would say otherwise. .draft-note is the line beside the button,
   which is where the operator is looking when they press it. */
.draft-bar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  background: var(--panel-quiet); border: 1px dashed var(--border-strong);
  color: var(--text); padding: 8px 12px; border-radius: 6px;
  font-size: 0.86rem; margin-bottom: 16px;
}
.draft-bar[hidden] { display: none; }
.draft-bar button { padding: 3px 10px; font-size: 0.8rem; margin: 0; }
.draft-note { color: var(--text-dim); font-size: 0.82rem; margin-left: 8px; }

/* Per-field tips (format/validation notes tied to one specific input) —
   hidden by default so a dense form doesn't read as a wall of small
   grey text. Revealed by clicking the small ⓘ toggle at the end of the
   field's label (see .hint-toggle) — NOT on hover: a hover-reveal here
   made the field's own hint pop in under the cursor, growing the
   wrapper and shifting layout right under the mouse, which retriggered
   hover on the next element and made the page visibly wobble. A click
   is a deliberate, position-independent action, so that loop can't
   happen. */
.field-hint { color: var(--text-dim); font-size: 0.84rem; margin-top: 6px; display: none; }
.field-hint.shown { display: block; }

.hint-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; padding: 0; margin-left: 4px;
  background: transparent; border: none; border-radius: 50%;
  color: var(--text-faint); font-size: 0.72rem; line-height: 1;
  cursor: pointer; vertical-align: middle;
}
.hint-toggle:hover { color: var(--accent); }

/* Floating popover variant of the same ⓘ button, used wherever a hint
   sits inside a heading/table-cell/label where a block-level reveal
   would shift surrounding layout (see hint-toggle.js case 2). */
.hint-popover {
  display: none;
  position: absolute;
  top: 100%; left: 0;
  margin-top: 6px;
  background: var(--bg-elevated, var(--bg));
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 0.8rem;
  font-weight: 400;
  line-height: 1.4;
  text-transform: none;
  letter-spacing: normal;
  color: var(--text-dim);
  width: max-content;
  max-width: 260px;
  /* ABOVE the typeahead dropdown panel, not level with it. Both used to sit
     at 60, and a tie is resolved by DOM order — which meant "does the hint
     or the dropdown win" depended on which happened to be written later in
     the page, i.e. on nothing meaningful. A hint is always the more
     transient of the two, so it wins outright. Still well under the modal
     layers (500/900/1200). */
  z-index: 120;
  box-shadow: 0 4px 14px rgba(0,0,0,0.18);
}
.hint-popover.shown { display: block; }
/* Opens UPWARD when opening downward would land on top of a form control —
   see hint-toggle.js's wouldCoverAControl() for the measurement and why
   only form controls count. The shadow flips with it so the box still
   reads as sitting above the page rather than under it. */
.hint-popover.above {
  top: auto;
  bottom: 100%;
  margin-top: 0;
  margin-bottom: 6px;
  box-shadow: 0 -4px 14px rgba(0,0,0,0.18);
}

/* Compact icon-style button for row-remove actions in dense tables —
   a full-size "Remove" button reads as too heavy repeated per row. */
.btn-remove-row {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0;
  background: transparent; border: 1px solid var(--border-strong);
  border-radius: 5px; color: var(--text-dim);
  font-size: 0.9rem; line-height: 1; cursor: pointer;
}
.btn-remove-row:hover { background: var(--status-danger-bg); border-color: var(--status-danger); color: var(--status-danger); transform: none; }

/* THE CUSTOM TYPEAHEAD DROPDOWN (14 Aug 2026, later) — replaces the native
   <input list="..."> popup datalist-typeahead.js used to lean on entirely.
   That popup opens on focus even with nothing typed, which is a browser
   default with no CSS/JS hook to suppress selectively, so the input's own
   `list` attribute is removed by that script and this markup (a wrapper +
   arrow + panel) stands in for it instead. See the .js file's header for
   the full reasoning.

   .dt-typeahead-wrap is DELIBERATELY GIVEN NO WIDTH OF ITS OWN. Every field
   this applies to already has its width driven by a descendant selector
   somewhere in this file (the grid-cell `input[type=text] { width:100% }`
   default, `.lr-filters input[type="text"] { width:120px }`,
   `.actions-row .search-form input { width:240px }`, and the mobile
   `.actions-row .search-form > * { flex:1 1 45% }` rule) — every one of
   those still matches straight through the wrapper (descendant/child
   selectors don't care about the extra level), so the wrapper is left to
   size itself off the input it contains rather than duplicating any of
   those numbers here, which would only drift out of sync with them.

   THE ARROW IS ANCHORED TO THE INPUT, NOT TO THE WRAPPER (14 Aug, last).
   It used to be absolutely positioned at `right: 3px` of the wrapper, on the
   assumption the two are always the same width. Measured across every screen
   after select-backed boxes started being wrapped too, that assumption broke
   in exactly one place and broke badly: the line-items UNIT picker, where
   `.line-items-table`'s own rule gives the input 64px inside a 160px cell, so
   a wrapper filling the cell put the arrow ~96px to the RIGHT of the field it
   belongs to. The wrapper legitimately has to keep filling its container —
   that is what makes a `width:100%` input fill a grid cell — so the ARROW is
   what had to change: it is now an in-flow flex item pulled back over the
   input's own right edge with a negative margin, which tracks the input at
   whatever width any rule gives it, with no per-screen CSS.

   The margins are chosen to cancel exactly: -25 + 22 (its own width) + 3 = 0.
   A net zero contribution matters because in a shrink-to-fit context (the
   filter rows, where the wrapper is itself a flex item) the wrapper sizes to
   its content — so a non-zero arrow would make the wrapper a few px narrower
   than its own input, which is what the width audit checks for. */
.dt-typeahead-wrap { position: relative; display: flex; align-items: center; }
input[type=text].dt-typeahead-input { padding-right: 30px !important; }
/* ONE ARROW, AND IT IS THE BLACK ONE (14 Aug, even later — live report:
   "now each drop box have two arrow (black arrow, red arrow), use only
   arrow preferable black arrow ... in whole app"). The app really did grow
   two different arrows the moment the custom panel landed: a <select>
   enhanced by select-typeahead.js keeps its `list` attribute, so CHROME
   draws its own near-black indicator, while a text input enhanced by
   datalist-typeahead.js has `list` removed (that removal is the whole
   point — see that file) and needs this one drawn by hand. Two mechanisms
   is unavoidable; two APPEARANCES was not.

   So this is deliberately styled to read as the same black arrow Chrome
   already draws next to it — var(--text), the same near-black the page's
   own body copy uses, in both the normal and hover state. It used to be
   var(--text-faint) going var(--accent) on hover, which is where the red
   came from. Hover now says "this is a control" with the quiet background
   alone, exactly like .theme-toggle/.sidebar-toggle already do, rather
   than by changing colour and reintroducing a second-looking arrow.

   (Updated 14 Aug, last: there is now only ONE mechanism as well as one
   appearance — select-backed inputs lost their `list` attribute too, so
   Chrome draws no indicator anywhere in the app and this is the only
   dropdown arrow that exists. See datalist-typeahead.js's own header.)

   SELECTOR SPECIFICITY IS LOAD-BEARING HERE, and was got wrong the first
   time: `.dt-arrow` alone is (0,1,0), which LOSES to `.lr-filters button`
   (0,1,1) — the filter rows' own `height: 32px; padding: 4px 12px`. Measured
   live on the LR list: the arrow rendered 24x32 instead of 22x22 (padding
   12px+12px flooring the border-box width, the explicit 32px height winning
   outright), so its hover background painted a tall grey slab over the right
   end of the field. That is the SAME trap as the ⓘ button on 14 Aug two
   entries above — a generic `button` rule reaching a fixed-size icon button —
   and the third time this file has been bitten by it, so it is fixed the way
   that entry established: qualify the selector high enough to win outright
   (.dt-typeahead-wrap .dt-arrow is (0,2,0), above any `.x button`), and
   restate padding/height/min-height explicitly rather than trusting
   inheritance. */
.dt-typeahead-wrap .dt-arrow {
  position: relative; z-index: 1;
  flex: 0 0 auto;
  margin: 0 3px 0 -25px;
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; min-height: 0; padding: 0;
  background: transparent; border: none; border-radius: 4px;
  color: var(--text); font-size: 0.6rem; line-height: 1; cursor: pointer;
  white-space: nowrap;
}
.dt-typeahead-wrap .dt-arrow:hover { color: var(--text); background: var(--panel-quiet); }
.dt-panel {
  display: none;
  position: absolute; top: 100%; left: 0; right: 0; margin-top: 4px;
  background: var(--bg-elevated); border: 1px solid var(--border-strong);
  border-radius: 6px; box-shadow: 0 4px 14px rgba(0,0,0,0.18);
  max-height: 240px; overflow-y: auto; z-index: 60; padding: 4px 0;
}
.dt-panel.shown { display: block; }
/* Qty × Rate calculator under a Truck Rate box (qty-rate-calc.js, item 6).
   One compact line, deliberately smaller than the box it fills, so it reads
   as a helper rather than two more fields to complete. */
.qty-rate-calc { display: flex; align-items: center; gap: 6px; margin: 4px 0 6px; flex-wrap: wrap; }
.qty-rate-calc input[type="text"] { width: 90px; padding: 4px 6px; font-size: 0.84rem; }
.qty-rate-calc .qrc-op { color: var(--text-dim); font-weight: 600; }
.qty-rate-calc .qrc-eq { color: var(--text-faint); font-size: 0.78rem; }
/* SEARCH suggestions (search-suggest.js) size to their longest value rather
   than to the box: a filter-bar search box is ~130px, and a party name or a
   bank narration cut to that is unreadable. Capped, and never wider than the
   screen; the script shifts it left if a box near the right edge would push
   it off a phone. */
.dt-panel.ss-panel {
  right: auto; min-width: 100%; width: max-content;
  max-width: min(420px, calc(100vw - 32px));
}
.dt-option {
  padding: 7px 12px; font-size: 0.88rem; color: var(--text); cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dt-option:hover, .dt-option.active { background: var(--accent-quiet); }

/* Collapsed panels app-wide (public/js/disclosure-toggle.js) — replaces
   nine separate <details> uses, all of which measured broken on real
   pages: this app's target browser does not hide a closed <details>'s
   own content once any page needs a real `display` on it for when it IS
   open, because author CSS always wins over the UA stylesheet's own
   hiding rule. display:none here is the entire fix — see that file's own
   header for the full story. Deliberately the SAME shape as .dt-panel
   above and .hint-popover below: base rule display:none, a JS-owned
   .shown class is the only thing that ever turns it on. */
.disclosure-toggle {
  background: none; border: 0; padding: 4px 0; cursor: pointer;
  font: inherit; text-align: left;
  color: inherit;   /* not the filled button's ink: invisible on a dark panel (1 Nov 2026) */
}
.disclosure-body { display: none; }
.disclosure-body.shown { display: block; }
/* P44 (23 Oct 2026): the credit-limit line under a client select - shown only
   by credit-warning.js, never blocking. */
.credit-warn { display: none; margin-top: 4px; font-size: 0.84rem; font-weight: 600; color: var(--status-danger); }
.credit-warn.shown { display: block; }
/* A data-enhance table whose scroll box IS the disclosure body (Bill Tracker's
   "By party", the tracker dashboard's tables) gets its toolbar and pager as
   SIBLINGS of that body, so they stayed on screen with the box closed (12 Oct
   2026). They follow the box: hidden until the disclosure is open. */
.disclosure:not(.is-open) > .table-toolbar,
.disclosure:not(.is-open) > .table-pager { display: none; }

/* ---------------------------------------------------------------- tables */
table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 0.96rem; }

/* Dense data-entry tables (LR line items, Memo payment breakdown) need
   much tighter cell padding than a normal read-only table — the
   default 12px td padding plus full-size input padding left almost no
   room for digits, cutting off values like "8000"/"2000" mid-figure. */
.line-items-table, .memo-payment-table {
  table-layout: fixed;
}
.line-items-table td, .memo-payment-table td,
.line-items-table th, .memo-payment-table th {
  padding: 6px 5px;
}
.line-items-table input[type=text], .line-items-table input[type=number],
.memo-payment-table input[type=number] {
  padding: 7px 6px;
  font-size: 0.88rem;
  min-width: 0;
}
.line-items-table th, .memo-payment-table th {
  font-size: 0.68rem;
  white-space: normal;
  line-height: 1.25;
}
.line-items-table .mono, .memo-payment-table .mono { font-size: 0.88rem; }
/* THE DATE UNDER EACH MEMO AMOUNT (13 Aug 2026, Changes 1.docx). Smaller and
   dimmer than the amount above it, because the amount is the figure being
   settled and the date is its annotation — and because a full-size date input
   in a six-column table forces the whole table wider than the form. No top
   border on the row, so an amount and its date read as one cell, not two. */
.memo-payment-table .memo-entry-date-row td { padding-top: 0; border-top: 0; }
.memo-payment-table .memo-entry-date-row input[type=date] {
  padding: 4px 5px;
  font-size: 0.76rem;
  color: var(--text-dim);
  min-width: 0;
  width: 100%;
}
th {
  text-align: left;
  text-transform: uppercase;
  font-size: 0.74rem;
  letter-spacing: 0.08em;
  /* --text-dim, not --text-faint (1 Nov 2026): faint grey on the heading
     band read 2.7:1 in the light theme, small uppercase text on every table. */
  color: var(--text-dim);
  border-bottom: 1px solid var(--border);
  padding: 10px 12px;
  font-weight: 600;
}
td { padding: 12px; border-bottom: 1px solid var(--border); vertical-align: middle; }
tr:nth-child(even) td { background: var(--panel-quiet); }
tr:hover td { background: var(--accent-quiet); }

/* Status dots — one dot, one word. No boxes. */
.status-dot {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.82rem;
  color: var(--text-dim);
}
.status-dot::before {
  content: "";
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--text-faint);
}
.status-dot.ok { color: var(--status-ok); }
.status-dot.ok::before { background: var(--status-ok); }
.status-dot.pending { color: var(--status-pending); }
.status-dot.pending::before { background: var(--status-pending); animation: pulseDot 1.8s ease-in-out infinite; }
.status-dot.danger { color: var(--status-danger); }
.status-dot.danger::before { background: var(--status-danger); }

/* Admin badge — kept small and quiet, one usage */
.tag-admin {
  display: inline-block;
  font-size: 0.68rem;
  padding: 1px 7px;
  border-radius: 4px;
  background: var(--accent-quiet);
  color: var(--accent);
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* Payment history / Credit notes side-by-side on the invoice view —
   each its own box with its table above its entry form, saving the
   vertical scroll the old stacked layout needed. */
.billing-side-by-side {
  display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
  margin-top: 18px; align-items: start;
}
.billing-side-by-side h2 { margin: 0 0 10px; font-size: 1.02rem; display: flex; align-items: center; gap: 6px; }
@media (max-width: 800px) { .billing-side-by-side { grid-template-columns: 1fr; } }

/* THREE ENTRY FORMS IN ONE ROW — payment, credit note, debit note.
   Owner's instruction (7 Aug, with a sketch): they were 2-up with the debit note
   stranded full-width underneath, which made the third look like a different KIND
   of thing rather than the third of three peers. They are peers, so they line up.
   1fr each, not auto: equal columns keep the three Amount boxes the same size, so
   the eye can compare them. `align-items: start` stops a taller card (the credit
   note has one more field than the payment) stretching its neighbours.
   Steps down 3 -> 2 -> 1 rather than 3 -> 1, because at ~900px three of these are
   too narrow for a date input but two are still comfortable. Below 740px the
   phone layer collapses every inline grid to one column anyway (§15). */
.billing-three-up {
  display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px;
  margin-top: 18px; align-items: start;
}
.billing-three-up h2 { margin: 0 0 10px; font-size: 1.02rem; display: flex; align-items: center; gap: 6px; }
/* Each card's own form must not force the column wider than its 1fr share —
   without this a long placeholder ("e.g. NEFT ref, cheque no.") pushes the grid
   out and reintroduces horizontal page scroll. */
.billing-three-up .stub { min-width: 0; }
.billing-three-up input, .billing-three-up select { max-width: 100%; }
@media (max-width: 1100px) { .billing-three-up { grid-template-columns: 1fr 1fr; } }
@media (max-width: 800px) { .billing-three-up { grid-template-columns: 1fr; } }
/* Four peers (payment, TDS deducted, credit note, debit note — 11 Oct 2026):
   4 across on a desktop, 2 x 2 below 1100px, one column on a phone. */
.billing-three-up.billing-four-up { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 1100px) { .billing-three-up.billing-four-up { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 800px) { .billing-three-up.billing-four-up { grid-template-columns: 1fr; } }
/* The bill screen's figure cards: seven since TDS deducted (11 Oct 2026). Was an
   inline repeat(6,1fr), which on a phone left each card ~55px wide. */
.bill-stats { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 12px; margin: 14px 0; }
.bill-stats .stat-card { min-width: 0; }
/* The memo screen's cards: five since Trip profit (12 Oct 2026). Was an inline
   repeat(4,1fr) with no phone rule. */
.memo-stats { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; margin: 20px 0; }
.memo-stats .stat-card { min-width: 0; }
@media (max-width: 1000px) { .memo-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 640px) { .memo-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* A MEMO'S PARTIES, PAYEE AND PAYMENTS (18 Oct 2026). Owner: "keep contact
   details of owner, driver and broker below each other", "mention PAN, bank
   details to whom money is send", "add multiple transaction in memo".
   Partials: views/partials/_memo_parties / _memo_payee / _memo_payments /
   _memo_summary. Class-based grids, so the phone tier's inline-grid collapse
   does not reach them; each has its own phone rule below instead. */
.memo-parties { display: grid; grid-template-columns: 64px minmax(0, 1fr) 150px; gap: 6px 12px; align-items: start; margin: 14px 0 6px; max-width: 760px; }
.memo-parties .mp-head { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-dim); }
.memo-parties .mp-label { margin: 0; padding-top: 10px; font-weight: 500; }
.memo-parties input { margin: 0; }
.memo-parties .mp-phone { min-width: 0; }
.memo-payee { border: 1px solid var(--border-strong); border-radius: 8px; padding: 10px 14px 12px; margin: 14px 0 6px; background: var(--panel-quiet); max-width: 760px; }
.memo-payee-title { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-dim); font-weight: 600; }
.memo-payee-grid { display: grid; grid-template-columns: 170px minmax(0, 1fr); gap: 0 12px; }
.memo-payee-bank { display: grid; grid-template-columns: 130px minmax(0, 1fr) 150px; gap: 0 12px; }
.memo-payee label { margin-top: 8px; }
.memo-payments { margin: 12px 0 4px; max-width: 760px; }
.memo-pay-scroll { overflow-x: auto; }
.memo-paid-list { table-layout: fixed; min-width: 520px; margin-top: 4px; }
.memo-paid-list th, .memo-paid-list td { padding: 4px 4px; }
.memo-paid-list th { font-size: 0.68rem; }
/* The payment ROWS are a grid, never a table and never inside anything that
   scrolls: the "Paid as" list opens as a panel inside its box, and an overflow
   container clips it to one row (see _memo_payments.ejs). */
.memo-pay-head, .memo-pay-row { display: grid; grid-template-columns: 140px 170px 120px minmax(0, 1fr) 28px;
  grid-template-areas: "date kind amt ref rm"; gap: 6px; align-items: center; }
.memo-pay-head { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-dim); padding-bottom: 4px; }
.memo-pay-row { padding: 5px 0; border-top: 1px solid var(--border); }
.memo-pay-row > div { min-width: 0; }
.memo-pay-row .mp-date { grid-area: date; } .memo-pay-row .mp-kind { grid-area: kind; } .memo-pay-row .mp-amt { grid-area: amt; }
.memo-pay-row .mp-ref { grid-area: ref; } .memo-pay-row .mp-rm { grid-area: rm; }
.memo-pay-row input, .memo-pay-row select { padding: 7px 6px; font-size: 0.88rem; min-width: 0; margin: 0; }
/* Truck Rate: a figure, not a sentence - as wide as its Qty x Rate helper. */
.memo-rate { max-width: 360px; }
.memo-paid-list td { font-size: 0.88rem; }
.memo-paid-list .num { text-align: right; }
.memo-paid-list .dim, .memo-summary .dim { color: var(--text-dim); }
.memo-paid-list tfoot td { border-bottom: 0; }
.memo-bank-tag { display: inline-block; padding: 0 5px; border-radius: 4px; font-size: 0.7rem; background: var(--accent-quiet); color: var(--accent); }
.memo-pay-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px 18px; flex-wrap: wrap; margin-top: 6px; }
.memo-pay-summary { display: flex; flex-wrap: wrap; gap: 4px 18px; font-size: 0.9rem; }
.memo-pay-summary .hint-toggle { vertical-align: middle; }
.memo-summary { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px 24px; margin-bottom: 10px; font-size: 0.9rem; }
.memo-sum-parties .msp-row { display: grid; grid-template-columns: 64px minmax(0, 1fr) 110px; gap: 0 10px; padding: 3px 0; border-bottom: 1px solid var(--border); }
.memo-sum-parties .msp-label { color: var(--text-dim); }
.memo-sum-payee .msp-payee-name { margin: 4px 0; }
.memo-sum-payee .msp-bank { display: flex; flex-wrap: wrap; gap: 2px 16px; }
/* THE SAVE BAR ON LONG ENTRY FORMS (18 Oct 2026: "optimized for data entry
   efficiency"). New/Edit LR and New Memo are several screens tall; their Save /
   Print / Save draft row now stays at the foot of the window until the form's
   real end scrolls into view, so saving never means scrolling to find the
   button. Sticky, not fixed: it keeps its place in the form, so nothing is ever
   covered once the form ends. z-index under the typeahead lists (60) and the
   hint popover (120), so a list opened near the bottom still shows over it. */
.form-actions-bar {
  position: sticky; bottom: 0; z-index: 30;
  margin-top: 22px; padding: 10px 0 12px;
  background: var(--bg);
  border-top: 1px solid var(--border);
}
/* On a phone the four buttons share ONE row (they wrapped to two, 115px of a
   800px screen): equal shares, the inline 8px gaps swapped for a flex gap. The
   44px touch height from the phone tier is kept. */
@media (max-width: 640px) {
  .form-actions-bar { display: flex; flex-wrap: wrap; gap: 6px; }
  /* Save draft's "Draft saved" note (draft-save.js) takes a line of its own. */
  .form-actions-bar > .draft-note { flex: 0 0 100%; }
  .form-actions-bar > .draft-note:empty { display: none; }
  .form-actions-bar > button, .form-actions-bar > .btn {
    flex: 1 1 0; min-width: 0; margin-left: 0 !important;
    padding-left: 4px; padding-right: 4px; white-space: nowrap; text-align: center;
  }
}
@media (max-width: 640px) {
  .memo-parties { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .memo-parties .mp-head { display: none; }
  .memo-parties .mp-label { grid-column: 1 / -1; margin-top: 6px; padding-top: 0; }
  .memo-payee-grid, .memo-payee-bank, .memo-summary { grid-template-columns: minmax(0, 1fr); }
  .memo-sum-parties .msp-row { grid-template-columns: 56px minmax(0, 1fr) 104px; }
  /* A payment row on a phone: date, kind, x / amount, reference. */
  .memo-pay-head { display: none; }
  .memo-pay-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 28px; grid-template-areas: "date kind rm" "amt ref ref"; }
}
@media (max-width: 1100px) { .bill-stats { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 640px) { .bill-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* Same cards, other counts (12 Oct 2026): three on the Transporters screens,
   six on a client's credit page — steps 6 -> 3 -> 2 rather than squeezing six
   rupee figures into a 945px row, where they spilled into each other. */
.bill-stats.stats-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.bill-stats.stats-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
@media (max-width: 1100px) { .bill-stats.stats-6 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 640px) { .bill-stats.stats-3, .bill-stats.stats-6 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* Invoice view meta row: "Firm: X | Client name: Y | Date: Z" — sits
   directly under the H1 in place of the old skinny subtitle. */
.invoice-meta {
  display: flex; flex-wrap: wrap;
  gap: 8px 22px;
  margin: -8px 0 18px;
  font-size: 1.05rem;
  line-height: 1.4;
}
.invoice-meta .label { color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.78rem; margin-right: 4px; }
.invoice-meta .value { color: var(--text); font-weight: 500; }

.actions-row { display: flex; gap: 10px; align-items: center; margin: 14px 0 10px; flex-wrap: wrap; }
.actions-row .search-form { display: flex; gap: 8px; margin-left: auto; }
.actions-row .search-form input { width: 240px; }

/* Compact LR filter bar. It WRAPS instead of overflowing — a filter you
   have to scroll sideways to reach is a filter you won't use, so the row
   flows onto a second line before it ever needs a horizontal scrollbar.
   Width/height are scoped to text/date/select controls only; the "Date"
   checkbox keeps its natural small size (see below) rather than being
   stretched to input width. Given its own full-width row under the "+ New"
   button so it has the whole container to lay the controls out in. */
.actions-row .search-form.lr-filters { margin-left: 0; flex: 1 1 100%; }
.lr-filters { flex-wrap: wrap; align-items: center; gap: 6px; }
.lr-filters select,
.lr-filters input[type="text"],
.lr-filters input[type="date"] { height: 32px; padding: 4px 8px; font-size: 0.85rem; }
.lr-filters select { width: auto; min-width: 0; max-width: 150px; }
.lr-filters input[type="text"] { width: 120px; }
.lr-filters input[type="date"] { width: 138px; }
.lr-filters button { height: 32px; padding: 4px 12px; white-space: nowrap; }

/* "Date" checkbox that reveals a from/to date-range pair — used on
   LR list, Billing list and Delivery Tracker filter bars. The checkbox
   is explicitly small: the generic .search-form input width would
   otherwise stretch it to a big square. */
.lr-filters .date-filter-toggle-label {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 0.85rem; color: var(--text-dim); white-space: nowrap;
}
.lr-filters input[type="checkbox"] {
  width: 15px; height: 15px; min-width: 15px; margin: 0; flex: 0 0 auto;
}

/* =====================================================================
   SHARED FILTER BAR (17 Aug 2026). One layout for every filter row in
   the app, replacing nine screens' worth of hand-placed inline styles.

   Root causes found live, not guessed, before this was written:
     - `.actions-row .search-form { margin-left: auto; }` is the DEFAULT
       for any plain filter form — only `.lr-filters` was ever exempted.
       A second, narrow form sharing an .actions-row with nothing else
       (Bank Reconciliation's old standalone "Asked or not" select) gets
       shoved to the far right by that auto margin with nothing to
       balance it — measured at margin-left:961px in a 1201px container.
       .filter-bar sidesteps this by being the ONLY form in its row: every
       control that used to be its own separate <form> is folded in.
     - date-filter.js correctly display:none's the from/to date inputs
       until their checkbox is ticked, but nothing ever filled the gap
       that leaves, so whatever field came next in the DOM sat flush
       against the checkbox — measured at 0-8px on four different pages.
       .filter-range gives every paired range (date-to-date, amount-to-
       amount) one bordered box, so there is always a real visual
       boundary between one filter and the next regardless of which
       inputs are currently shown or hidden.

   NOT a replacement for .lr-filters above — that class still works and
   still ships on any page not yet migrated to this one. Both can be live
   in the codebase at once without conflict; they don't share a selector. */
.filter-bar {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px 16px;
  background: var(--panel-quiet); border: 1px solid var(--border); border-radius: 8px;
  padding: 9px 12px; margin: 8px 0 10px; width: 100%;
}
/* Plain source-order grouping — no named "primary/secondary" tiers to
   keep in sync with the markup. A hairline rule between groups is enough
   to read as "these belong together" without a heavier visual device. */
.filter-group { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; }
.filter-group + .filter-group { padding-left: 16px; border-left: 1px solid var(--border); }
.filter-field { display: flex; flex-direction: column; gap: 3px; }
/* A REAL <label for="…">, not a decorative caption — Reports (/reports/ca)
   already proved this reads fine at this size and is strictly better for
   anyone using a screen reader or clicking the label to focus the field. */
.filter-field label {
  font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-dim); font-weight: 600;
  /* CAPPED, not nowrap (re-fixed 17 Aug 2026, later still — found live on
     three separate fields: Bank Reconciliation's, Memo's and LR Entry's
     search boxes, all captioned with a list of what they match instead of
     "Search"). `.filter-field` has no width of its own — it is a column
     flex box, and a column flex box's own auto width is set by its WIDEST
     child regardless of align-items. `white-space: nowrap` let a long
     label win that over the 130px input it sits above every time —
     "Client / reference / narration" measured at 205px for a 130px box.
     A max-width this input/select's own typical size, plus wrapping
     instead of nowrap, means a future long label degrades to two short
     lines instead of silently widening the whole field again. */
  max-width: 150px; white-space: normal; line-height: 1.2;
}
/* Field height was 32px (17 Aug 2026, then re-fixed same day, later still):
   a live height audit against every OTHER already-tightened control in the
   app — .pager-btn (26px), .site-size-btn (26px), .table-size-toggle's own
   pager-btn (measured 24px) — found the filter bar was the one remaining
   oversized thing on these screens, and on a bar with 6-7 fields (Bill
   Tracker) that difference alone was worth two extra wrapped rows. 28px
   matches the app's own established control height rather than inventing a
   new one. */
.filter-bar select, .filter-bar input[type="text"], .filter-bar input[type="date"],
.filter-bar input[type="number"] {
  height: 28px; padding: 3px 7px; font-size: 0.82rem; border: 1px solid var(--border-strong);
  border-radius: 6px; background: var(--bg-elevated); color: var(--text);
}
.filter-bar select { width: auto; min-width: 0; max-width: 160px; }
.filter-bar input[type="text"] { width: 130px; }
.filter-bar input[type="date"] { width: 140px; }
/* EVERY button in the bar, not just the submit one (widened 17 Aug 2026).
   The rule used to be `button[type="submit"]` alone, which quietly missed the
   commonest neighbour it has: an `<a class="btn">`. Measured on Bill Tracker's
   own bar — Filter rendered 28px and the Export CSV link beside it 45px, two
   controls in the same row differing by 17px, because an anchor is not a
   submit button and matched nothing. Anything sitting in a filter bar is a
   filter-bar-sized control. */
.filter-bar button[type="submit"],
.filter-bar .btn,
.filter-bar button.secondary {
  height: 28px; padding: 3px 12px; white-space: nowrap; font-size: 0.83rem;
  min-height: 0;
}
/* THE RANGE PAIR — one bordered unit instead of two adjacent fields with
   nothing marking them as related. Direct fix for the DATE/₹ adjacency
   bug: the amount range now visually ends before the date range begins,
   so a unit mismatch like that could not read as one control again. */
.filter-range {
  display: flex; align-items: center;
  border: 1px solid var(--border-strong); border-radius: 6px; background: var(--bg-elevated);
  height: 28px;
}
.filter-range input {
  border: none !important; border-radius: 0 !important; background: transparent !important;
  height: 26px !important; width: 112px !important;
}
.filter-range input[type="date"] { width: 128px !important; }
.filter-range .filter-range-sep { padding: 0 6px; color: var(--text-faint); font-size: 0.8rem; flex: 0 0 auto; }
.filter-range:focus-within { border-color: var(--accent); }
/* A checkbox is ALWAYS paired with its own explanatory text — no bare
   orphaned boxes. Generic enough to cover a date-range toggle or a plain
   yes/no filter with the same rule, so a page never needs to choose
   between the two the way `.date-filter-toggle-label` being reused for
   non-date checkboxes on Bill Tracker used to require. */
.filter-check {
  display: inline-flex; align-items: center; gap: 6px; height: 28px;
  font-size: 0.83rem; color: var(--text); white-space: nowrap;
}
.filter-check input[type="checkbox"] { width: 15px; height: 15px; min-width: 15px; margin: 0; flex: 0 0 auto; }
/* NO margin-left:auto here any more (dropped 17 Aug 2026, later still —
   live report on Bank Reconciliation, whose filter bar has enough groups
   to wrap unevenly at ordinary desktop widths). That auto-margin was the
   direct fix for the ORIGINAL stranding bug this whole pattern exists to
   prevent — but it assumed .filter-submit always shares a well-filled row
   with the rest of the bar, and on a bar with three groups that is not
   guaranteed: whichever group wraps onto the LAST row can be short (here,
   one checkbox and one select), and margin-left:auto still pushes the
   button to that short row's own far right — measured at 752px of empty
   background between "Asked" and "Filter", the identical bug in a new
   shape. Letting it sit in normal flow, right after the last group with
   the bar's own ordinary gap, cannot reproduce that: there is no row it
   can be alone on with nothing built up before it. */
.filter-submit { display: flex; align-items: center; gap: 8px; }
.filter-submit .filter-clear { font-size: 0.83rem; }

/* A FILTER BAR COLLAPSES BEHIND ITS OWN BUTTON — AT EVERY WIDTH since 12 Oct
   2026 (CHANGES-2026-10-11 item 8, "the filter view is too big"; it was
   phone-only from 17 Aug). The collapse mechanism is this app's settled one:
   the base rule HIDES and a JS-owned class SHOWS. Never `[hidden]` alone — it
   loses to any rule that sets display, which is the trap disclosure-toggle.js
   exists to avoid. public/js/filter-collapse.js inserts the button and the
   active-filter chips, and remembers open/closed per screen. */
.filter-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 8px 0 0; }
.filter-toggle {
  display: inline-block; margin: 0; width: auto;
  background: var(--panel-quiet); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: 8px;
  padding: 5px 12px; font-size: 0.86rem; font-weight: 600; text-align: left;
}
.filter-toggle:hover { background: var(--panel-quiet); color: var(--text); border-color: var(--accent); transform: none; }
.filter-toggle::after { content: ' ▾'; color: var(--text-dim); }
.filter-toggle[aria-expanded="true"]::after { content: ' ▴'; }
form.filter-bar[data-fb-collapse="1"] { display: none; }
form.filter-bar[data-fb-collapse="1"].fb-open { display: flex; }
/* Active filters as chips: one line saying what narrowed the list, each with
   an × that removes just that filter. */
.filter-chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; min-width: 0; }
.filter-chip {
  display: inline-flex; align-items: center; gap: 6px; max-width: 260px;
  padding: 2px 4px 2px 10px; border-radius: 999px; font-size: 0.8rem;
  background: var(--accent-quiet); color: var(--accent); border: 1px solid var(--accent-line);
}
.filter-chip > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filter-chip:hover { text-decoration: none; border-color: var(--accent); }
.filter-chip-x { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 50%; font-size: 0.9rem; line-height: 1; }
.filter-chip:hover .filter-chip-x { background: var(--accent); color: var(--accent-ink); }
.filter-chip-clear { padding-right: 10px; background: transparent; color: var(--text-dim); border-color: var(--border-strong); }

@media (max-width: 640px) {
  .filter-toggle { padding: 9px 12px; font-size: 0.88rem; }

  /* A 2-COLUMN GRID, not a strict single-column stack (re-fixed 17 Aug
     2026 — the owner's report: a full stack of every field in a bar —
     Firm, LR/vehicle, Consignor/consignee, Date range, Delivery, Billing,
     Rows per page… — read as "the filter has taken over the screen",
     which measured out to ~450-500px of pure vertical filter chrome on a
     7-field bar. Selects AND single-line text boxes now pair up two to a
     row — a 170px-ish input still types an LR/vehicle/party search fine,
     it just shows less of it at once. Only the .filter-range pair (two
     date/amount inputs plus a separator, ~250px minimum) is forced to
     span both columns via :has() — that one genuinely does not fit at
     half width. :has() is safe here: this app is only ever opened in
     Chrome/Edge, both far past the 2022 support date. */
  .filter-bar { flex-direction: column; align-items: stretch; padding: 10px; gap: 8px; }
  .filter-group { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  .filter-group + .filter-group { padding-left: 0; border-left: none; padding-top: 8px; margin-top: 0; border-top: 1px solid var(--border); }
  .filter-bar select { width: 100%; max-width: none; }
  .filter-bar input[type="text"], .filter-bar input[type="date"], .filter-bar input[type="number"] { width: 100%; }
  .filter-field:has(.filter-range) { grid-column: 1 / -1; }
  .filter-range { width: 100%; }
  .filter-range input { width: 0 !important; flex: 1 1 auto; }
  .filter-submit { margin-left: 0; justify-content: flex-start; }
}

/* Server-side pager (real links, not the client data-table pager) */
.server-pager { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
.server-pager .pager-btn.disabled { opacity: 0.4; pointer-events: none; }

/* Table toolbar (rows-per-page + text-size + count) and pager */
.table-toolbar { display: flex; align-items: center; gap: 12px; margin: 8px 0 0; flex-wrap: wrap; }
/* The table's own search box (data-table.js, item 7, 12 Oct 2026). Sized like
   a filter-bar text box; a miss is hidden by class with !important so
   column-filter.js's inline style.display can never un-hide it. */
.table-toolbar input.dt-search { width: 190px; padding: 4px 8px; font-size: 0.85rem; margin: 0; }
tr.dt-search-miss { display: none !important; }
@media (max-width: 640px) { .table-toolbar input.dt-search { width: 100%; } }
.table-size-toggle .pager-btn { min-width: 26px; padding: 3px 7px; font-size: 0.82rem; }

/* Delivery Tracker cell: toggle next to "N days" counter. */
.tracker-cell { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
.tracker-toggle { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
.tracker-days {
  font-size: 0.82rem; color: var(--text-dim); font-family: var(--mono);
  /* Data, not a field name: opt out of the uppercase the wrapping <label>
     imposes. This is why "167 days" was rendering as "167 DAYS". */
  text-transform: none; letter-spacing: normal;
}
.tracker-days.overdue { color: var(--status-pending); font-weight: 600; }

/* Toggle switch (used for delivery tracker milestones). The checkbox
   sits directly over the whole 38x20 box and IS the click target — its
   native UI is removed with appearance:none (rather than opacity:0) so
   it stays a genuinely visible, hit-testable element for every input
   method, not just real mouse clicks. The .slider sibling behind it
   draws the on/off visual. */
.toggle-sw { position: relative; display: inline-block; width: 38px; height: 20px; flex-shrink: 0; }
.toggle-sw input {
  appearance: none; -webkit-appearance: none;
  width: 100%; height: 100%; position: absolute; inset: 0; margin: 0;
  background: transparent; border: none; cursor: pointer; z-index: 1;
}
.toggle-sw .slider { position: absolute; inset: 0; background: var(--border-strong); border-radius: 10px; transition: background 0.2s ease; pointer-events: none; }
.toggle-sw .slider::before { content: ""; position: absolute; height: 14px; width: 14px; left: 3px; top: 3px; background: #fff; border-radius: 50%; transition: transform 0.2s ease; box-shadow: 0 1px 3px rgba(0,0,0,0.25); }
.toggle-sw input:checked ~ .slider { background: var(--status-ok); }
.toggle-sw input:checked ~ .slider::before { transform: translateX(18px); }
.toggle-sw input:disabled ~ .slider { opacity: 0.55; }
.toggle-sw input:disabled { cursor: not-allowed; }

/* Global site text-size scaling (A- / A / A+ in topbar) */
:root[data-site-size="sm"] { font-size: 88%; }
:root[data-site-size="lg"] { font-size: 112%; }
.site-size-ctrl { display: inline-flex; gap: 2px; align-items: center; }
.site-size-btn {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0 7px; height: 26px; border-radius: 6px;
  border: 1px solid var(--border-strong); background: var(--panel-quiet);
  color: var(--text-dim); cursor: pointer; font-size: 0.75rem; font-weight: 600;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.site-size-btn:hover { border-color: var(--accent); color: var(--accent); }
.site-size-btn.active-size { border-color: var(--accent); color: var(--accent); background: var(--accent-quiet); }

/* Grid tables honor the app-wide text-size setting stored on <html>. */
:root { --dt-scale: 1; }
:root[data-table-size="sm"] { --dt-scale: 0.86; }
:root[data-table-size="lg"] { --dt-scale: 1.14; }
table.grid-table { font-size: calc(0.96rem * var(--dt-scale)); }
/* Column widths across the app are fixed px, measured against the header
   labels they have to hold. So HEADERS DELIBERATELY DO NOT SCALE with the
   text-size control: if they did, "Larger" would push a single-word label
   like "Vehicle" or "Consignor" past its column and break it mid-word,
   which is what the run-together headers looked like. The control exists to
   make the DATA readable — a column label is read once and learned — so the
   data scales and the headings stay put, which keeps every measured width
   valid at every setting. */
table.grid-table th { font-size: 0.74rem; }
.table-pager { display: flex; gap: 4px; margin-top: 14px; flex-wrap: wrap; }
.pager-btn {
  min-width: 34px; padding: 5px 10px; font-size: 0.85rem;
  border: 1px solid var(--border-strong); border-radius: 6px;
  background: var(--panel); color: var(--text-dim); cursor: pointer;
}
.pager-btn:hover:not(:disabled):not(.active) { border-color: var(--accent); color: var(--accent); }
.pager-btn.active { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); cursor: default; }
.pager-btn:disabled { opacity: 0.4; cursor: default; }

/* Grid table — stronger, more visible boundaries than the default
   hairline-bottom-only table (owner asked for clearer borders). */
.grid-table { border: 1px solid var(--border-strong); }
.grid-table th, .grid-table td { border: 1px solid var(--border-strong); }
/* Tight cell padding is part of the ergonomic-table recipe, and it belongs
   HERE rather than as an inline style on every td, because the views that set
   it inline all set it on td ONLY — leaving their headers on the loose global
   10px/12px. That cost each heading 16px of label room in a column measured
   against the heading, which is why "LRS" and "REMAINING" on the Memo screen
   broke mid-word inside columns that looked wide enough. Any check that
   assumes th padding equals td padding cannot see it. */
.grid-table th, .grid-table td { padding: 4px 4px; }
/* On the fixed-layout data tables (LR list, Delivery, Memo, Billing), a
   very long single word in an auto-width cell — e.g. a party name like
   "Hinduja Renewables Energy Private Limited" squeezed into a narrow
   column — could otherwise poke past its cell and paint over the next
   one. Breaking long words keeps every cell's content inside its own
   column, which is the whole ergonomic-table promise (no overflow).

   HEADERS NEED THE SAME RULE, and for a long time only td had it. That
   asymmetry is a real bug, not a cosmetic one: under table-layout:fixed a
   header wider than its column does not wrap, it SPILLS across the next
   header — which is what produced "VEHICLECONSIGNOR", "HAMALIOTHER
   CHARGES" and "OUTSTANDINGACTIONS" on the Billing and Bill Tracker
   screens. It is also invisible to any check that only measures cells, so
   it survived several rounds of review. Both halves of the table now wrap
   the same way.

   `break-word`, NOT `anywhere` — the difference matters and is the whole
   reason this rule reads oddly. Both break a long word rather than let it
   spill, but only `anywhere` also counts those break points when the browser
   computes a cell's MIN-CONTENT width. On the auto-layout tables (Clients,
   Vehicles, Invoice list) that told the layout engine every heading could
   shrink to one letter, so it happily squeezed "CONTACT" and "STATUS" into
   40px and broke them mid-word — the exact defect this rule was added to
   prevent, reintroduced from the other direction. With `break-word` the
   engine still reserves each heading's longest word, and a fixed-layout
   column that is genuinely too narrow still breaks instead of spilling. */
.grid-table td, .grid-table th { overflow-wrap: break-word; }

/* ONE FONT, ONE SIZE inside every data table (owner's request: "fix
   different font for different text ... carry alignment same font and size
   on all tables"). Previously LR numbers, dates, vehicle numbers and money
   carried .mono while party and firm names were sans, so a single row used
   two typefaces and two apparent sizes and read as scrappy.
   The override is scoped to .grid-table so .mono still does its job
   elsewhere (ids in the topbar, the dense line-item entry tables).
   tabular-nums is kept on the numeric cells: it lines figures up on the
   decimal WITHOUT changing the typeface, which is the only thing the
   monospace font was really buying here. */
.grid-table .mono {
  font-family: inherit;
  font-variant-numeric: tabular-nums;
  font-size: inherit;
}
/* Header alignment must match its own column's data alignment — a heading
   sitting over the opposite edge of its figures is what made these tables
   look inconsistent column to column. Text left, money right, and the
   header follows the cell rather than being set independently. */
.grid-table th { text-align: left; }
.grid-table th.num, .grid-table td.num { text-align: right; }
.grid-table th.mid, .grid-table td.mid { text-align: center; }

/* Vehicle numbers print on two stacked lines, centred on each other. */
.grid-table .veh {
  text-align: center;
  line-height: 1.15;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* STICKY HEADER (16 Aug 2026). Scrolling any long table lost the column
   headings along with the top nav — the whole page scrolls as one block,
   confirmed in Phase 0: no view wraps its table in its own overflow-y
   panel (the .table-scroll/enhance()-inserted wrappers are overflow-x
   only, for wide tables to scroll sideways, never vertically), so `top: 0`
   here is relative to the real page scroll for every table in the app,
   with no per-view exception needed.
   Sticky lives on the TH cells themselves, not on <thead> — the more
   broadly reliable target for a sticky table header, and it is also
   already how the background rule below was scoped, so this extends an
   existing rule rather than adding a new selector shape.
   The background was ALREADY solid in every theme (--panel-quiet is a
   plain hex colour everywhere, checked — light/dark/sepia/dark-sepia/
   high-contrast), so scrolled body rows cannot show through underneath;
   nothing needed to change there. z-index is deliberately low (2, the
   same value already used for .module-info's own in-flow stacking) —
   comfortably above ordinary body cells, which never declare a z-index of
   their own, and comfortably below every popover/dropdown/modal layer in
   this file (.dt-panel and friends start at 60), so a header can never
   sit on top of something it should be behind.
   `.grid-table th.sortable` below used to declare its own `position:
   relative` (needed only so its ::after arrow could be absolutely
   positioned against it) — at two classes vs. this rule's one, that
   selector is MORE specific than `.grid-table thead th`, so on every
   sortable header (the majority of them) it silently overrode `position:
   sticky` back to `relative` and the header never stuck. Fixed by
   dropping `position: relative` from .sortable entirely: `position:
   sticky` already establishes the same containing block for an
   absolutely-positioned descendant that `relative` did, so the arrow
   still positions correctly and there is no longer a competing
   declaration to lose the cascade. */
.grid-table thead th {
  background: var(--panel-quiet);
  position: sticky;
  top: 0;
  z-index: 2;
  /* A DELIBERATELY 2px BOTTOM EDGE (16 Aug 2026). Reported twice as "the
     header has no visible outline", after the background and 1px borders
     had both been confirmed present by measurement — so the rule was
     applying and the line still was not reading as one. The mechanism,
     found by measuring rather than guessing again: getComputedStyle
     reported the header's border-width as 0.666667px, i.e. the browser
     was at ~67% zoom, where a 1px hairline lands on a fraction of a
     device pixel and the renderer can dim it to near-invisibility or drop
     an edge entirely. 2px still reads as a single crisp rule at 100% and
     survives being scaled down. Bottom only: that is the edge that
     separates headings from data, the one being looked for, and thickening
     all four would turn a quiet grid into a boxy one. */
  border-bottom: 2px solid var(--border-strong);
  /* The table's own `border-collapse: collapse` merges the header's bottom
     border with the first body row's top border into one line owned by
     the table's border grid, not by either cell's own box — and that
     collapsed border does not reliably repaint at a sticky cell's stuck
     position (a known cross-browser quirk: collapse resolves/paints
     borders at the table level, sticky promotes the cell to its own
     stacking position, and the two don't stay in sync while scrolling).
     Reported live: the header had no visible boundary line once real rows
     had scrolled up underneath it. box-shadow sidesteps the whole
     mechanism — it is never subject to border-collapse, always paints as
     part of the element's own box, and therefore always travels correctly
     with a sticky element. Purely additive; the existing collapsed border
     is untouched for every non-scrolled state. */
  box-shadow: 0 1px 0 var(--border-strong);
}
/* A wrapped two-line header should sit on the same baseline as its
   single-line neighbours rather than floating in the middle of the row. */
.grid-table thead th { vertical-align: bottom; }
.grid-table th.sortable { cursor: pointer; user-select: none; padding-right: 22px; }

/* SCROLLABLE TABLE WRAPPER (16 Aug 2026). Every table on this site that can
   run wider than the viewport is wrapped in a div carrying overflow-x:auto
   — some hand-authored per view, some inserted by data-table.js's enhance().
   Live-verified this same div was silently defeating the sticky header
   above: per the CSS Overflow spec, setting overflow-x to anything but
   visible forces overflow-y's COMPUTED value to auto too, even if overflow-y
   is left unset or written as `visible` explicitly (checked: the browser
   still reports "auto" back from getComputedStyle either way, no escape
   hatch on the same element). So the wrapper was already an accidental
   scroll container the moment overflow-x:auto was set on it, whether
   anyone intended that or not — and because it had no bounded height, it
   never scrolled independently of the page (it just grows to fit its
   content), so a sticky child sticks to ITS scrollport, which is
   indistinguishable from "never sticks" since that scrollport's own
   scrollTop never moves. This is a correction to the original Phase 1
   assumption ("no per-view exception needed, sticky is relative to the
   page") — recorded here and in the blueprint log rather than silently
   patched over.
   The fix used everywhere else a scrollable table needs this (already
   named `table-scroll` by billTracker/index.ejs before this date) is to
   give the wrapper a real bounded height and let it become a genuine,
   independently-scrolling pane — then the sticky header sticks to the top
   of THAT pane exactly as a frozen header is expected to. 70vh comfortably
   fits 15-20 data rows on a normal desktop window while still leaving the
   page's own scroll (and the top nav within it) largely undisturbed; on
   short tables it never engages (content stays under 70vh, no scrollbar
   ever appears) so applying it universally is harmless. */
.table-scroll {
  overflow-x: auto;
  overflow-y: auto;
  max-height: 70vh;
}

/* EXCEL-STYLE COLUMN FILTER (16 Aug 2026) — public/js/column-filter.js,
   self-wiring on table[data-col-filter="true"]. Same visual language as
   .dt-panel/.dt-option (border/background/shadow/z-index all match — one
   dropdown idiom app-wide) but its own class rather than literally reusing
   those: this panel has a fixed header (sort buttons, search box) and
   footer (Apply/Clear) around its own scrollable middle, where .dt-panel
   is a single scrolling region top to bottom — reusing it directly would
   have made the search box and Apply/Clear scroll away with the list. */
/* Absolutely positioned in the th's top-right corner, NOT appended inline
   after the header's own content (the original approach) — live-verified
   two problems with that: on a plain sortable th, an inline button sitting
   right after the text crowds the existing ::after sort arrow (both cram
   into the same few pixels at the text's trailing edge); on Bill Tracker's
   headers specifically, the label is a `.th-sort { display: block }` link,
   so an appended sibling button is forced onto its own third line below a
   two-line label — measured at 68.85px tall vs. ~29px for a normal single-
   line header. Corner-absolute sidesteps both: it never participates in
   text flow, so it can't push a block link onto a new line or crowd
   trailing inline content, and thead th's own `vertical-align: bottom`
   (set above so wrapped two-line headers share one baseline) already
   leaves the top of most header cells empty, which is exactly where this
   sits — the ::after sort arrow flows with the (bottom-aligned) text, so
   the two land in different bands of the cell rather than on top of each
   other. Same idiom the sort arrow itself already uses (position:
   absolute against the th), just anchored to the opposite corner. */
.col-filter-btn {
  position: absolute; top: 3px; right: 3px; z-index: 1;
  display: inline-flex; align-items: center; justify-content: center;
  width: 14px; height: 14px; min-height: 0; padding: 0;
  background: transparent; border: none; border-radius: 3px;
  color: var(--text-dim); font-size: 0.6rem; line-height: 1; cursor: pointer;
}
.col-filter-btn:hover { background: var(--panel-quiet); color: var(--text); }
/* Gutter for the corner button above, on any th that gets one and isn't
   ALSO .sortable (that rule's own 22px already covers it — more specific,
   wins naturally). Without this a wide single-line label with no gutter of
   its own runs its last word under the button — measured on Bill
   Tracker's "Balance Due". Written as `.grid-table th.col-filter-th`, not
   bare `.col-filter-th`, because the base cell-padding rule right below
   (`.grid-table th, .grid-table td { padding: 4px 4px; }`) is a class+
   element selector — a bare single-class rule here is LESS specific than
   that and silently loses regardless of source order; live-verified this
   exact miss once already (computed padding-right still read 4px after
   first adding the bare-class version). */
.grid-table th.col-filter-th { padding-right: 20px; }
/* A restricted column stays visibly marked after the panel closes — the
   only way to tell "this column is filtered" without reopening it. */
.col-filter-btn.active { color: var(--accent); }
.col-filter-panel {
  display: none;
  position: absolute; top: 100%; margin-top: 4px; width: 220px;
  background: var(--bg-elevated); border: 1px solid var(--border-strong);
  border-radius: 6px; box-shadow: 0 4px 14px rgba(0,0,0,0.18);
  z-index: 60;
  font-weight: 400; font-size: 0.85rem; text-transform: none; letter-spacing: normal;
  text-align: left; color: var(--text);
}
.col-filter-panel.shown { display: block; }
.col-filter-sort { padding: 6px; border-bottom: 1px solid var(--border); display: flex; flex-direction: column; gap: 2px; }
.col-filter-sortbtn {
  background: none; border: none; text-align: left; padding: 5px 6px; border-radius: 4px;
  color: var(--text); font-size: 0.82rem; cursor: pointer; width: 100%;
}
.col-filter-sortbtn:hover { background: var(--panel-quiet); }
.col-filter-search { padding: 6px; border-bottom: 1px solid var(--border); }
.col-filter-search-input { width: 100%; padding: 4px 6px; font-size: 0.82rem; }
.col-filter-list { max-height: 180px; overflow-y: auto; padding: 2px 0; }
.col-filter-option {
  display: flex; align-items: center; gap: 6px; padding: 4px 8px; cursor: pointer;
  font-size: 0.82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-weight: 400;
}
.col-filter-option:hover { background: var(--panel-quiet); }
.col-filter-option.col-filter-search-hidden { display: none; }
.col-filter-selectall { border-bottom: 1px solid var(--border); font-weight: 600; }
.col-filter-actions { display: flex; gap: 6px; padding: 6px; border-top: 1px solid var(--border); }
.col-filter-actions .pager-btn { flex: 1 1 auto; text-align: center; }
/* Only ever visible while at least one column filter is actually active
   (column-filter.js toggles .shown) — silent on every table until touched. */
.col-filter-status { font-size: 0.82rem; color: var(--accent); margin: 6px 0 0; display: none; }
.col-filter-status.shown { display: block; }
/* A CENTRED sortable heading needs the sort arrow's 22px gutter mirrored on
   the left, otherwise the label centres inside a box that is short on one side
   and lands visibly left of its own column's centred data (Vehicle on the
   Memo and Delivery screens). The arrow itself is absolutely positioned, so
   the extra padding costs nothing but symmetry. */
.grid-table th.sortable.mid { padding-left: 22px; }
.grid-table th.sortable:hover { color: var(--accent); }
.grid-table th.sortable::after { content: "⇅"; position: absolute; right: 8px; opacity: 0.35; font-size: 0.8em; }
.grid-table th.sortable[data-sort-dir="asc"]::after { content: "↑"; opacity: 1; color: var(--accent); }
.grid-table th.sortable[data-sort-dir="desc"]::after { content: "↓"; opacity: 1; color: var(--accent); }

.perm-grid td, .perm-grid th { text-align: center; }
.perm-grid td:first-child, .perm-grid th:first-child { text-align: left; }

/* Idle-lock overlay — a warning shown when Admin has enabled the
   inactivity auto-logout and the countdown to it has started. Any
   activity anywhere on the page cancels it (see idle-lock.js); there's
   no button because moving the mouse already proves the person is
   present, and the actual logout is enforced server-side regardless. */
.idle-lock-overlay {
  display: none;
  position: fixed; inset: 0; z-index: 500;
  background: rgba(0,0,0,0.55);
  align-items: center; justify-content: center;
  padding: 20px;
}
.idle-lock-box {
  background: var(--panel); border: 1px solid var(--border-strong);
  border-radius: 10px; padding: 28px 30px; max-width: 360px; text-align: center;
}
.idle-lock-box h2 { margin: 0 0 8px; font-size: 1.1rem; }
.idle-lock-box p { color: var(--text-dim); font-size: 0.9rem; margin: 0; }

/* ---------------------------------------------------------------- login */
.login-shell {
  min-height: 100vh;
  display: flex; align-items: center; justify-content: center;
  position: relative;
  padding: 24px;
}
.login-shell .corner-controls { position: absolute; top: 24px; right: 24px; display: flex; gap: 8px; }
.login-box { width: 380px; }
.login-box .stub { padding: 32px 32px 28px; }
.login-brand { text-align: center; margin-bottom: 32px; }
/* The company logo on the sign-in card (partials/brand-logo.ejs). */
.login-brand .login-logo { width: 240px; max-width: 80%; height: auto; margin: 0 auto 4px; }
.login-brand .login-logo.logo-ink-light { display: block; }
[data-theme="dark"] .login-brand .login-logo.logo-ink-light { display: none; }
[data-theme="dark"] .login-brand .login-logo.logo-ink-dark { display: block; }
.login-brand .tag {
  color: var(--text-faint);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-top: 8px;
}
.login-brand .accent-line {
  width: 32px; height: 2px;
  background: var(--accent-2);
  margin: 16px auto 0;
  border-radius: 1px;
}

footer.app-footer { text-align: center; color: var(--text-faint); font-size: 0.72rem; padding: 24px; }

/* ── Gmail-style email composer ─────────────────────────────────────── */
.compose-card {
  max-width: 860px; border: 1px solid var(--border); border-radius: 12px;
  background: var(--panel); box-shadow: 0 6px 24px rgba(0,0,0,0.06); overflow: hidden;
}
.compose-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 18px; background: var(--panel-quiet); border-bottom: 1px solid var(--border);
}
.compose-head .compose-title { font-weight: 600; font-size: 0.95rem; }
.compose-head .compose-sub { color: var(--text-dim); font-size: 0.82rem; }
.compose-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 18px; border-bottom: 1px solid var(--border);
}
.compose-row > label {
  margin: 0; min-width: 62px; color: var(--text-dim); font-size: 0.85rem;
  text-transform: none; letter-spacing: normal; font-weight: 500;
}
.compose-row input[type="text"], .compose-row input[type="email"] {
  flex: 1; border: none; background: transparent; padding: 6px 2px; font-size: 0.92rem; box-shadow: none;
}
.compose-row input:focus { outline: none; box-shadow: none; }
.compose-row.subject input { font-weight: 600; }
.compose-toolbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 2px;
  padding: 6px 12px; background: var(--panel-quiet); border-bottom: 1px solid var(--border);
}
.compose-toolbar button.rt-btn {
  border: 1px solid transparent; background: transparent; border-radius: 6px;
  min-width: 30px; height: 30px; padding: 0 7px; cursor: pointer; color: var(--text);
  font-size: 0.9rem; line-height: 1; display: inline-flex; align-items: center; justify-content: center;
}
.compose-toolbar button.rt-btn:hover { background: var(--border); }
.compose-toolbar button.rt-btn b, .compose-toolbar button.rt-btn i, .compose-toolbar button.rt-btn u { pointer-events: none; }
.compose-toolbar .rt-sep { width: 1px; height: 20px; background: var(--border); margin: 0 5px; }
.compose-toolbar select.rt-select {
  height: 30px; padding: 0 6px; font-size: 0.82rem; border: 1px solid var(--border);
  border-radius: 6px; background: var(--panel);
}
.compose-editor-frame { width: 100%; min-height: 460px; border: none; background: #fff; display: block; }
.compose-source {
  width: 100%; min-height: 460px; border: none; padding: 14px 18px; display: none;
  font-family: var(--mono); font-size: 0.78rem; line-height: 1.45; white-space: pre; overflow: auto; background: var(--panel);
}
.compose-foot {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 18px; border-top: 1px solid var(--border); background: var(--panel-quiet);
}
.compose-foot .compose-send { padding: 9px 26px; font-weight: 600; }
.compose-foot .source-toggle {
  margin-left: auto; display: inline-flex; align-items: center; gap: 6px;
  font-size: 0.82rem; color: var(--text-dim); text-transform: none; letter-spacing: normal; font-weight: 400; margin-bottom: 0;
}

/* --------------------------------------------------- compose-in-place modal
   public/js/email-modal.js. Every "Email"/"Remind" link opens the composer
   OVER the screen the operator is working on instead of navigating to
   /email/compose and back (17 Aug 2026) — leaving a filtered, ranked
   worklist like the Bill Tracker to send one reminder threw away the
   filters, the page and the scroll position every time.

   Same shape as .cam-ovl / .crop-ovl above, deliberately: this app already
   had a settled full-screen-overlay idiom and a third variation on it would
   be a third thing to keep in step. Note the explicit `[hidden]` rule —
   `display: flex` here beats the UA stylesheet's own `[hidden]` otherwise,
   which is the trap the app's other collapse mechanisms are careful about
   too (see disclosure-toggle.js). z-index sits below the camera/crop pair
   (900/1200): those can legitimately open ON TOP of this one, when a photo
   is attached to an email being written. */
.email-ovl {
  position: fixed; inset: 0; z-index: 800; display: flex;
  align-items: flex-start; justify-content: center;
  padding: 18px 12px; overflow-y: auto;
  background: rgba(17, 19, 24, 0.72);
}
.email-ovl[hidden] { display: none; }
.email-box {
  width: 100%;
  /* Same min() shape as .crop-box, for the same reason: one rule serves a
     laptop and a phone with no breakpoint. 900px is enough for a composer
     whose body editor is the only wide thing in it. */
  max-width: min(900px, calc(100vw - 24px));
  margin: auto;
}
/* The card already carries its own border/background from .compose-card;
   inside the overlay it only needs the shadow that lifts it off the dim. */
.email-box .compose-card { box-shadow: 0 18px 48px rgba(0, 0, 0, 0.4); margin: 0; }
/* Shorter than the full page's 460px: in a modal the surrounding chrome is
   fixed cost, and the operator is usually adjusting a sentence in a
   generated draft rather than writing a long letter from scratch. */
.email-box .compose-editor-frame { min-height: 300px; }
.email-modal-loading { padding: 40px; text-align: center; color: #fff; font-size: 0.9rem; }
.email-modal-close {
  margin-left: auto; background: none; border: none; cursor: pointer;
  color: var(--text-dim); font-size: 1.4rem; line-height: 1;
  padding: 0 2px; min-height: 0;
}
.email-modal-close:hover { color: var(--text); }
/* Only meaningful inside the modal — on the full page the composer's head
   has no close button to make room for. */
.email-compose-page .email-modal-close { display: none; }
.email-modal-error { margin: 0 0 10px; }

/* -------------------------------------------------------------------- toast
   A short confirmation that does NOT interrupt. Used after an in-place send:
   an alert() there would replace one interruption with another, which is the
   opposite of the point. */
.app-toast {
  position: fixed; left: 50%; bottom: 24px; transform: translate(-50%, 12px);
  z-index: 1400; max-width: min(460px, calc(100vw - 32px));
  background: var(--text); color: var(--bg-elevated);
  padding: 10px 16px; border-radius: 8px; font-size: 0.88rem;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
  opacity: 0; transition: opacity 0.22s ease, transform 0.22s ease;
  pointer-events: none;
}
.app-toast.shown { opacity: 1; transform: translate(-50%, 0); }
.app-toast-error { background: var(--status-danger); color: #fff; }
@media (prefers-reduced-motion: reduce) {
  .app-toast { transition: none; }
}
/* The row that was just emailed, so the operator can see WHICH one went
   without the page reloading under them. Fades on its own — a permanent
   highlight would still be there, meaning nothing, ten reminders later. */
@keyframes row-emailed-fade {
  from { background: var(--accent-quiet); }
  to   { background: transparent; }
}
tr.row-just-emailed > td { animation: row-emailed-fade 2.6s ease-out forwards; }

/* ---------------------------------------------------------------- camera scan
   The overlay behind public/js/camera-capture.js. Sized so the whole panel —
   preview, thumbnail strip and buttons — fits a phone screen held upright
   without the page behind it scrolling. */
.cam-btn {
  margin-left: 6px; padding: 3px 9px; font-size: 0.74rem; line-height: 1.5;
  white-space: nowrap; vertical-align: middle;
}
.cam-ovl[hidden] { display: none; }
.cam-ovl {
  position: fixed; inset: 0; z-index: 900; display: flex;
  align-items: center; justify-content: center; padding: 12px;
  background: rgba(17, 19, 24, 0.72);
}
.cam-box {
  width: 100%;
  /* Was a flat 560px — smaller than the crop editor's own OLD "cramped" 640px
     (see .crop-box below, fixed 12 Aug) — on a real laptop the operator was
     framing a document inside a box a third the width of the window. Same
     min() shape as .crop-box for the same reason: it degrades to near-full-
     width on its own as the viewport narrows, so one rule serves a laptop and
     a phone with no breakpoint, and 900px (vs the crop editor's 1500px) is
     enough here because this box is mostly controls and a thumbnail strip
     around the video, not a full-resolution photo being precision-edited. */
  max-width: min(900px, calc(100vw - 24px));
  max-height: 94vh; overflow-y: auto;
  background: var(--panel); border: 1px solid var(--border-strong);
  border-radius: 12px; padding: 14px 16px 16px;
}
.cam-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.cam-head .cam-x {
  background: none; border: none; color: var(--text-dim);
  font-size: 1.4rem; line-height: 1; padding: 0 4px; cursor: pointer;
}
.cam-stage {
  background: #000; border-radius: 8px; overflow: hidden;
  min-height: 130px; display: flex; align-items: center; justify-content: center;
}
/* 46vh -> 62vh: a bigger live preview means less guesswork positioning the
   phone/webcam before pressing Capture. Purely cosmetic — captureFromVideo()
   reads the video's INTRINSIC videoWidth/videoHeight, never its rendered CSS
   size, so this cannot affect capture resolution or quality either way. */
.cam-video { width: 100%; max-height: 62vh; display: block; object-fit: contain; background: #000; }
.cam-video[hidden] { display: none; }
.cam-handoff { color: #E5E7EB; font-size: 0.86rem; padding: 26px 18px; text-align: center; }
.cam-handoff[hidden] { display: none; }
.cam-msg { min-height: 1.2em; margin: 8px 2px 0; font-size: 0.8rem; color: var(--text-dim); }
.cam-thumbs { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 12px; }
.cam-thumb {
  position: relative; width: 76px; height: 76px; border: 1px solid var(--border-strong);
  border-radius: 6px; overflow: hidden; background: var(--panel-quiet);
}
.cam-thumb.retaking { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-line); }
.cam-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cam-thumb .cam-no {
  position: absolute; left: 3px; bottom: 3px; min-width: 15px; text-align: center;
  background: rgba(17, 19, 24, 0.75); color: #fff; font-size: 0.66rem;
  border-radius: 3px; padding: 0 3px;
}
.cam-thumb .cam-del, .cam-thumb .cam-retake {
  position: absolute; top: 2px; width: 19px; height: 19px; padding: 0; cursor: pointer;
  border: none; border-radius: 4px; background: rgba(17, 19, 24, 0.75); color: #fff;
  font-size: 0.78rem; line-height: 19px; text-align: center;
}
.cam-thumb .cam-del { right: 2px; }
.cam-thumb .cam-retake { right: 24px; }
.cam-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.cam-actions button[hidden] { display: none; }
.cam-actions .cam-cancel { margin-left: auto; }

/* The scanner "Look" row. Below the buttons on purpose (see camera-capture.js):
   it is set once per person, while Capture is pressed on every page, so it must
   not push the primary action down a phone screen. */
.cam-mode { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px; margin-top: 10px; }
.cam-mode label {
  display: flex; align-items: center; gap: 6px; margin: 0;
  font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim);
}
.cam-mode select { width: auto; min-width: 190px; padding: 3px 6px; font-size: 0.82rem; margin: 0; }
.cam-mode-hint { font-size: 0.74rem; color: var(--text-dim); }

/* ------------------------------------------------- crop / edge editor
   public/js/doc-crop.js. Sits ABOVE the camera overlay in z-order because it
   opens on top of it after each capture, and the camera overlay stays behind so
   the operator can see they are still mid-document. */
.crop-ovl {
  position: fixed; inset: 0; z-index: 1200;
  background: rgba(0, 0, 0, 0.78);
  display: flex; align-items: center; justify-content: center; padding: 10px;
}
.crop-ovl[hidden] { display: none; }
.crop-box {
  background: var(--panel); border: 1px solid var(--border-strong); border-radius: 10px;
  padding: 12px 14px; width: 100%;
  /* Was a flat 640px — a small fraction of a real laptop window, which is what
     made the editor read as cramped (reported 12 Aug 2026 against a 1920x1080
     screen). `min()` degrades to near-full-width on its own as the viewport
     narrows, so one rule serves a laptop and a phone without a breakpoint: the
     100vw term is what actually governs on a phone, the 1500px cap is what
     stops a wide monitor from stretching the editor absurdly wide.
     doc-crop.js's viewBudget()/refit() is the matching JS-side change — this
     rule alone would just add empty padding around a still-560px canvas.
     If you change these numbers, no JS needs updating: refit() MEASURES the
     stage rather than duplicating them, precisely so they cannot drift apart. */
  max-width: min(1500px, calc(100vw - 24px));
  max-height: calc(100vh - 20px); overflow-y: auto;
}
.crop-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.crop-x { background: none; border: 0; font-size: 1.4rem; line-height: 1; cursor: pointer; color: var(--text-dim); padding: 0 4px; }
/* The stage is mid-grey on purpose: white paper against a white panel gives no
   sense of where the page ends, which is the one thing this screen is for. */
.crop-stage {
  background: #6b6b6b; border-radius: 6px; padding: 6px;
  display: flex; align-items: center; justify-content: center;
}
/* touch-action:none is REQUIRED, not cosmetic — without it a drag on the canvas
   scrolls the page on a phone and the corner never moves. */
.crop-canvas { touch-action: none; display: block; max-width: 100%; cursor: crosshair; }
.crop-msg { min-height: 1.2em; margin: 8px 2px 0; font-size: 0.8rem; color: var(--text-dim); }
.crop-tools { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 10px 0; }
.crop-tools button { padding: 4px 10px; font-size: 0.8rem; }
.crop-turns { font-size: 0.78rem; font-weight: 600; color: var(--text-dim); min-width: 2.4em; text-align: center; }
.crop-mode { display: flex; align-items: center; gap: 6px; margin: 0; font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); }
.crop-mode select { width: auto; min-width: 175px; padding: 3px 6px; font-size: 0.8rem; margin: 0; }
.crop-preview-toggle { display: flex; align-items: center; gap: 5px; margin: 0; font-size: 0.8rem; text-transform: none; letter-spacing: normal; }
.crop-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.crop-actions .crop-cancel { margin-left: auto; }
/* On a phone the canvas is WIDTH-bound, never height-bound — a portrait photo on
   a portrait screen runs out of width long before it runs out of height. So every
   pixel the frame spends on padding is a pixel taken off the thing being cropped,
   and the frame gets out of the way here: 4/8/3 instead of 10/14/6 hands back
   ~35px of canvas width (~23% more area) on a 390px screen. Nothing in JS needs
   to know — refit() measures the stage, so it picks this up on its own. */
@media (max-width: 480px) {
  .crop-ovl { padding: 4px; }
  .crop-box { padding: 8px; max-width: calc(100vw - 8px); }
  .crop-stage { padding: 3px; }
}

/* Page edit / reorder controls on a thumbnail. The strip is small, so these sit
   on the four corners of the tile rather than in a row underneath it. */
.cam-thumb .cam-edit, .cam-thumb .cam-mv {
  position: absolute; background: rgba(0, 0, 0, 0.62); color: #fff;
  border: 0; border-radius: 4px; cursor: pointer;
  font-size: 0.78rem; line-height: 1; padding: 3px 5px;
}
.cam-thumb .cam-edit { left: 2px; bottom: 2px; }
.cam-thumb .cam-mv { bottom: 2px; left: 50%; transform: translateX(-90%); }
.cam-thumb .cam-mv-r { transform: translateX(10%); }
.cam-thumb .cam-edit:hover, .cam-thumb .cam-mv:hover { background: rgba(0, 0, 0, 0.85); }

/* ------------------------------------------------- delivery-receipt upload
   The Delivery Proof cell in the Delivery Tracker. "Received, no scan on file"
   is a real state that keeps the row listed, so the control has to sit inside a
   narrow fixed-width column without pushing it wider. */
.scan-wanted { font-size: 0.68rem; color: var(--status-pending); margin-top: 2px; }
.tracker-upload { margin-top: 4px; display: flex; flex-wrap: wrap; gap: 3px; justify-content: center; align-items: center; }
.tracker-upload .tracker-photo { width: 104px; font-size: 0.68rem; }
.tracker-upload .cam-btn { margin-left: 0; }
.tracker-upload .tracker-upload-btn { padding: 3px 9px; font-size: 0.72rem; line-height: 1.5; }

/* Re-scan, collapsed. On a row whose scan IS on file the usual action is to open
   it, so the control that would overwrite it stays folded away — and folded, it
   costs this fixed-width column one short line instead of a file input's full
   height (see the ergonomic table recipe in CLAUDE.md: these widths are tuned
   and a control that expands the cell would break the sweep). */
/* A plain div/button, not <details> — see public/js/disclosure-toggle.js's
   own header for why: an unconditional display on the collapsible content
   (here, .tracker-upload's own `display: flex`) always beat a closed
   <details>'s native hiding regardless of open/closed state. */
.tracker-rescan { margin-top: 3px; }
.tracker-rescan > .disclosure-toggle {
  cursor: pointer; font-size: 0.68rem; color: var(--text-dim);
  display: inline-block;
}
.tracker-rescan > .disclosure-toggle::before { content: '↻ '; }
.tracker-rescan > .disclosure-toggle:hover { color: var(--text); text-decoration: underline; }
.tracker-rescan.is-open > .disclosure-toggle { color: var(--status-pending); }


/* ============================================================================
   PHONE LAYER — everything from here down applies only at <= 640px.
   ============================================================================
   The app was built for a desktop browser and had exactly ONE layout media
   query before this. On a phone it was not merely cramped: several screens
   pushed the PAGE itself sideways, so the whole document scrolled left-right and
   headings drifted off the edge. Measured at 375px before this block existed:

     /dashboard        375px  fine
     /lr               375px  fine — its table already sits in an overflow-x box
     /clients          385px  bare table, no scroll box
     /billing          540px  bare table, no scroll box
     /lr/new           725px  a 4-column inline grid that cannot shrink

   That last one is the pattern, not an outlier: 47 places in the views carry an
   inline `display:grid; grid-template-columns: 1.4fr 1fr 1fr 1fr`, and grid
   tracks will not shrink below their content's minimum, so a four-field row
   demands ~725px however narrow the screen is.

   WHY THIS IS ONE CSS BLOCK AND NOT 47 EDITS. Those grids are inline styles on
   individual field rows, written per screen. Editing each one means touching
   every form in the app to solve a problem that is purely about viewport width,
   and any new form would reintroduce it. A media query fixes all of them and
   every future one. `!important` is not laziness here — it is the only way a
   stylesheet can override an inline style, and it is scoped to a width no
   desktop browser reports.

   THE PRINTED DOCUMENTS ARE NOT AFFECTED. lr/print.ejs and billing/print.ejs are
   standalone pages with their own <style> and do not load style.css at all
   (verified). Nothing here can move a millimetre on a letterhead.

   TWO TIERS, SPLIT BY PURPOSE — the first pass used one at 640px and a landscape
   sweep found what that missed. A phone in landscape is 667–932px wide, so it
   fell straight through the 640 cutoff onto the desktop layout, which at 667px
   (iPhone SE / 8 sideways) overflowed to 713px, and which kept its 0.85rem inputs
   so iOS still zoomed on every tap from 641px all the way up.

     TIER 1, <= 1024px — the iOS zoom guard ONLY.
       A font-size problem, not a layout one: it applies to a phone or tablet in
       any orientation, and 16px inputs cost a laptop nothing.
     TIER 2, <= 740px — the layout collapse and touch sizing.
       740 is measured, not round: the widest inline form grid needs ~725px, so
       anything narrower than about 740 is what actually cannot fit it. Kept below
       1024 deliberately, so a 1024px laptop does not get phone-sized controls. */
@media (max-width: 1024px) {
  input[type=text], input[type=password], input[type=email], input[type=number],
  input[type=date], input[type=time], input[type=search], select, textarea {
    font-size: 16px !important;
  }
}

@media (max-width: 740px) {
  /* Desktop padding is 40px/32px; on a 375px screen that is 17% of the width. */
  .container { padding: 18px 12px 40px; }
  h1 { font-size: 1.35rem; }
  h2 { font-size: 0.95rem; }

  /* THE MAIN FIX. Any inline multi-column grid becomes a single column, so each
     field gets the full width instead of a track too narrow to type in. The
     attribute-substring selector is what reaches an inline style. */
  [style*="grid-template-columns"] {
    grid-template-columns: 1fr !important;
    gap: 0 !important;
  }

  /* Tables scroll INSIDE themselves rather than widening the page. A data table
     on a phone is meant to be swiped sideways — the alternative (stacking every
     row into a card) loses the column alignment that makes these readable, and
     would need per-screen markup. */
  table {
    display: block;
    max-width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
  /* ...EXCEPT a table already inside .table-scroll (effectively every
     .grid-table app-wide, see style.css's SCROLLABLE TABLE WRAPPER comment).
     The old comment here called that "harmless" — true before the sticky
     header existed, false now: overflow-x:auto forces the table's own
     overflow-y to compute to auto too (same spec quirk .table-scroll itself
     works around), making the <table> a SECOND, closer, unbounded scroll
     container nested inside the wrapper's bounded one — and position:sticky
     binds to the nearest scroll-container ancestor, not the outer one.
     Live-verified at 375px: with this rule absent, the sticky header moved
     1:1 with scroll (no stickiness at all). The wrapper already provides
     the horizontal scroll this table would otherwise add for itself, so
     turning it off here is pure duplication removed, not a capability
     lost. */
  .table-scroll table {
    overflow-x: visible;
  }

  /* Filter bars: stack instead of running off the edge, and let each control
     take the full width so it is actually tappable. */
  .actions-row { gap: 8px; }
  .actions-row .search-form { margin-left: 0; flex-wrap: wrap; width: 100%; }
  .actions-row .search-form > * { flex: 1 1 45%; min-width: 0; }
  .lr-filters input[type=text], .lr-filters input[type=date], .lr-filters select { width: 100%; }

  /* TOUCH TARGETS + THE iOS ZOOM TRAP. Mobile Safari zooms the whole page in
     when a focused input's font-size is below 16px, and never zooms back out —
     which reads as the layout breaking the moment you tap a field. 16px is not
     a preference here, it is the threshold that stops that happening. 44px is
     the minimum comfortable touch height.

     The font-size half of this lives in the 1024px tier above, because iOS zooms
     on a tablet and a landscape phone too. `!important` there because the first
     pass DID NOT WORK and the measurement showed why: plain `input[type=text]` is
     specificity (0,1,1), and the app has more specific rules —
     `.lr-filters input[type=text]`, `.line-items-table input[type=text]` — which
     kept their 0.85rem and went on triggering the zoom. */
  input[type=text], input[type=password], input[type=email], input[type=number],
  input[type=date], input[type=time], input[type=search], select, textarea {
    min-height: 44px;
  }
  button, .btn {
    min-height: 44px;
    padding: 10px 16px;
  }
  /* The line-items table's own compact inputs are exempt from the 44px rule —
     they sit in a table that already scrolls, and forcing them taller would
     make a single row taller than the screen. They still get 16px from the
     1024px tier, so they do not trigger the zoom. */
  .line-items-table input, .memo-payment-table input { min-height: 0; }
  /* Controls that live INSIDE a table cell or beside a field are exempt from the
     44px rule, because 44px would make a single row taller than the screen. But
     the first pass exempted them to `min-height: 0`, and measuring found what
     that actually meant on the Delivery Tracker: the Scan button rendered 27px
     tall and the file input 20px. 20px is not a tap target, it is a dare.
     36px is the compromise — comfortably hittable, still short enough that a
     table row stays a row. */
  .hint-toggle, .btn-remove-row, .particular-br {
    min-height: 0;
  }
  .cam-btn, .tracker-upload-btn { min-height: 36px; }

  /* FIXED-SIZE ICON BUTTONS, the same trap as .hint-toggle above but caught
     later (14 Aug 2026, "the i button is larger" on a real phone): each of
     these already declares its own small width/height AND (since this same
     pass) its own padding: 0, but none had a min-height EXEMPTION — so the
     44px floor from `button, .btn` above still won regardless of the
     explicit height, being a hard floor a smaller height cannot go under.
     .hint-toggle rendered 32x20 instead of 15x15 (padding alone, at
     10px 16px against an explicit 15px box, is enough on its own to inflate
     a border-box element past its declared size — content collapses to 0,
     but the box floors at the padding sum, not the declared width/height);
     .theme-toggle/.sidebar-toggle/.sidebar-close/.site-size-btn/.module-info
     were ALSO stuck at 44px tall from the min-height floor alone, some with
     the same padding inflation on top. Fixed at the root (explicit
     padding: 0 added to each one's own base rule) AND exempted here, same
     belt-and-braces reasoning as draft-save/select-typeahead elsewhere in
     this file: two independent guards outlast one. */
  .theme-toggle, .sidebar-toggle, .sidebar-close, .site-size-btn, .module-info, .dt-arrow {
    min-height: 0;
  }

  /* FILE INPUTS were missed entirely by the rule above, because `file` is not in
     its selector list — so they kept the UA's ~20px height everywhere they
     appear: the LR photo, the delivery receipt, client documents, the letterhead,
     the statement and Excel uploads. Every one of those is something the operator
     taps on a phone. */
  input[type=file] {
    min-height: 36px;
    width: 100%;
    max-width: 100%;
  }

  /* The delivery-receipt cell holds three controls side by side in a 172px
     column. Stacked and full width they are each tappable; in a row they are
     each about a third of 172px. The cell is inside a table that already
     scrolls, so the extra height costs nothing. */
  .tracker-upload {
    flex-direction: column;
    align-items: stretch;
    gap: 5px;
  }
  .tracker-upload .tracker-photo,
  .tracker-upload .cam-btn,
  .tracker-upload .tracker-upload-btn { width: 100%; }

  /* The delivery/proof switch is the primary action on the Delivery Tracker and
     it renders 38x20 — small for a thumb. The whole `.tracker-toggle` label wraps
     both the switch and the days counter, so the effective hit area is already
     wider than the switch looks, but the switch itself is scaled up here so it
     reads as something meant to be touched. Geometry recomputed rather than just
     stretched: knob 20px inset 4px, so the checked travel is 50-4-20-4 = 22px. */
  .toggle-sw { width: 50px; height: 28px; }
  .toggle-sw .slider { border-radius: 14px; }
  .toggle-sw .slider::before { height: 20px; width: 20px; left: 4px; top: 4px; }
  .toggle-sw input:checked ~ .slider::before { transform: translateX(22px); }

  /* A row of buttons wraps instead of overflowing. */
  .form-actions, .actions-row { flex-wrap: wrap; }

  /* CHECKBOXES were rendering 13-15px — the "Date" filter toggle and "Link
     multiple LRs" among them. min-height does not size a checkbox, which is why
     the input rule above never touched them; width/height does.
     `:not(.tracker-cb)` is load-bearing: the Delivery Tracker's checkbox is the
     invisible hit layer inside `.toggle-sw`, sized `width:100%;height:100%` at the
     SAME specificity as this rule — so without the exclusion this would win on
     source order and break the toggle switch into a 22px square. */
  input[type=checkbox]:not(.tracker-cb) {
    width: 22px;
    height: 22px;
    /* `width` is only a BASIS inside a flex row, not a floor. "Link multiple LRs
       for same vehicle" sits in a `display:flex` label next to a long caption, so
       the box was being shrunk straight back to 13px while its height stayed 22 —
       a 13x22 sliver. flex-shrink:0 is what actually holds the size. */
    flex-shrink: 0;
  }

  /* THE TOP BAR'S ACCOUNT CLUSTER. `.topbar .who` carries flex-shrink:0 so that
     the number of module shortcuts can never shove the account controls around
     on a desktop — correct there, but on a phone it pinned the cluster at its
     620px max-content width and that alone made the whole DOCUMENT 655px wide.
     Its own flex-wrap could not help, because an item that refuses to shrink is
     never narrow enough to need to wrap. Given its own row it wraps normally. */
  .topbar { padding: 10px 12px; }
  .topbar .who {
    flex-shrink: 1;
    flex-basis: 100%;
    min-width: 0;
    justify-content: flex-start;
    row-gap: 6px;
  }

  /* THE TOP BAR WAS EATING 316 OF 812 PIXELS — 39% of the screen gone before a
     single row of data. Measured, not estimated: brand row 44 + module nav 153 +
     account row 74 + padding. On a phone you scrolled past a full screen of
     chrome to reach the page you asked for.

     The module nav was 153 of that, because ten chips wrap onto three rows. It is
     NOT hidden here, which was the tempting fix: the off-canvas sidebar is
     "Setup and administration" (Admin, Firm Master, Import, Backup, OCR) and does
     NOT carry the module links, so hiding the nav would leave Dashboard-then-tile
     as the only way between modules. Instead it becomes what a phone nav should
     be — ONE swipeable row. Every link stays reachable, at ~40px instead of 153. */
  .topbar-nav {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    margin-top: 0;
    padding-top: 8px;
  }
  .topbar-nav::-webkit-scrollbar { display: none; }
  /* 38px minimum, because these ten links were measured at less on a phone and
     they are the app's primary navigation — the one place a missed tap costs a
     whole page load. The ⓘ hint buttons stay exempt on purpose (P15): they sit
     inline inside labels and table cells, where growing them pushes every row
     taller for a control that is not the target. */
  .topbar-nav-link { flex: 0 0 auto; min-height: 38px; display: inline-flex; align-items: center; }
  /* A right-edge fade, so the rail reads as "there is more this way". The
     half-cut chip at the edge already implies it, but the fade is what makes it
     unambiguous — and with the scrollbar hidden (correct on a touch device) it is
     the only remaining cue. mask-image degrades to no fade where unsupported,
     which loses the hint and nothing else. */
  .topbar-nav {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent 100%);
    mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent 100%);
  }

  /* Two things in the account row earn their place on a 375px screen and two do
     not. The "Dashboard" link duplicates the brand mark two inches to its left,
     and the six-swatch accent picker is decoration. Both are still on the desktop
     bar; the theme picker stays because dark mode genuinely matters on a phone,
     and the text-size control stays because it is an accessibility control. */
  .topbar .who > a[href="/dashboard"] { display: none; }
  .topbar .who .scheme-picker { display: none; }
  /* The bell and its count say it; the word would push the account row onto
     a second line (23 Oct 2026). A finger needs more than the 38x18 the bell
     measured on a phone (24 Oct, checked for the Android app, whose office
     view is this same page): padding makes the tap area 34px tall and 44px
     wide, and the equal negative margin keeps the row exactly the height it
     was (a min-height grew it by 10px - measured). */
  .topbar .who .notif-word { display: none; }
  .topbar .who .notif-link { padding: 8px 6px; margin: -8px 0; min-width: 44px; justify-content: center; }
  /* THE ACCOUNT ROW, revisited 12 Aug 2026 after the owner reported the phone
     layout directly ("all feature not properly visible, tab are to large").
     It was left at two rows in the previous pass, and the note said the only ways
     to one row were shrinking the text or hiding "Change password" — both wrong.
     There was a third: the TEXT-SIZE and THEME controls are not account links at
     all, and the brand row above has 126 of its 366px standing empty. Lifting
     them there costs nothing and takes a whole row out of the account cluster.

     Absolutely positioned rather than reordered, because they must land on a row
     that is a different flex line. The theme POPOVER still works: it is inside
     .theme-picker and positioned against it, and nothing here sets overflow on an
     ancestor (which is exactly why .who could never become a scroll rail). */
  .topbar { position: relative; }
  .topbar .site-size-ctrl,
  .topbar .theme-picker { position: absolute; top: 6px; }
  .topbar .theme-picker { right: 10px; }
  .topbar .site-size-ctrl { right: 48px; }
  /* The brand row must not run UNDER those two. Measured: the lifted cluster is
     138px wide (217->350 at a 360px viewport) while the brand text ran to 249, so
     they overlapped by 32px. Two changes, and the order matters:

     the brand was shortened to one word (it was text then), and the reserved
     padding below guards whatever the brand becomes later: at 148px nothing can
     slide under the controls.

     11 Oct 2026, the company logo replaced the words. It is drawn 34px tall
     here (85px wide), which fits the room left beside the menu and back
     buttons even at 360px; overflow:hidden still clips rather than collides
     if a future logo is wider. */
  .topbar-left { padding-right: 148px; min-width: 0; }
  .topbar-left .brand-mark { min-width: 0; overflow: hidden; }
  .topbar-left .brand-logo { height: 34px; }
  .topbar .who { gap: 6px 10px; }

  /* THE DASHBOARD WAS 3,776 PIXELS TALL — four and a half screens for nine tiles.
     Measured at 390x844: `aspect-ratio: 1/1` with auto-fill's 180px minimum gives
     ONE 366px column, so every tile was a 366x366 SQUARE holding a title and one
     line of capabilities. That is what the owner was looking at when they said the
     tabs were too large: nothing wrong with the tiles except that each one was the
     size of half a phone.

     Two columns of short cards instead. The square is deliberate on a desktop (a
     grid of equal tiles reads as a launcher) and simply does not survive one
     column, so it is dropped here rather than fought with. */
  .module-grid { grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px; }
  .module-card {
    aspect-ratio: auto;
    min-height: 76px;
    padding: 12px 12px 10px;
    border-radius: 12px;
  }
  .module-card .module-title { font-size: 0.9rem; padding-right: 18px; }
  .module-card .access-row { font-size: 0.72rem; margin-top: 8px; gap: 6px; }
  /* The corner flash is sized for an 18px inset on a 180px card it would cover a
     third of the title. */
  .module-card::before { top: -14px; left: -14px; width: 28px; height: 28px; }

  /* THE NAV RAIL SHOWED 3 OF 10 LINKS at a time — the owner's screenshot cuts off
     at "Delivery Trac". The rail itself is right (see the note above); the chips
     were just built for a desktop bar. Tighter padding and a slightly smaller
     label roughly doubles how much of the app you can see at once, and 38px of
     height is kept because that is the tap target. */
  .topbar-nav { gap: 2px; }
  .topbar-nav-link { padding: 6px 8px; font-size: 0.78rem; }
}

/* ------------------------------------------------- attached documents
   The repeatable upload row shared by the LR, Vehicle and Memo screens
   (src/views/partials/_documents.ejs). One row per document: what it is, an
   optional description, the file. Wraps rather than scrolls, because on a phone
   these are three controls and not a data table. */
.doc-block { max-width: 1000px; }
.doc-row {
  display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
  padding: 5px 0; border-bottom: 1px dashed var(--border);
}
.doc-row:last-child { border-bottom: 0; }
.doc-row select { width: auto; min-width: 210px; margin: 0; }
.doc-row .doc-label { width: auto; min-width: 160px; flex: 1 1 160px; margin: 0; }
.doc-row input[type="file"] { width: auto; flex: 1 1 220px; margin: 0; font-size: 0.8rem; }
.doc-row .cam-btn { margin-left: 0; }
/* 38px so it is a real target on a phone, unlike the inline ⓘ (see P15). */
.doc-row .doc-remove {
  color: var(--text-dim); font-size: 1.1rem; line-height: 1;
  min-width: 38px; min-height: 38px;
}
.doc-row .doc-remove:hover { color: var(--status-danger); }
@media (max-width: 640px) {
  /* One control per line: three of these side by side at 375px leaves each too
     narrow to read the option labels in. */
  .doc-row { flex-direction: column; align-items: stretch; }
  .doc-row select, .doc-row .doc-label, .doc-row input[type="file"] { width: 100%; min-width: 0; }
  /* THE FLEX BASIS HAS TO BE RESET WITH THE AXIS. `flex: 1 1 220px` on the file
     input is a WIDTH basis in the desktop row; the moment this container becomes
     a column, 220px is a HEIGHT — and the file input rendered 220px tall, over a
     quarter of a phone screen for one control. Measured at 360px on the New LR
     screen, not spotted by eye. */
  .doc-row select, .doc-row .doc-label, .doc-row input[type="file"] { flex: 0 0 auto; }
  .doc-row .doc-remove { align-self: flex-end; }
}

/* ------------------------------------------------ File Compress tick-box
   public/js/upload-compress.js inserts this beside the last file input on
   any form that can take an image. Deliberately quiet: it is an option, not
   an instruction, and the default is off (raw upload, untouched). */
.compress-opt {
  display: flex; align-items: center; gap: 6px;
  margin: 6px 0 0; font-size: 0.83rem; color: var(--text-dim);
}
.compress-opt label {
  display: inline-flex; align-items: center; gap: 6px; margin: 0;
  text-transform: none; letter-spacing: normal; font-weight: 400;
  font-size: 0.83rem; color: var(--text-dim); cursor: pointer;
}
.compress-opt input[type="checkbox"] {
  width: 15px; height: 15px; min-width: 15px; margin: 0; flex: 0 0 auto;
}
.compress-status { color: var(--status-ok); font-size: 0.8rem; }

/* ---------------------------------------------- Admin > Signed-in devices
   One block per user, each holding that user table of live devices. The head
   is a flex row so the sign-out-all button sits opposite the name at any
   width, and WRAPS on a phone rather than squeezing the name — per the phone
   layout rules, the button dropping to its own line is correct and a
   250px-wide truncated name is not. */
.device-group { margin-bottom: 18px; }
.device-group-head {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 8px; margin-bottom: 6px;
}
.device-group-head h3 {
  margin: 0; display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  font-size: 0.95rem;
}
.device-group-head form { margin: 0; }
.device-username { color: var(--text-dim); font-size: 0.82rem; }
/* The dash standing in for the sign-out button on the admin own row. Dimmed
   so the column reads as deliberately empty rather than as a missing button. */
.device-self { color: var(--text-dim); }
/* Fixed px widths per the table recipe; the min-width makes anything narrower
   scroll inside .table-scroll instead of moving the whole page. */
.device-table { min-width: 716px; }

/* CLIENT RISK CHIP (12 Oct 2026, P33) — Bill Tracker rows, its By-party table
   and dashboard, and the client credit page. The word is always there; the
   colour only reinforces it. Reasons ride in the title. */
.risk-chip { display: inline-block; font-size: 0.68rem; font-weight: 600; line-height: 1.3; padding: 0 5px;
  border-radius: 3px; border: 1px solid currentColor; cursor: help; white-space: nowrap; }
.risk-chip.risk-high { color: var(--status-danger); }
.risk-chip.risk-medium { color: var(--status-pending); }
.risk-chip.risk-low { color: var(--status-ok); }

/* TRIPS (14 Oct 2026, vehicle tracking S1) — the trip page. The map is 420px on
   a desktop and 45% of the screen on a phone, so the reports table below it is
   always reachable without hunting. */
.trip-map { height: 420px; border: 1px solid var(--border); border-radius: 8px; z-index: 0; }
.trip-status { display: inline-block; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.02em; padding: 1px 7px;
  border-radius: 999px; border: 1px solid currentColor; vertical-align: middle; }
.trip-status-active { color: var(--status-ok); }
.trip-status-paused { color: var(--status-pending); }
.trip-status-ended { color: var(--text-dim); }
.trip-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 0 0 18px; }
.trip-grid > .stub { margin: 0; }
.trip-plan-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.trip-inline { margin: 0 0 10px; }
.trip-inline-row { display: flex; gap: 6px; }
.trip-inline-row input { flex: 1 1 auto; min-width: 0; }
.trip-newlink input { width: 100%; }
/* ONE-TAP SHARING (19 Oct 2026, partials/_share_link): a row per person with a
   number on record - who, then WhatsApp / SMS - and "someone else" last. */
.share-box { margin: 10px 0; }
.share-rows { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
.share-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
.share-row .share-who { min-width: 220px; font-size: 0.86rem; }
/* The message itself, shown so the person sees what they will send (24 Oct 2026). */
.share-box .share-msg { display: block; width: 100%; box-sizing: border-box; font-size: 0.86rem; line-height: 1.4;
  resize: vertical; min-height: 4.4em; margin-top: 4px; }
/* The LR screen's Tracking box: the trip line, then a row per link - its label,
   what became of it (partials/_link_status), Copy / send, New link - and the
   driver's mobile (24 Oct 2026). */
.lr-tracking { display: flex; flex-direction: column; gap: 6px; margin: 0 0 12px; max-width: 1000px; }
form.lr-tracking { flex-direction: row; align-items: center; flex-wrap: wrap; gap: 8px; }
.lr-tracking-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
.lr-tracking-actions form { display: inline; margin: 0; }
.lr-trip-top { font-size: 0.85rem; margin: 0 0 10px; }
.lr-link-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; margin: 0; font-size: 0.85rem; }
.lr-link-row form { display: inline; margin: 0; }
.lr-link-row .lr-link-label { min-width: 96px; font-weight: 600; margin: 0; text-transform: none; letter-spacing: normal; font-size: 0.85rem; }
.lr-link-row .lr-driver-phone { width: 150px; }
.link-status { color: var(--text-dim); }
.link-state.ok { color: var(--status-ok); font-weight: 600; }
.link-state.warn { color: var(--status-pending); font-weight: 600; }
.lr-link-msg { font-size: 0.85rem; }
.lr-link-msg.ok { color: var(--status-ok); }
.lr-link-msg.err { color: var(--status-danger); }
@media (max-width: 640px) {
  .share-row .share-who { min-width: 0; flex-basis: 100%; }
  .lr-link-row .lr-link-label { min-width: 0; flex-basis: 100%; }
}
.trip-flag { display: inline-block; font-size: 0.68rem; line-height: 1.3; padding: 0 5px; margin: 1px 0;
  border-radius: 3px; border: 1px solid var(--status-pending); color: var(--status-pending); white-space: nowrap; }
.trip-flag.ok { border-color: var(--status-ok); color: var(--status-ok); }
/* Alerts (slice S2, 15 Oct 2026): the trip page's open alerts and the
   dashboard strip. Red until acknowledged, then quiet but still there. */
.trip-alerts { display: flex; flex-direction: column; gap: 6px; margin: 0 0 12px; }
.trip-alert { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap;
  padding: 8px 12px; border: 1px solid var(--status-danger); border-left-width: 4px; border-radius: 6px;
  background: var(--bg-elevated); }
.trip-alert strong { color: var(--status-danger); }
.trip-alert.acked { border-color: var(--border); }
.trip-alert.acked strong { color: var(--text); }
.trip-alert form { margin: 0; }
.trip-alert > div:first-child { flex: 1 1 320px; min-width: 0; }
.trip-alert-actions { display: flex; gap: 6px; align-items: center; }
/* The Delivery Tracker's trip line under the vehicle (green / amber / red). */
/* white-space:normal because it sits in the Vehicle cell, which is nowrap so a
   plate never breaks - the line under it must wrap instead of spilling. */
.trip-line { font-size: 0.72rem; line-height: 1.25; white-space: normal; overflow-wrap: break-word; }
.trip-line.ok { color: var(--status-ok); }
.trip-line.warn { color: var(--status-pending); }
.trip-line.bad { color: var(--status-danger); font-weight: 600; }
.trip-line a { color: inherit; }
@media (max-width: 1100px) { .trip-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 640px) {
  .trip-grid { grid-template-columns: 1fr; }
  .trip-map { height: 45vh; }
  .trip-plan-row { grid-template-columns: 1fr; }
}

/* TALLY SYNC (19 Oct 2026). The firm's tabs, the "Tally data last read" line on
   every Tally page, and Dr / Cr amounts. */
/* .view-tabs: the same tabs elsewhere (the Delivery Tracker's LRs | Trucks,
   24 Oct 2026). */
.tally-tabs, .view-tabs { display: flex; flex-wrap: wrap; gap: 4px; margin: 6px 0 10px; border-bottom: 1px solid var(--border); }
.tally-tabs a, .view-tabs a { padding: 6px 12px; border: 1px solid transparent; border-bottom: none; border-radius: 6px 6px 0 0;
  color: var(--text-dim); font-size: 0.88rem; margin-bottom: -1px; }
.tally-tabs a:hover, .view-tabs a:hover { text-decoration: none; color: var(--text); }
.tally-tabs a.on, .view-tabs a.on { color: var(--text); background: var(--panel); border-color: var(--border); font-weight: 600; }
/* THE CASH REGISTER (24 Oct 2026, Bank Reconciliation -> Cash). The client /
   bill and the free-text name swap by head: display:none here, .shown from
   public/js/cash-register.js (the app's one show/hide pattern). */
.cash-kind { display: flex; flex-wrap: wrap; gap: 6px 18px; margin: 0 0 8px; }
.cash-kind-opt { display: flex; align-items: center; gap: 6px; margin: 0; font-weight: 600; text-transform: none; letter-spacing: normal; font-size: 0.95rem; }
.cash-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 4px 14px; }
.cash-grid label { margin-top: 4px; }
.cash-grid .cash-note { grid-column: 1 / -1; }
.cash-client, .cash-party { display: none; }
.cash-client.shown, .cash-party.shown { display: block; }
.cash-head-stats { display: flex; flex-wrap: wrap; gap: 14px 40px; align-items: flex-end; margin: 4px 0 0; }
/* BANK LINES BY TYPE (25 Oct 2026, Bank Reconciliation -> Bank lines): one
   card per line; the two dropdowns, then only the box the type needs
   (display:none here, .shown from public/js/bank-lines.js). */
.bl-section { max-width: 1200px; margin: 0 0 18px; }
.bl-section h2 { font-size: 1rem; margin: 6px 0 8px; }
.bl-row { border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; margin: 0 0 8px; background: var(--panel); }
.bl-row.bl-err { border-color: var(--status-danger); }
.bl-row:target { border-color: var(--accent-line); }
.bl-head { display: flex; gap: 6px 12px; align-items: baseline; flex-wrap: wrap; min-width: 0; }
.bl-date { color: var(--text-dim); font-size: 0.84rem; }
.bl-amt { font-weight: 600; white-space: nowrap; }
.bl-amt.in { color: var(--status-ok); }
.bl-who { font-weight: 600; overflow-wrap: break-word; min-width: 0; }
.bl-narr { flex: 1 1 260px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bl-pick { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-top: 6px; }
.bl-pick .bl-group { width: 160px; }
.bl-pick .bl-type { width: 320px; max-width: 100%; }
.bl-f { display: none; }
.bl-f.shown { display: inline-flex; }
.bl-f input { width: 230px; max-width: 100%; }
.bl-f select { width: 210px; max-width: 100%; }
.bl-why { margin-top: 4px; }
/* A line's documents (27 Oct 2026): an upload or a "not needed" per row. */
.doc-up { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.doc-up input[type="text"] { width: 170px; max-width: 100%; }
.doc-up input[type="file"] { max-width: 100%; }
/* A truck's papers (27 Oct 2026, P62 phase 3): on the memo / Vehicle page, and
   live under the truck number on New Memo and the LR (public/js/truck-papers.js
   adds .shown once it has something to say). */
.papers-box { display: none; margin-top: 6px; margin-bottom: 12px; font-size: 0.86rem; }
.papers-box.shown { display: block; }
.papers-list { display: flex; flex-wrap: wrap; gap: 2px 14px; margin-top: 4px; }
@media (max-width: 640px) {
  .bl-pick .bl-group, .bl-pick .bl-type, .bl-f, .bl-f input, .bl-f select { width: 100%; }
  .bl-f.shown { display: flex; }
}
.cash-totals td { border-top: 2px solid var(--border-strong); font-weight: 600; }
.tally-fresh { font-size: 0.82rem; color: var(--text-dim); margin: 0 0 12px; }
.tally-fresh.stale { color: var(--status-pending); font-weight: 600; }
.tally-stat-label { color: var(--text-dim); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.06em; }
.tally-stat-value { font-size: 1.2rem; font-weight: 600; margin-top: 4px; }
.tally-stat-sub { font-size: 0.75rem; margin-top: 2px; color: var(--text-dim); }
.tally-amt { text-align: right; white-space: nowrap; }
.tally-dr { color: var(--text); }
.tally-cr { color: var(--status-ok); }
.tally-sub { font-size: 0.74rem; color: var(--text-dim); line-height: 1.25; overflow-wrap: break-word; }
.tally-badge { display: inline-block; font-size: 0.68rem; line-height: 1.3; padding: 0 5px; border: 1px solid var(--border-strong);
  border-radius: 4px; color: var(--text-dim); white-space: nowrap; }
.tally-badge.warn { border-color: var(--status-pending); color: var(--status-pending); }
.tally-badge.bad { border-color: var(--status-danger); color: var(--status-danger); }
.tally-steps { margin: 6px 0 0; padding-left: 20px; line-height: 1.55; font-size: 0.9rem; }
.tally-steps kbd { font-family: var(--mono); font-size: 0.82rem; padding: 0 4px; border: 1px solid var(--border-strong); border-radius: 3px; background: var(--panel-quiet); }
.tally-group-row td { background: var(--panel-quiet); font-weight: 600; font-size: 0.8rem; }
.tally-grid-2 { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; align-items: start; }
@media (max-width: 900px) { .tally-grid-2 { grid-template-columns: 1fr; } }

/* TALLY SYNC PHASE 3 (20 Oct 2026): the bank lines review. Each line is two
   rows - the bank's line, then its draft as one wrapping row of controls. */
.tally-badge.ok { border-color: var(--status-ok); color: var(--status-ok); }
.tally-bank .tally-line-head td { border-bottom: none; }
.tally-row-form, .tally-row-done { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.tally-row-form select, .tally-row-form input[type="text"] { padding: 3px 6px; font-size: 0.82rem; min-width: 0; }
.tally-row-form .tally-vt { width: 96px; }
.tally-row-form .tally-ledger { width: 220px; }
.tally-row-form .tally-billtype { width: 112px; }
.tally-row-form .tally-billname { width: 140px; }
.tally-row-form .tally-cc { width: 120px; }
.tally-row-form .tally-narr { flex: 1 1 240px; }
.tally-row-form .tally-confirm { padding: 3px 12px; }
.tally-edit-lines { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
.tally-edit-line { border: 1px solid var(--border); border-radius: 6px; padding: 8px; }
.tally-edit-line .tally-edit-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 4px; }
.tally-edit-line input[type="text"], .tally-edit-line input[type="number"], .tally-edit-line select { padding: 3px 6px; font-size: 0.84rem; }
@media (max-width: 640px) {
  .tally-row-form .tally-ledger, .tally-row-form .tally-narr { flex: 1 1 100%; width: auto; }
}
/* Tally Sync phase 4 (20 Oct 2026): a tick-box line in normal case - the
   app's label style is upper-case, too loud for a sentence. */
label.tally-check { display: flex; gap: 6px; align-items: center; margin: 8px 0 0; font-size: 0.86rem;
  text-transform: none; letter-spacing: normal; font-weight: 400; }
label.tally-check.ask { color: var(--status-pending); font-weight: 600; }
label.tally-check input[type="checkbox"] { width: auto; margin: 0; }
/* Phase 5: Settings -> Ledgers for bills, one row per part of a bill. */
.tally-bill-ledgers { display: grid; grid-template-columns: minmax(0, 200px) minmax(0, 1fr); gap: 6px 12px; align-items: start; margin-top: 6px; }
.tally-bill-ledgers label { margin: 6px 0 0; }
@media (max-width: 640px) { .tally-bill-ledgers { grid-template-columns: minmax(0, 1fr); } }
/* Settings -> Connection: only the chosen way's fields (P58). Hidden until the
   page's script (or the server, for the saved choice) adds .shown. */
.ts-switch { display: none; }
.ts-switch.shown { display: block; }
/* Phase 6b: the cash book - its add line and the cash-in-hand strip. */
.tally-cash-head { display: flex; gap: 28px; align-items: flex-end; flex-wrap: wrap; }
.tally-cash-form { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.tally-cash-form input, .tally-cash-form select { padding: 4px 6px; font-size: 0.86rem; min-width: 0; }
.tally-cash-form input[type="date"] { width: 140px; }
.tally-cash-form select[name="direction"] { width: 96px; }
.tally-cash-form .tally-cash-amt { width: 100px; }
.tally-cash-form .tally-cash-ledger { width: 210px; }
.tally-cash-form .tally-cash-billtype { width: 104px; }
.tally-cash-form .tally-cash-bill { width: 100px; }
.tally-cash-form .tally-cash-cc { width: 130px; }
.tally-cash-form .tally-cash-narr { flex: 1 1 160px; }
@media (max-width: 640px) {
  .tally-cash-form .tally-cash-ledger, .tally-cash-form .tally-cash-narr { flex: 1 1 100%; width: auto; }
}
/* Phase 6c: a trip settlement's expense lines, two to a row on a desktop. */
.tally-settle-lines { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px 18px; margin-top: 8px; }
@media (max-width: 760px) { .tally-settle-lines { grid-template-columns: minmax(0, 1fr); } }

/* THE NARROWEST PHONES (24 Oct 2026, checked for the Android app, whose office
   view is these same pages). At 320px the brand row - menu, back, logo, and the
   148px kept free for the lifted text-size and theme controls - needed 337px:
   the page could be swiped 17px sideways and the logo ran 13px under "A-".
   Tighter gaps and a 28px logo make it 310px, logo clear of the controls;
   phones 341px and wider are untouched. Android's "Display size: large" can
   bring an ordinary phone down to this width. */
@media (max-width: 340px) {
  .topbar-left { gap: 8px; }
  .topbar-left .brand-logo { height: 28px; }
}
/* TOUCH SCREENS: a bigger invisible tap area for the tiny icon buttons
   (31 Oct 2026, pre-deployment sweep at 375px). The ⓘ hint (15px) and a
   column's filter ▾ (14px) were too small to hit with a thumb on 36 screens;
   the owner wants the ⓘ to LOOK small (14 Aug, "the i button is larger"), so
   the drawn size stays and only a transparent ::after grows the target. Kept
   shallower than wide so it does not reach into the field under a label. */
@media (pointer: coarse) {
  .hint-toggle { position: relative; }
  .hint-toggle::after, .col-filter-btn::after { content: ''; position: absolute; inset: -6px -9px; }
}

/* PHONE: THE LR'S GOODS ROW (2 Nov 2026, owner: LR screen "not cluttered").
   The fixed columns (Qty, Rate, Freight, Hamali, Other, Total) left the
   description ("Particular") a few pixels on a phone - "Goo de..." - and the
   Rate box showed "e.g". At phone width the table is given room and scrolls
   inside its own box (it already sits in an overflow-x box), so every box is
   readable. Desktop and laptop are untouched. */
@media (max-width: 640px) {
  .line-items-table { min-width: 860px; }
}

/* KEYBOARD SHORTCUTS (2 Nov 2026, public/js/keyboard.js). The top bar's button
   and the list it opens - the app's display:none + .shown pattern. */
.kb-help-btn {
  background: none; border: 1px solid var(--border-strong); border-radius: 6px;
  color: var(--text-dim); font: inherit; font-size: 0.8rem; padding: 2px 8px; min-height: 0; cursor: pointer;
  align-items: center; gap: 4px; white-space: nowrap;
}
.kb-help-btn:hover { color: var(--accent); border-color: var(--accent); }
.kb-help { display: none; position: fixed; inset: 0; z-index: 2000; background: rgba(0, 0, 0, 0.35); align-items: center; justify-content: center; }
.kb-help.shown { display: flex; }
.kb-help-box { background: var(--panel); color: var(--text); border: 1px solid var(--border-strong); border-radius: 10px; padding: 16px 18px; max-width: 460px; width: calc(100% - 32px); max-height: 85vh; overflow: auto; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); }
.kb-help-title { display: flex; justify-content: space-between; align-items: center; font-weight: 600; margin-bottom: 10px; }
.kb-help-close { background: none; border: 0; color: var(--text-dim); font-size: 1.3rem; line-height: 1; cursor: pointer; padding: 0 4px; min-height: 0; }
.kb-help table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
.kb-help td { padding: 5px 6px; border-bottom: 1px solid var(--border); }
.kb-help td:first-child { white-space: nowrap; width: 1%; }
.kb-help kbd { font-family: var(--mono); font-size: 0.8rem; background: var(--panel-quiet); border: 1px solid var(--border-strong); border-bottom-width: 2px; border-radius: 4px; padding: 1px 6px; }
/* Below 1100px the word goes and the key icon stays: at 945px the word alone
   pushed the top bar 27px past the screen. Below 900px (tablet, phone) the
   button goes: there is rarely a keyboard, and ? still opens the list. */
@media (max-width: 1100px) { .kb-help-word { display: none; } }
@media (max-width: 900px) { .kb-help-btn { display: none; } }
