/* ═══════════════════════════════════════════════════════════
   Pool App — App Styles
   ═══════════════════════════════════════════════════════════ */

/* ── CSS Variables (Pool Blue default) ──────────────────── */
:root {
  /* Theme palette — overridden at runtime by theme.js light/dark per palette */
  --bg:            #edf5ff;
  --card:          #ffffff;
  --border:        #c2dcf5;
  --text:          #0c2a48;
  --text-muted:    #5278a0;
  --accent:        #1a7ec8;
  --accent-light:  #d8ecfb;
  --danger:        #c4533a;
  --danger-light:  #fdf0ed;
  --warning:       #b8860b;
  --warning-light: #fef9e6;
  --success:       #10985f;
  --success-light: #d8f3e5;
  --info:          #2c8fcb;
  --info-light:    #d4ebf9;
  --blue:          #1a7ec8;
  --blue-light:    #d8ecfb;
  --purple:        #7c5cbf;
  --purple-light:  #f0ebfa;
  --header-bg:     #0c2a48;

  /* Spacing scale (4-pt grid). Use --space-* over magic numbers. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;

  /* Radius scale */
  --radius-sm:   4px;
  --radius-md:   8px;
  --radius-lg:   12px;
  --radius-pill: 999px;
  --radius:      10px; /* legacy alias used by .card */

  /* Z-index scale */
  --z-base:    1;
  --z-nav:     100;
  --z-overlay: 149;
  --z-sidebar: 150;
  --z-modal:   200;
  --z-popover: 300;
  --z-toast:   500;

  --shadow:        0 2px 12px rgba(0,0,0,0.08);
  --font-family:   system-ui, -apple-system, 'Segoe UI', sans-serif;

  /* Scalable font sizes */
  --fs-xs:   11px;
  --fs-sm:   12px;
  --fs-base: 14px;
  --fs-md:   15px;
  --fs-lg:   17px;
  --fs-xl:   20px;
  --fs-h2:   18px;
}
html[data-font-size="s"]  { --fs-xs:10px; --fs-sm:11px; --fs-base:13px; --fs-md:14px; --fs-lg:15px; --fs-xl:18px; --fs-h2:16px; }
html[data-font-size="l"]  { --fs-xs:12px; --fs-sm:14px; --fs-base:16px; --fs-md:17px; --fs-lg:19px; --fs-xl:22px; --fs-h2:20px; }
html[data-font-size="xl"] { --fs-xs:13px; --fs-sm:15px; --fs-base:18px; --fs-md:19px; --fs-lg:22px; --fs-xl:26px; --fs-h2:23px; }

* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }

/* ── Layout / spacing utilities ─────────────────────────────
   Prefixed `.u-` so they never collide with component classes. Composable;
   use them instead of inline `style="display:flex; gap:8px; ..."` blocks. */
.u-row         { display: flex; align-items: center; gap: var(--space-2); }
.u-row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.u-row-start   { display: flex; align-items: flex-start; gap: var(--space-2); }
.u-col         { display: flex; flex-direction: column; }
.u-wrap        { flex-wrap: wrap; }
.u-flex-1      { flex: 1; min-width: 0; }
.u-w-full      { width: 100%; }
.u-center      { text-align: center; }
.u-gap-1 { gap: var(--space-1); } .u-gap-2 { gap: var(--space-2); }
.u-gap-3 { gap: var(--space-3); } .u-gap-4 { gap: var(--space-4); }
.u-mt-1 { margin-top: var(--space-1); }    .u-mt-2 { margin-top: var(--space-2); }
.u-mt-3 { margin-top: var(--space-3); }    .u-mt-4 { margin-top: var(--space-4); }
.u-mb-1 { margin-bottom: var(--space-1); } .u-mb-2 { margin-bottom: var(--space-2); }
.u-mb-3 { margin-bottom: var(--space-3); } .u-mb-4 { margin-bottom: var(--space-4); }
.u-mb-0 { margin-bottom: 0; }
.u-muted     { color: var(--text-muted); }
.u-text-xs   { font-size: var(--fs-xs); }
.u-text-sm   { font-size: var(--fs-sm); }
.u-text-md   { font-size: var(--fs-md); }
.u-text-lg   { font-size: var(--fs-lg); }
.u-bold      { font-weight: 700; }

html, body {
  font-family: var(--font-family);
  background: var(--bg);
  color: var(--text);
  font-size: var(--fs-base);
  line-height: 1.5;
  min-height: 100vh;
  overscroll-behavior: none;
  /* Opt OUT of the browser's own text inflation. Chrome on Android multiplies
     every font-size by the OS/Chrome "text scaling" accessibility factor, and
     that factor can change without anyone touching this app — a system font
     bump, a display-size change, an accidental drag of Chrome's slider. The
     tech reads it as "the screen size randomly changed": text balloons, the
     stop cards squeeze, and addresses wrap one word per line.
     The app already owns text size — Settings offers S/M/L/XL, which drive the
     --fs-* tokens above — so the browser's second, invisible multiplier on top
     of ours is pure interference. This pins the page to our own scale.
     Accessibility is not lost: pinch-zoom still works (the viewport meta is not
     locked) and the in-app XL setting is the supported large-text path. */
  -webkit-text-size-adjust: 100%;
          text-size-adjust: 100%;
}

/* ── Login ──────────────────────────────────────────────── */
#login-screen {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px 16px;
  background: var(--header-bg);
}
.login-card {
  background: var(--card);
  border-radius: 16px;
  padding: 36px 28px;
  width: 100%;
  max-width: 380px;
  box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.login-logo { text-align: center; margin-bottom: 28px; }
.login-logo-icon { font-size: 40px; margin-bottom: 10px; }
.login-logo-img { height: var(--logo-h, 64px); width: auto; display: block; margin: 0 auto 8px; object-fit: contain; }
.logo-preview   { max-height: var(--logo-h, 64px); width: auto; border-radius: var(--radius-sm); border: 1px solid var(--border); padding: 4px; background: #fff; object-fit: contain; }
.sidebar-logo {
  height: 100%;
  width: auto;
  min-height: 26px;
  max-height: 64px;
  object-fit: contain;
}
.login-logo h1 { font-size: var(--fs-h2); font-weight: 700; line-height: 1.3; color: var(--text); }
.login-logo p { font-size: var(--fs-xs); color: var(--text-muted); letter-spacing: 0.08em; text-transform: uppercase; margin-top: 4px; }

/* ── App Shell ──────────────────────────────────────────── */
#app { display: flex; flex-direction: column; min-height: 100vh; }

/* ── Header ─────────────────────────────────────────────── */
.app-header {
  background: var(--header-bg);
  color: #f0f4ff;
  padding: 13px 16px;
  padding-top: max(13px, env(safe-area-inset-top));
  display: flex;
  align-items: center;
  justify-content: space-between;
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  gap: 10px;
}
.header-left { display: flex; align-items: center; gap: 10px; }
.header-title { font-size: var(--fs-lg); font-weight: 700; letter-spacing: -0.02em; }
.beta-badge { display: inline-block; font-size: 9px; font-weight: 600; letter-spacing: 0.04em; background: var(--warning); color: var(--warning-light); border-radius: var(--radius-sm); padding: 1px 4px; vertical-align: middle; margin-left: 5px; }
.header-sub { font-size: var(--fs-xs); color: rgba(255,255,255,0.5); letter-spacing: 0.1em; text-transform: uppercase; display: none; }
.header-right { display: flex; align-items: center; gap: 10px; }
#header-date { font-size: var(--fs-xs); color: rgba(255,255,255,0.55); }
.btn-icon { background: none; border: none; color: rgba(255,255,255,0.6); cursor: pointer; font-size: 18px; padding: 4px; }

/* ── Sync queue pill (offline mutations indicator) ──────── */
.sync-pill {
  display: inline-flex; align-items: center; gap: 5px;
  background: rgba(255,255,255,0.10);
  color: #f0f4ff;
  border: none; border-radius: var(--radius-pill);
  padding: 3px 9px 3px 8px;
  font-size: 12px; font-weight: 700;
  cursor: pointer;
  transition: background .15s;
}
.sync-pill[hidden] { display: none; }
/* The glyph IS the label. This used to be a 7px coloured dot, so a tech told in
   words to "open the ⏳ in the header" had nothing to look for — see the
   contract comment in sync-queue.js. State rides the pill's own background,
   exactly like .email-alert-pill below, rather than the glyph's colour: ⏳ is
   Emoji_Presentation, so Android paints it in colour and ignores `color:`
   outright, and tinting only ⚠ (which is text-presentation and would obey)
   would leave the two states signalled inconsistently. */
.sync-glyph {
  font-size: 13px; line-height: 1;
  animation: sync-glyph-pulse 1.6s ease-in-out infinite;
}
.sync-pill[data-state="pending"] { background: rgba(245,166,35,0.26); }
.sync-pill[data-state="failed"]  { background: rgba(220,53,69,0.28); }
/* Hover brightens rather than REPLACING the background: the tint is now the
   only thing carrying state, and a background swap here would erase it — on
   Android :hover sticks after a tap, so the red pill would sit washed white
   until the user tapped elsewhere. (.email-alert-pill dodges this by ordering
   its swap before its state rules, which just makes the hover dead; this keeps
   the affordance and the state.) */
.sync-pill:hover { filter: brightness(1.35); }
/* Nothing is on its way once a row has been refused — a pulsing "still working
   on it" is the wrong thing to say when what it needs is a tap. */
.sync-pill[data-state="failed"] .sync-glyph { animation: none; }
@keyframes sync-glyph-pulse { 0%,100% { opacity: 1; } 50% { opacity: .45; } }
/* The glyph is bigger and busier than the dot it replaced; respect the setting. */
@media (prefers-reduced-motion: reduce) {
  .sync-glyph { animation: none; }
}

.sync-list { display: flex; flex-direction: column; gap: 6px; max-height: 60vh; overflow-y: auto; }
.sync-row {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.sync-row[data-state="failed_4xx"],
.sync-row[data-state="failed_auth"],
.sync-row[data-state="failed_retries"] { border-color: var(--danger); }
.sync-row-main { flex: 1; min-width: 0; }
.sync-row-target { font-size: 14px; font-weight: 600; color: var(--text); }
.sync-row-meta   { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
.sync-empty { padding: 24px 12px; text-align: center; color: var(--text-muted); font-size: 13px; }

/* ── Admin alert pills (email delivery + system alerts) ─── */
/* assets/js/system-alerts.js reuses these classes verbatim — the two pills
   sit side by side in the header and must look like one family. */
.email-alert-pill {
  display: inline-flex; align-items: center; gap: 5px;
  background: rgba(255,255,255,0.10);
  color: #f0f4ff;
  border: none; border-radius: var(--radius-pill);
  padding: 3px 9px 3px 8px;
  font-size: 12px; font-weight: 700;
  cursor: pointer;
  transition: background .15s;
}
.email-alert-pill:hover { background: rgba(255,255,255,0.18); }
.email-alert-pill[hidden] { display: none; }
.email-alert-icon { font-size: 13px; line-height: 1; }
.email-alert-pill[data-state="failed"]    { background: rgba(220,53,69,0.28); }
.email-alert-pill[data-state="uncertain"] { background: rgba(245,166,35,0.26); }

.email-alert-list { display: flex; flex-direction: column; gap: 8px; max-height: 60vh; overflow-y: auto; }
.email-alert-row {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 10px 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
}

/* ── Hamburger button ───────────────────────────────────── */
.hamburger-btn {
  background: none;
  border: none;
  color: #f0f4ff;
  font-size: 22px;
  cursor: pointer;
  padding: 2px 6px;
  line-height: 1;
  border-radius: 6px;
  flex-shrink: 0;
  transition: background .15s;
}
.hamburger-btn:active { background: rgba(255,255,255,0.12); }

/* ── Sidebar overlay ────────────────────────────────────── */
#sidebar-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.45);
  z-index: var(--z-overlay);
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s;
}
#sidebar-overlay.open { opacity: 1; pointer-events: all; }

/* ── Sidebar ────────────────────────────────────────────── */
#sidebar {
  position: fixed;
  top: 0; left: 0;
  width: 260px;
  height: 100%;
  background: var(--header-bg);
  z-index: var(--z-sidebar);
  transform: translateX(-100%);
  transition: transform .25s ease;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  padding-top: env(safe-area-inset-top);
}
#sidebar.open { transform: translateX(0); }

.sidebar-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 16px 8px;
}
/* Logo and name live in separate grid cells so a wrapped name stays in
   its own column instead of flowing under the logo. The grid row stretches
   to fit the (possibly wrapped) name; the logo fills that height (capped)
   so a 2-line brand gets a proportionally bigger icon. */
.sidebar-title {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: stretch;
  gap: 8px;
  font-size: var(--fs-lg);
  font-weight: 700;
  color: #f0f4ff;
  min-width: 0;
  min-height: 32px;
}
.sidebar-name {
  align-self: center;
  text-align: center;
  min-width: 0;
  line-height: 1.2;
  word-break: break-word;
}
#sidebar-close {
  background: none; border: none; color: rgba(255,255,255,0.5);
  font-size: 24px; cursor: pointer; line-height: 1; padding: 2px 4px;
}

.sidebar-user-info {
  padding: 10px 16px 14px;
  border-bottom: 1px solid rgba(255,255,255,0.1);
  margin-bottom: 6px;
}
.sidebar-username { font-size: var(--fs-sm); font-weight: 600; color: #f0f4ff; }
.sidebar-role { font-size: var(--fs-xs); color: rgba(255,255,255,0.45); text-transform: uppercase; letter-spacing: .08em; margin-top: 2px; }

.sidebar-nav { display: flex; flex-direction: column; flex: 1; }

#sidebar .nav-btn {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 12px 16px;
  border: none;
  border-radius: 0;
  background: transparent;
  color: rgba(255,255,255,0.65);
  font-size: var(--fs-sm);
  font-weight: 500;
  cursor: pointer;
  font-family: inherit;
  transition: background .15s;
  white-space: nowrap;
  flex-shrink: 0;
}
#sidebar .nav-btn:hover  { background: rgba(255,255,255,0.08); }
#sidebar .nav-btn.active { background: rgba(255,255,255,0.15); color: #fff; font-weight: 700; }

.sidebar-logout {
  margin-top: auto;
  padding: 14px 16px;
  border-top: 1px solid rgba(255,255,255,0.1);
  background: none;
  border-left: none; border-right: none; border-bottom: none;
  color: rgba(255,255,255,0.5);
  font-size: var(--fs-sm);
  font-family: inherit;
  cursor: pointer;
  text-align: left;
  display: flex;
  align-items: center;
  gap: 10px;
  padding-bottom: max(14px, env(safe-area-inset-bottom));
  transition: color .15s;
}
.sidebar-logout:hover { color: rgba(255,255,255,0.8); }

/* ── Main content ───────────────────────────────────────── */
.app-main { flex: 1; max-width: 700px; width: 100%; margin: 0 auto; padding: 20px 14px 80px; }

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

/* ── Bottom nav (mobile only) ───────────────────────────── */
.bottom-nav {
  display: none; /* Shown via @media below — desktop keeps the sidebar */
  position: fixed;
  bottom: 0; left: 0; right: 0;
  background: var(--header-bg);
  border-top: 1px solid rgba(255,255,255,0.08);
  z-index: var(--z-nav);
  padding-bottom: env(safe-area-inset-bottom);
}
.bottom-nav-btn {
  flex: 1;
  min-width: 0; /* allow children to ellipsize instead of pushing siblings */
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px;
  padding: 8px 4px 6px;
  min-height: 56px;
  background: transparent;
  border: none;
  color: rgba(255,255,255,0.6);
  font-family: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: rgba(255,255,255,0.08);
  transition: color .12s, background .12s;
}
.bottom-nav-btn:active { background: rgba(255,255,255,0.08); }
.bottom-nav-btn.active {
  color: #fff;
  /* Slim accent indicator at top — replaces the sidebar's left bar */
  box-shadow: inset 0 2px 0 0 var(--accent);
}
/* Icon + label scale with the user's chosen --fs-* size (Settings →
   Appearance → Text Size). Label uses a slightly tighter step than --fs-xs
   so it fits in a 1/5-screen-wide column at the default size; truncates
   with an ellipsis at the largest sizes rather than wrapping. */
.bottom-nav-btn .bn-icon  { font-size: var(--fs-xl);  line-height: 1; }
.bottom-nav-btn .bn-label {
  font-size: calc(var(--fs-xs) - 1px);
  font-weight: 600;
  letter-spacing: .02em;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Nav count badges ───────────────────────────────────── */
/* Today: the open work-order count (index.php, data-live="work-orders").
   Hidden via [hidden] at zero, so a cleared board shows a clean nav. */
#sidebar .nav-btn .nav-badge {
  margin-left: auto;              /* pushes the pill to the row's right edge */
  flex: none;
  background: var(--accent);
  color: #fff;
  font-size: calc(var(--fs-xs) - 1px);
  font-weight: 700;
  line-height: 1;
  padding: 3px 7px;
  border-radius: 999px;
  min-width: 20px;
  text-align: center;
}
/* Bottom bar: a pill on the icon's top-right. Absolute so it can't disturb
   the icon/label column or push a 1/5-width button out of alignment. */
.bottom-nav-btn { position: relative; }
.bottom-nav-btn .nav-badge {
  position: absolute;
  top: 3px;
  left: 50%;
  margin-left: 5px;
  background: var(--accent);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  padding: 2px 5px;
  border-radius: 999px;
  min-width: 16px;
  text-align: center;
}

@media (max-width: 720px) {
  .bottom-nav { display: flex; }
  /* Add the bottom-nav height to .app-main so cards don't sit under the bar */
  .app-main { padding-bottom: calc(80px + env(safe-area-inset-bottom)); }
}

/* ── Cards ──────────────────────────────────────────────── */
.card {
  background: var(--card);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  padding: 16px;
  margin-bottom: 12px;
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
  gap: 8px;
  flex-wrap: wrap;
}
.card-title { font-size: var(--fs-lg); font-weight: 700; }
.section-label {
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  margin-bottom: 8px;
}

/* ── Buttons ────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 9px 18px;
  border: none;
  border-radius: var(--radius-md);
  font-size: var(--fs-base);
  font-weight: 600;
  cursor: pointer;
  transition: all .15s;
  font-family: inherit;
  white-space: nowrap;
  text-decoration: none;
  min-height: 40px; /* gloved-hand touch target */
}
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn-primary  { background: var(--accent);       color: #fff; }
.btn-danger   { background: var(--danger-light);  color: var(--danger); }
.btn-warning  { background: var(--warning-light); color: var(--warning); }
.btn-blue     { background: var(--blue-light);    color: var(--blue); }
.btn-ghost    { background: transparent;          color: var(--text-muted); }
.btn-outline  { background: transparent;          color: var(--text); border: 1px solid var(--border); }
.btn-danger-outline { background: transparent; color: var(--danger); border: 1.5px solid var(--danger); }
.btn-sm       { padding: 7px 12px; font-size: var(--fs-sm); min-height: 32px; }
.btn-full     { width: 100%; }

/* ── Forms ──────────────────────────────────────────────── */
.field { margin-bottom: 14px; }
.field label {
  display: block;
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  margin-bottom: 5px;
}
input:not([type]), input[type=text], input[type=email], input[type=password],
input[type=date], input[type=number], input[type=tel], input[type=search],
select, textarea {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-size: var(--fs-base);
  font-family: inherit;
  background: var(--bg);
  color: var(--text);
  outline: none;
  transition: border .15s;
  /* Normalize intrinsic height so a <select> and <input> with the same padding
     and font-size end up the same pixel height. Selects otherwise default to
     content-box and a chevron-aware line-height that makes them taller. */
  box-sizing: border-box;
  line-height: 1.3;
}
/* Strip platform chrome only on text-like inputs (where it's noisy and the
   custom border looks better). Leave date/number/select alone so iOS/Android
   pickers and the native select chevron continue to work as users expect. */
input:not([type]), input[type=text], input[type=email], input[type=password],
input[type=tel], input[type=search], textarea {
  appearance: none;
  -webkit-appearance: none;
}
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
textarea { resize: vertical; min-height: 72px; }
input[type=range] { touch-action: pan-x; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }

/* ── Pills & Tags ───────────────────────────────────────── */
.pill {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 4px;
  font-size: var(--fs-xs);
  font-weight: 700;
  white-space: nowrap;
}
.tag-btn {
  padding: 5px 12px;
  border-radius: 20px;
  border: 1.5px solid var(--border);
  background: transparent;
  color: var(--text-muted);
  font-size: var(--fs-sm);
  cursor: pointer;
  font-family: inherit;
  transition: all .15s;
}
.tag-btn.active-accent { border-color: var(--accent); background: var(--accent-light); color: var(--accent); font-weight: 600; }
.tag-btn.active-purple { border-color: var(--purple); background: var(--purple-light); color: var(--purple); font-weight: 600; }

/* ── Stop cards ─────────────────────────────────────────── */
.stop-card {
  padding: 12px;
  border-radius: 8px;
  border: 1.5px solid var(--border);
  margin-bottom: 6px;
  background: var(--bg);
  user-select: none;
  touch-action: pan-y;
  transition: transform .18s ease-out, opacity .15s, border-color .12s, background .12s;
}
.drag-handle {
  touch-action: none;
  cursor: grab;
  color: var(--text-muted);
  font-size: 20px;
  font-weight: 700;
  flex-shrink: 0;
  /* 44 × 44 minimum hit area for gloved hands. Negative left/top margin
     pulls it back so the visual handle sits flush against the card edge
     while the touch target extends into the card padding. */
  width: 44px;
  height: 44px;
  margin: -8px 0 -8px -10px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-md);
  user-select: none;
  -webkit-user-select: none;
}
.drag-handle:hover    { background: rgba(0,0,0,0.04); color: var(--text); }
.drag-handle:active   { cursor: grabbing; background: rgba(0,0,0,0.08); }
[data-dark="1"] .drag-handle:hover { background: rgba(255,255,255,0.06); }
[data-dark="1"] .drag-handle:active { background: rgba(255,255,255,0.10); }
.stop-card.complete  { background: var(--accent-light); }
.stop-card.skip      { background: var(--border); opacity: 0.7; }
.stop-card.drag-over { border-color: var(--accent); }
.stop-card.email-failed {
  border-color: var(--danger);
  background: rgba(239, 68, 68, 0.06);
}
/* Service report logged but still sitting in the offline outbox — it has not
   reached the server yet. Amber, not green: the visit is done but not filed.
   Declared after .complete so it wins over that green background. */
.stop-card.send-pending {
  border-color: var(--warning);
  background: var(--warning-light);
}
/* Same, but the server refused it — it will never send on its own. */
.stop-card.send-rejected {
  border-color: var(--danger);
  background: var(--danger-light);
}
/* Pill beside the stop's action button, mirroring the work-order card's
   "⏳ Completed" text so ⏳ always reads "done, not delivered" on both
   card types.
   FILL is var(--card), not the matching --*-light tint: the card underneath
   is already tinted (.send-pending / .send-rejected above), so a tinted pill
   on a tinted card disappears. The border + text colour carry the meaning. */
.stop-send-badge {
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--warning);
  background: var(--card);
  border: 1px solid var(--warning);
  padding: 3px 8px;
  border-radius: 999px;
  white-space: nowrap;
  flex-shrink: 0;
}
.stop-send-badge.failed {
  color: var(--danger);
  border-color: var(--danger);
}
/* Explanatory note inside a queued / rejected stop card ("Waiting to send",
   "Report was not accepted"). Same fill reasoning as .stop-send-badge — a
   tech staring at a disabled "⏳ Waiting to send" button needs to be told
   what is happening, and the note has to be legible on the tinted card. */
.stop-send-note {
  margin-top: 6px;
  padding: 8px 10px;
  background: var(--card);
  border: 1px solid var(--warning);
  border-radius: 6px;
}
.stop-send-note.failed { border-color: var(--danger); }
/* Pending, but bouncing: amber border is too calm for a report the server has
   already refused several times. Read as "act on this", not "keep working". */
.stop-send-note.struggling { border-color: var(--danger); background: var(--danger-light); }
.stop-send-note.struggling .stop-send-note-title { color: var(--danger); }
.stop-send-note-title  { font-size: var(--fs-sm); font-weight: 600; color: var(--warning); }
.stop-send-note.failed .stop-send-note-title { color: var(--danger); }
.stop-send-note-body   { font-size: var(--fs-sm); color: var(--text-muted); margin-top: 2px; }
/* Hide the source card while the floating clone follows the pointer. The
   source still occupies its layout slot so the shift math works; it's just
   visually empty so adjacent cards' transforms don't overlap a half-faded
   ghost. */
.stop-card.dragging  { opacity: 0; }
.stop-card.selected  { border-color: var(--blue); background: var(--blue-light); }
/* Keep card body visible in select mode so cards stay the same height as
   the regular view; make it inert so taps bubble to the card and toggle
   selection rather than firing the buried buttons. */
.stop-card.select-mode .stop-body { pointer-events: none; }
.inv-card.selected   { border-color: var(--blue); background: var(--blue-light); }
.ser-card.selected   { border-color: var(--blue); background: var(--blue-light); }
.today-only-badge {
  display: inline-block;
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--purple);
  background: var(--purple-light);
  padding: 1px 5px;
  border-radius: 3px;
  margin-bottom: 4px;
}

/* ── Multi-select action bar ─────────────────────────────── */
#select-action-bar {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  background: var(--header-bg);
  color: #f0f4ff;
  padding: 12px 16px;
  padding-bottom: max(12px, env(safe-area-inset-bottom));
  display: flex;
  align-items: center;
  gap: 10px;
  z-index: var(--z-popover);
  box-shadow: 0 -4px 20px rgba(0,0,0,0.3);
}
#select-action-bar .sel-count { font-size: var(--fs-sm); font-weight: 600; color: rgba(255,255,255,0.55); flex: 1; }
#select-action-bar .btn { font-size: var(--fs-sm); padding: 7px 14px; }

/* The right-hand meta column (today-only badge, filter badge, ETA) used to be
   flex-shrink:0 with .stop-info as the only thing allowed to give. At large
   text those badges are wide and unshrinkable, so the name/address column
   collapsed to a couple of characters and every address wrapped one word per
   line — the "2801 / SW / 50th / Ter," cards from 2026-08-25.
   The fix for that let the whole ROW wrap, which traded one ugliness for
   another: whether meta sat beside the name or dropped below the address came
   down to a few pixels of badge width, so a single overdue stop
   ("Filter: 57d ago ⚠" is wider than "Filter: 12d ago") rendered in a shape
   nothing like the stops above it. Same list, two layouts.
   So: the row never wraps. The meta column is capped at 40% and shrinks — its
   badges stack onto their own lines inside it — while .stop-info keeps the
   remainder and never falls under 12em (em, so the floor tracks the font),
   itself capped at 50% so a huge text-scale setting can't make that floor
   wider than the card. Every stop card now has the same shape: name + address
   left, badges stacked right. */
.stop-top     { display: flex; align-items: flex-start; gap: 10px; }
.stop-info    { flex: 1 1 12em; min-width: min(12em, 50%); }
.stop-meta    { flex: 0 1 auto; min-width: 0; max-width: 40%; margin-left: auto; text-align: right; }
/* Pills are nowrap everywhere else, which would make a squeezed meta column
   overflow the card instead of getting shorter. In here they may wrap. */
.stop-meta .pill { white-space: normal; }
.stop-name    { font-weight: 700; font-size: var(--fs-md); overflow-wrap: anywhere; }
.stop-addr    { font-size: var(--fs-sm); color: var(--text-muted); margin-top: 1px; overflow-wrap: anywhere; }
.stop-last    { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 1px; }
.filter-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 7px; border-radius: 4px; font-size: var(--fs-xs); font-weight: 600; margin-top: 4px; }
.stop-actions { display: flex; gap: 5px; align-items: center; flex-wrap: wrap; margin-top: 8px; }
.drive-time-chip { text-align: center; padding: 1px 0; min-height: 16px; }
.drive-time-chip .dtchip-inner { font-size: 11px; color: var(--text-muted); letter-spacing: 0.02em; }
.stop-note-input {
  width: 100%; padding: 5px 8px;
  border: 1px solid var(--border); border-radius: 5px;
  font-size: var(--fs-sm); background: transparent;
  color: var(--text); outline: none; font-family: inherit; margin-top: 6px;
}
.progress-bar  { height: 4px; background: var(--border); border-radius: 2px; margin-bottom: 12px; overflow: hidden; }
.progress-fill { height: 100%; background: var(--accent); border-radius: 2px; transition: width .3s; }

/* Indeterminate "something is loading" bar for silent route-day fetches. */
.rt-fetch-bar { height: 0; background: var(--border); border-radius: 2px; overflow: hidden; transition: height .12s ease; }
.rt-fetch-bar.active { height: 3px; }
.rt-fetch-bar.active::before {
  content: ''; display: block; height: 100%; width: 40%;
  background: var(--accent); border-radius: 2px;
  animation: rtFetchBar 1.1s ease-in-out infinite;
}
@keyframes rtFetchBar {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}

/* ── Route day header (date + tech + meta) ──────────────────
   Deliberately TWO fixed lines, never a reflowing one:
     row 1  <date> ................................ <tech name>
     row 2  0/14 · ~4h 45m
   The meta string changes length after first paint (_updateRouteEta
   injects "~4h 45m" / "Done in 2h 49m ✓" asynchronously). When the
   meta shared row 1 the longer finished-route string pushed the tech
   name onto its own line, so the header silently reflowed depending
   on how the day went. Keeping meta on its own nowrap/ellipsis line
   means row 1 — and the tech name's position — is identical before
   and after the ETA lands, in every route state. */
.rt-head-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: nowrap;      /* never let the tech name drop a line */
  margin-bottom: 2px;
}
.rt-head-date {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Absorbs nearly all of the shrink, so a very long tech name ellipses
   rather than eating into the date. */
.rt-head-tech {
  flex: 0 20 auto;
  min-width: 0;
  max-width: 42%;  /* leaves the date room to stay whole */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--text-muted);
}
.rt-head-meta {
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-height: 1.2em;      /* reserve the line so the ETA can't shift the stops */
  margin-bottom: 6px;
}
/* The separator belongs to the ETA and only exists when there is both a
   count and a resolved ETA — a route with fewer than 2 stops never gets
   an ETA, and an all-skipped route has no count, so neither should show
   a dangling "·". */
.rt-head-count + .rt-eta:not(:empty)::before { content: ' \00B7  '; }

/* ── Shopping list (routes tab) ─────────────────────────── */
/* No overflow:hidden here — a row's ⋯ action menu is absolutely positioned
   inside this card, and clipping it cut the menu off mid-item. The rounded
   corners are instead carried by the header itself (the only child with a
   background of its own that reaches the card edge). */
.rt-sl {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  margin-bottom: 12px;
}
.rt-sl-header {
  border-radius: 7px 7px 0 0;
  display: flex; align-items: center; gap: 8px;
  padding: 8px 12px;
  cursor: pointer;
  user-select: none;
  font-size: 14px;
}
.rt-sl:not(.rt-sl-open) .rt-sl-header { border-radius: 7px; }
.rt-sl-header:hover { background: var(--accent-light); }
.rt-sl-caret { color: var(--text-muted); font-size: 12px; width: 12px; display: inline-block; text-align: center; }
.rt-sl-title { font-weight: 600; color: var(--text); }
.rt-sl-today-pill {
  margin-left: auto;
  font-size: 11px; font-weight: 600;
  background: var(--accent-light);
  color: var(--accent);
  padding: 3px 8px;
  border-radius: 10px;
  white-space: nowrap;
}
.rt-sl-today-pill { cursor: pointer; }
.rt-sl-soon-pill {
  margin-left: auto;
  font-size: 11px; font-weight: 600;
  background: var(--bg);
  color: var(--text-muted);
  border: 1px solid var(--border);
  padding: 3px 8px;
  border-radius: 10px;
  white-space: nowrap;
  cursor: pointer;
}
/* "In the truck" — bought, not yet used. Informational, never a warning, so it
   rides ALONGSIDE whichever needed-today/needed-soon pill is showing rather
   than replacing it. Those pills carry margin-left:auto; two auto margins would
   split the free space and strand the first pill mid-header, so this one only
   claims the auto margin when it is the header's sole pill. */
.rt-sl-held-pill {
  margin-left: 6px;
  font-size: 11px; font-weight: 600;
  background: var(--bg);
  color: var(--text-muted);
  border: 1px dashed var(--border);
  padding: 3px 8px;
  border-radius: 10px;
  white-space: nowrap;
  cursor: pointer;
}
.rt-sl-title + .rt-sl-held-pill { margin-left: auto; }
.rt-sl-held-mark { color: var(--text-muted); font-weight: 600; }
.rt-sl-item-held { opacity: .72; }
.rt-sl-group-held {
  background: var(--bg);
  color: var(--text-muted);
  border: 1px dashed var(--border);
}

.rt-sl-body {
  border-top: 1px solid var(--border);
  padding: 10px 12px 12px;
}
/* Day agenda headers: the day each item is needed is read once, here, instead
   of repeating as a chip inside every row (which wrapped mid-title and left the
   row actions at a different height on every line). */
.rt-sl-group {
  list-style: none;
  display: flex; align-items: center; gap: 8px;
  font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-muted);
  background: var(--bg);
  border-radius: 4px;
  padding: 6px 8px;
  margin: 10px 0 2px;
  border-bottom: none;
}
.rt-sl-group:first-child { margin-top: 0; }
.rt-sl-group-n {
  margin-left: auto;
  font-weight: 600;
  text-transform: none;
  letter-spacing: 0;
  opacity: 0.8;
}
.rt-sl-group-today { background: var(--accent-light); color: var(--accent); }
.rt-sl-empty {
  color: var(--text-muted);
  font-size: 13px;
  padding: 6px 0 10px;
}
.rt-sl-list {
  list-style: none; margin: 0 0 10px; padding: 0;
}
.rt-sl-item {
  display: flex; align-items: flex-start;
  gap: 8px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}
.rt-sl-item:last-child { border-bottom: none; }
.rt-sl-item-main { flex: 1; min-width: 0; }
.rt-sl-item-line1 {
  display: flex; align-items: flex-start; gap: 6px;
  font-size: 14px; font-weight: 600; color: var(--text);
}
.rt-sl-qty {
  display: inline-block;
  flex-shrink: 0;
  min-width: 24px;
  padding: 1px 6px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  font-size: 12px; font-weight: 600;
  color: var(--text-muted);
  text-align: center;
}
.rt-sl-name { word-break: break-word; }
.rt-sl-item-line2 {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  font-size: 12px;
  color: var(--text-muted);
  margin-top: 2px;
}
.rt-sl-customer { font-weight: 500; color: var(--text); }
.rt-sl-sep { opacity: 0.5; }
.rt-sl-addr { color: var(--text-muted); }
.rt-sl-item-today { border-left: 3px solid var(--accent); padding-left: 8px; }
.rt-sl-del {
  background: none; border: none; cursor: pointer;
  color: var(--text-muted);
  font-size: 20px; line-height: 1;
  padding: 2px 8px;
  border-radius: 4px;
  flex-shrink: 0;
}
.rt-sl-del:hover { background: var(--danger); color: white; }
.rt-sl-edit {
  background: none; border: none; cursor: pointer;
  font-size: 14px; line-height: 1;
  padding: 4px 6px;
  border-radius: 4px;
  flex-shrink: 0;
  opacity: 0.7;
}
.rt-sl-edit:hover { background: var(--bg); opacity: 1; }
/* One ⋯ column instead of three floating icons, so the actions sit at the same
   x and y on every row no matter how long the product name is. */
.rt-sl-menu-wrap { flex-shrink: 0; }
.rt-sl-more {
  background: none; border: none; cursor: pointer;
  color: var(--text-muted);
  font-size: 20px; line-height: 1;
  width: 32px; height: 32px;
  border-radius: 4px;
  padding: 0;
}
.rt-sl-more:hover { background: var(--bg); color: var(--text); }
/* While editing, the row becomes the draft form — drop the flex row layout and
   the today-gradient padding so the form spans the full width of the list. */
.rt-sl-item-editing { display: block; padding: 8px 0; background: none; }

.rt-sl-add { flex: 1; }

/* Footer actions. "+ Add item" fills the row. */
.rt-sl-footer { display: flex; gap: 8px; }

.rt-sl-draft {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
  padding: 10px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
}
.rt-sl-input {
  width: 100%;
  box-sizing: border-box;
  padding: 8px 10px;
  font-size: 14px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--card);
  color: var(--text);
}
.rt-sl-input:focus { outline: none; border-color: var(--accent); }
.rt-sl-input:disabled { background: var(--bg); color: var(--text-muted); cursor: not-allowed; }
.rt-sl-cust-wrap { position: relative; }
.rt-sl-draft-actions { display: flex; gap: 8px; justify-content: flex-end; }
/* "General item" checkbox — spans the draft grid so it reads as a mode switch
   for the whole row, not a field sitting beside the customer box. */
.rt-sl-general-toggle {
  grid-column: 1 / -1;
  display: flex; align-items: center; gap: 8px;
  font-size: 0.85rem; color: var(--text-muted); cursor: pointer;
}
.rt-sl-general-toggle input { width: 16px; height: 16px; margin: 0; accent-color: var(--accent); }
.rt-sl-general { font-style: italic; color: var(--accent); font-weight: 600; }

/* The week organizer is a drag-and-drop 7-column board — unusable on a phone.
   Hidden by default; desktop.css unhides it at the ≥1024px shell breakpoint. */
.rt-organize-btn { display: none; }

@media (min-width: 560px) {
  .rt-sl-draft {
    grid-template-columns: 1fr 1fr;
  }
  .rt-sl-item-input { grid-column: 1 / -1; }
  .rt-sl-qty-input { grid-column: 1 / 2; max-width: 120px; }
  .rt-sl-draft-actions { grid-column: 1 / -1; }
}

/* ── Modal ──────────────────────────────────────────────── */
.modal-overlay {
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.4);
  z-index: var(--z-modal);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 16px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.modal-overlay.hidden { display: none; }
.modal-box {
  background: var(--card);
  border-radius: 14px;
  border: 1px solid var(--border);
  padding: 22px 20px;
  width: 100%; max-width: 560px;
  box-shadow: 0 20px 60px rgba(0,0,0,.25);
  margin: auto;
}
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; }
.modal-title  { font-size: var(--fs-h2); font-weight: 700; }
.modal-close  {
  background: none; border: none; font-size: 22px; cursor: pointer;
  color: var(--text-muted); line-height: 1;
  /* 36 × 36 hit target — was 4 px padding × 22 px glyph (≈ 22 px). */
  width: 36px; height: 36px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius-md);
  margin: -4px -4px -4px 0; /* keep visual position while expanding hit area */
  transition: background .12s, color .12s;
}
.modal-close:hover  { background: rgba(0,0,0,0.06); color: var(--text); }
.modal-close:active { background: rgba(0,0,0,0.10); }
[data-dark="1"] .modal-close:hover  { background: rgba(255,255,255,0.08); }
[data-dark="1"] .modal-close:active { background: rgba(255,255,255,0.12); }
.modal-footer { display: flex; gap: 8px; padding-top: 14px; border-top: 1px solid var(--border); flex-wrap: wrap; margin-top: 14px; }

/* Opt-in scrollable layout: when a modal contains a .modal-body wrapper,
   the box is capped to viewport height and the body scrolls instead of
   the whole overlay. Header and footer stay fixed at the top and bottom. */
.modal-box:has(> .modal-body) {
  max-height: calc(100vh - 32px);
  /* Visual viewport on mobile (subtracts URL bar) when supported. */
  max-height: calc(100dvh - 32px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.modal-box:has(> .modal-body) > .modal-header,
.modal-box:has(> .modal-body) > .modal-footer {
  flex-shrink: 0;
}
.modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* Negative margins pull the scroll edge to the box's padding so the
     scrollbar isn't inset awkwardly; restore the inner padding so the
     content keeps the same horizontal whitespace as before. */
  margin-left: -20px;
  margin-right: -20px;
  padding-left: 20px;
  padding-right: 20px;
}

/* ── Toggle checkbox ────────────────────────────────────── */
.toggle-row {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px; border-radius: 8px;
  border: 1.5px solid var(--border); background: var(--bg);
  cursor: pointer; user-select: none; margin-bottom: 14px; transition: all .15s;
}
.toggle-row.checked { border-color: var(--warning); background: var(--warning-light); }
.toggle-box {
  width: 20px; height: 20px; border-radius: 4px;
  border: 2px solid var(--border); background: transparent;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; transition: all .15s;
}
.toggle-row.checked .toggle-box { border-color: var(--warning); background: var(--warning); color: #fff; }
.toggle-label { font-size: var(--fs-sm); font-weight: 600; }
.toggle-sub   { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 1px; }

/* ── Checklist items ───────────────────────────────────── */
.checklist-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 8px;
  border: 1.5px solid var(--border);
  margin-bottom: 6px;
  cursor: pointer;
  transition: background .15s, border-color .15s;
  user-select: none;
}
/* Countdown state on a checklist item — Clean Filter and Inspect Salt Cell are
   the same countdown with different intervals, so they share one pair of looks.
   Both are grouped rather than merged into one class because the JS toggles
   them independently: a pool can be overdue for one and fine on the other. */
.checklist-item.filter-overdue:not(.checked),
.checklist-item.salt-overdue:not(.checked) {
  border-color: var(--danger);
  background: rgba(239, 68, 68, 0.06);
}
.checklist-item.filter-warning:not(.checked),
.checklist-item.salt-warning:not(.checked) {
  border-color: var(--warning);
  background: var(--warning-light);
}
.checklist-item.checked {
  background: var(--accent-light);
  border-color: var(--accent);
}
.checklist-item.scheduled-highlight {
  background: #edf5ff;
  border-color: var(--blue, #1a7ec8);
}
.checklist-item.scheduled-highlight .checklist-name {
  color: var(--blue, #1a7ec8);
  font-weight: 600;
}
.checklist-dot {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 2px solid var(--border);
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background .15s, border-color .15s;
}
.checklist-dot.checked {
  background: var(--accent);
  border-color: var(--accent);
}
.checklist-dot.checked::after {
  content: '✓';
  color: white;
  font-size: 11px;
  font-weight: 700;
}
.checklist-name {
  flex: 1;
  font-size: var(--fs-md);
  font-weight: 500;
}
.checklist-last {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  flex-shrink: 0;
}

/* ── Toast ──────────────────────────────────────────────── */
#toast-container { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); z-index: var(--z-toast); display: flex; flex-direction: column; gap: 8px; align-items: center; pointer-events: none; }
.toast {
  padding: 10px 18px; border-radius: 8px;
  font-size: var(--fs-sm); font-weight: 600;
  color: #fff; background: var(--text);
  box-shadow: var(--shadow);
  animation: toast-in .2s ease;
  pointer-events: auto; max-width: 320px; text-align: center;
}
.toast.success { background: var(--accent); }
.toast.error   { background: var(--danger); }
/* Partly-succeeded actions — e.g. a supplier cart where some lines had no part
   number. Not an error: the rest of the work landed. */
.toast.warn    { background: var(--warning); }
@keyframes toast-in { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }

/* ── Upload pills (background video uploads) ─────────────
   Bottom-right stack of in-flight uploads. A 1 GB video can take many
   minutes, so each pill collapses to a 56px progress bubble — the upload
   keeps running, it just stops sitting on top of the app. Cleared above
   the mobile bottom-nav so it never covers the tab bar. */
#upload-pills {
  position: fixed;
  left: 12px; right: 12px;
  bottom: calc(12px + env(safe-area-inset-bottom));
  /* Above toasts (and therefore above modals) — an upload outlives whatever
     is on screen and its controls must stay reachable. */
  z-index: calc(var(--z-toast) + 10);
  display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
  pointer-events: none;
}
@media (max-width: 720px) {
  #upload-pills { bottom: calc(68px + env(safe-area-inset-bottom)); }
}
.upl-pill {
  pointer-events: auto;
  width: 100%; max-width: 420px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 6px 24px rgba(0,0,0,0.18);
  padding: 10px 12px;
  font-size: var(--fs-sm);
  color: var(--text);
  transition: max-width .18s ease, padding .18s ease, border-radius .18s ease;
}
.upl-pill.collapsed {
  width: 56px; max-width: 56px; height: 56px;
  padding: 0; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
}
.upl-pill.collapsed:active { transform: scale(0.95); }
.upl-min {
  background: none; border: none; cursor: pointer;
  color: var(--text-muted); font-size: var(--fs-base); line-height: 1;
  padding: 2px 6px; border-radius: var(--radius-sm); font-family: inherit;
}
.upl-min:hover { background: var(--border); color: var(--text); }
/* Bubble = ring of progress (conic gradient driven by --upl-pct) with the
   percentage punched out of the middle. */
.upl-bubble { position: relative; width: 100%; height: 100%; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.upl-bubble::before {
  content: ''; position: absolute; inset: 0; border-radius: 50%;
  background: conic-gradient(var(--accent) calc(var(--upl-pct, 0) * 1%), var(--border) 0%);
}
.upl-bubble::after { content: ''; position: absolute; inset: 4px; border-radius: 50%; background: var(--card); }
.upl-bubble.done::before { background: var(--success); }
.upl-bubble.err::before  { background: var(--danger); }
.upl-bubble > span { position: relative; font-size: var(--fs-xs); font-weight: 700; }
.upl-bubble.done > span { color: var(--success); font-size: var(--fs-md); }
.upl-bubble.err  > span { color: var(--danger);  font-size: var(--fs-md); }

/* ── Misc ───────────────────────────────────────────────── */
.empty-state { text-align: center; padding: 48px 24px; color: var(--text-muted); }
.empty-state .empty-icon { font-size: 36px; margin-bottom: 10px; }
.empty-state h3 { font-size: var(--fs-md); font-weight: 600; margin-bottom: 4px; }
.empty-state p  { font-size: var(--fs-sm); }
.error-msg { color: var(--danger); font-size: var(--fs-sm); margin-bottom: 10px; padding: 8px 12px; background: var(--danger-light); border-radius: 6px; }
.info-tip  { font-size: var(--fs-sm); color: var(--blue); background: var(--blue-light); padding: 8px 12px; border-radius: 7px; margin-top: 10px; line-height: 1.5; }
.divider   { border: none; border-top: 1px solid var(--border); margin: 14px 0; }
.flex-row  { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.readings-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; margin-bottom: 14px; }
.reading-box { background: var(--bg); border-radius: 7px; padding: 8px 10px; }
.reading-box .rb-label { font-size: var(--fs-xs); color: var(--text-muted); margin-bottom: 2px; }
.reading-box .rb-value { font-size: var(--fs-xl); font-weight: 700; }
.photo-grid  { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.photo-thumb { position: relative; }
.photo-thumb img { width: 80px; height: 80px; object-fit: cover; border-radius: 8px; border: 1px solid var(--border); display: block; }
.photo-remove { position: absolute; top: -6px; right: -6px; width: 20px; height: 20px; border-radius: 50%; background: var(--danger); color: #fff; border: none; cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; }
/* Report videos (report-video.js) — chips in the log modals, plus the
   read-only poster grid used by review screens and saved reports. Wider than a
   photo thumb because a chip carries a progress bar and a duration/size line. */
.vid-grid  { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
.vid-chip  { position: relative; width: 116px; }
.vid-thumb { position: relative; width: 116px; height: 76px; border-radius: 8px; overflow: hidden;
             border: 1px solid var(--border); background: #000; cursor: pointer; }
.vid-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.vid-thumb-blank { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
                   font-size: 26px; background: var(--bg); }
.vid-play  { position: absolute; inset: 0; margin: auto; width: 30px; height: 30px; border-radius: 50%;
             background: rgba(0,0,0,.55); color: #fff; font-size: 13px; display: flex;
             align-items: center; justify-content: center; pointer-events: none; }
.vid-chip.is-uploading .vid-thumb { opacity: .6; }
.vid-bar   { height: 4px; border-radius: 2px; background: var(--border); margin-top: 5px; overflow: hidden; }
.vid-bar span { display: block; height: 100%; background: var(--accent); transition: width .15s linear; }
.vid-meta  { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 3px; line-height: 1.3;
             overflow: hidden; text-overflow: ellipsis; }
.vid-meta.vid-err { color: var(--danger); }
.vid-hint  { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 6px; }
/* One control for both attachments: tap = photo, press-and-hold = video
   (ReportVideo.mediaButtonHtml). The fill sweeps left-to-right over exactly the
   hold window, so the tech can see the gesture arming before the camera opens. */
.media-btn { position: relative; overflow: hidden; text-align: left; line-height: 1.25;
             -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
             touch-action: manipulation; }
.media-btn-label { position: relative; z-index: 1; display: block; pointer-events: none; }
.media-btn-label em { display: block; font-style: normal; font-weight: 400;
                      font-size: var(--fs-xs); color: var(--text-muted); margin-top: 1px; }
.media-btn-fill { position: absolute; inset: 0; width: 0; background: var(--accent-light);
                  transition: width .12s ease-out; pointer-events: none; }
.media-btn.is-holding .media-btn-fill { width: 100%; transition: width .5s linear; }
.media-btn.is-holding { border-color: var(--accent); }
.vid-preview { position: relative; display: inline-block; width: 116px; height: 76px; border-radius: 8px;
               overflow: hidden; border: 1px solid var(--border); background: #000; }
.vid-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }

.perm-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.perm-row  { display: flex; align-items: center; gap: 8px; font-size: var(--fs-sm); cursor: pointer; }
.perm-row input { width: auto; }
.log-entry { margin-bottom: 10px; }
.log-chems { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
table.data-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
table.data-table th { text-align: left; padding: 8px 10px; font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); border-bottom: 2px solid var(--border); }
table.data-table td { padding: 10px; border-bottom: 1px solid var(--border); }
.spinner { width: 20px; height: 20px; border: 2px solid rgba(255,255,255,.3); border-top-color: #fff; border-radius: 50%; animation: spin .7s linear infinite; display: inline-block; }
.spinner-inline { width: 13px; height: 13px; border: 2px solid rgba(255,255,255,.35); border-top-color: currentColor; border-radius: 50%; animation: spin .7s linear infinite; display: inline-block; vertical-align: middle; margin-right: 6px; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ── Pagination ─────────────────────────────────────────── */
.pagination { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 14px 0 4px; }
.pagination button {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 6px 14px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--card); color: var(--text);
  font-size: var(--fs-sm); font-weight: 600; cursor: pointer; font-family: inherit; transition: all .15s;
}
.pagination button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.pagination button:disabled { opacity: .4; cursor: not-allowed; }
.pagination .page-info { font-size: var(--fs-sm); color: var(--text-muted); font-weight: 600; min-width: 80px; text-align: center; }

/* ── Theme palette swatch ───────────────────────────────── */
.pal-swatch {
  border-radius: 10px; padding: 8px 10px; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  min-width: 58px; border: 2px solid var(--border); transition: border-color .15s;
  background: var(--card);
}
.pal-swatch.selected { border-color: var(--accent); }
.pal-swatch-bar { width: 36px; height: 6px; border-radius: 3px; }
.pal-swatch-name { font-size: 10px; color: var(--text-muted); white-space: nowrap; }

/* ── Dark mode toggle ───────────────────────────────────── */
.dark-toggle-track {
  background: var(--border); border: none; border-radius: 12px;
  width: 44px; height: 24px; cursor: pointer; position: relative; transition: background .2s; flex-shrink: 0;
}
.dark-toggle-track.on { background: var(--accent); }
.dark-toggle-thumb {
  position: absolute; top: 3px; left: 3px;
  width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: left .2s;
}
.dark-toggle-track.on .dark-toggle-thumb { left: 23px; }

/* ── Responsive ─────────────────────────────────────────── */
@media (max-width: 480px) {
  .grid-2 { grid-template-columns: 1fr; }
  .grid-3 { grid-template-columns: 1fr 1fr; }
  .readings-grid { grid-template-columns: 1fr 1fr; }
}

/* ── Expandable admin nav group ──────────────────────────── */
.nav-group-toggle {
  width: 100%; text-align: left; background: none; border: none;
  padding: 12px 16px; color: rgba(255,255,255,0.65); font-size: var(--fs-sm);
  font-family: inherit; font-weight: 500; cursor: pointer;
  display: flex; justify-content: space-between; align-items: center;
  transition: background .15s;
}
.nav-group-toggle:hover { background: rgba(255,255,255,0.08); color: #fff; }
.nav-group-toggle.has-active { color: #fff; font-weight: 700; }
.nav-group-items { display: flex; flex-direction: column; }
#sidebar .nav-btn.nav-sub { padding-left: 32px; font-size: var(--fs-sm); opacity: .9; }

/* ── Dashboard stat cards ───────────────────────────────── */
.stat-grid { display: grid; grid-template-columns: repeat(2,1fr); gap: 12px; margin-bottom: 20px; }
.stat-card {
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 14px 16px;
}
.stat-label { font-size: var(--fs-xs); color: var(--text-muted); text-transform: uppercase; letter-spacing: .06em; margin-bottom: 4px; }
.stat-value { font-size: 22px; font-weight: 700; color: var(--text); }
.stat-sub   { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 2px; }
.stat-card.good .stat-value { color: var(--success); }
.stat-card.bad  .stat-value { color: var(--danger); }

/* ── Chart wrapper ──────────────────────────────────────── */
.chart-wrap { position: relative; height: 240px; margin-bottom: 20px; }

/* ── Dashboard chart carousel ───────────────────────────── */
.dash-carousel-wrap { position: relative; }
.dash-carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* Negative margins extend the slide row to the panel padding edges so a
     single slide fills the same width the old standalone cards had. */
  -webkit-overflow-scrolling: touch;
}
.dash-carousel::-webkit-scrollbar { display: none; }
.dash-carousel > .dash-slide {
  flex: 0 0 100%;
  scroll-snap-align: start;
  margin-bottom: 0;
  /* Slides stack edge-to-edge horizontally; only one is visible at a time.
     The donut variant uses a column layout so its chart body can grow and
     center vertically when the slide has spare height (the trend-chart
     slide is taller, so the carousel auto-stretches the donut slides to
     match). */
}
.dash-slide.dash-donut {
  display: flex;
  flex-direction: column;
}
.dash-donut .dash-slide-body {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  flex-wrap: wrap;
  min-height: 0;
}
.dash-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--border);
  border: none; padding: 0; cursor: pointer;
  transition: background .15s, transform .15s;
}
.dash-dot.active { background: var(--accent); transform: scale(1.25); }
.dash-dot:hover  { background: var(--text-muted); }
.dash-dot.active:hover { background: var(--accent); }

/* ── Accounting / Reports pill tabs ─────────────────────── */
/* wrap, don't overflow: Team & Settings carries six pills, which is wider
   than a phone at any font-size setting. */
.pill-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 16px; }
.pill-tab {
  padding: 6px 14px; border-radius: 20px; border: 1px solid var(--border);
  background: var(--card); color: var(--text-muted); font-size: var(--fs-sm);
  cursor: pointer; font-family: inherit; transition: all .15s;
}
.pill-tab.active { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }

/* ── Expense category badges ────────────────────────────── */
.cat-badge {
  display: inline-block; font-size: var(--fs-xs); padding: 2px 8px;
  border-radius: 10px; font-weight: 600; text-transform: capitalize;
  background: var(--accent-light); color: var(--accent);
}
/* Expense category badges intentionally use a distinct accent palette
   (green/blue/amber/purple) to make categories scannable at a glance.
   These don't theme — that's deliberate; categories should read the same
   regardless of which palette/dark mode is active. */
.cat-badge.chemicals { background: #d1fae5; color: #065f46; }
.cat-badge.equipment { background: #dbeafe; color: #1e40af; }
.cat-badge.vehicle   { background: #fef3c7; color: #92400e; }
.cat-badge.labor     { background: #ede9fe; color: #5b21b6; }
.cat-badge.office    { background: var(--accent-light); color: var(--accent); }
.cat-badge.other     { background: var(--border); color: var(--text-muted); }

/* ── Import tab ─────────────────────────────────────────── */
.import-dropzone {
  border: 2px dashed var(--border); border-radius: var(--radius);
  padding: 28px 16px; text-align: center; cursor: pointer;
  transition: border-color .2s, background .2s;
  color: var(--text-muted); font-size: var(--fs-sm); display: block;
}
.import-dropzone:hover, .import-dropzone.drag-over {
  border-color: var(--accent); background: var(--accent-light);
}
.import-dropzone.has-file { border-color: var(--success); background: var(--success-light); color: var(--success); }

.import-review-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.import-review-table th {
  text-align: left; font-size: var(--fs-xs); color: var(--text-muted);
  text-transform: uppercase; letter-spacing: .06em; padding: 6px 8px;
  border-bottom: 2px solid var(--border);
}
.import-review-table td { padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.import-review-table tr.excluded td { opacity: 0.4; }
.import-review-table tr.excluded td:last-child { opacity: 1; }

.btn-link-inline {
  background: none; border: none; color: var(--accent);
  font-size: var(--fs-xs); font-weight: 600; cursor: pointer;
  padding: 0; text-decoration: underline;
}
.btn-link-inline:hover { opacity: 0.8; }

/* ── Route stop card: what's already in the truck ───────── */
/* Items bought for this property. They are deliberately absent from every
   "needed" counter — the buying is done; what's left is carrying them in. */
.stop-held-items {
  display: flex; flex-wrap: wrap; align-items: center; gap: 5px;
  margin-top: 6px; padding: 6px 8px;
  background: var(--accent-light);
  border-radius: 6px;
  font-size: 12px;
}
.stop-held-label { font-weight: 700; color: var(--accent); white-space: nowrap; }
.stop-held-chip {
  background: var(--card, var(--bg));
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 2px 8px;
  color: var(--text);
}

/* ── Work-order invoice reference link ──────────────────── */
/* The "Invoice: 000422-WO-0001 — $1465.00 · sent" line in the work-order log
   modal opens that invoice. It inherits the surrounding 13px so the line reads
   the same as before, and the negative margin trades a hairline text hit-box
   for a thumb-sized one without moving anything on the page. */
.wo-invoice-link {
  background: none; border: none; font: inherit; text-align: left;
  color: var(--accent); font-weight: 600;
  text-decoration: underline; text-underline-offset: 2px;
  padding: 7px 2px; margin: -7px 0; cursor: pointer;
}
.wo-invoice-link:hover  { opacity: .8; }
.wo-invoice-link:active { opacity: .6; }

/* ── Progress bar (P&L breakdown) ───────────────────────── */
.progress-bar-wrap { background: var(--border); border-radius: 4px; height: 6px; margin-top: 4px; overflow: hidden; }
.progress-bar-fill { height: 100%; background: var(--accent); border-radius: 4px; transition: width .3s; }

/* ── Reports table ──────────────────────────────────────── */
.report-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.report-table th { text-align: left; padding: 8px 10px; border-bottom: 2px solid var(--border); color: var(--text-muted); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .06em; }
.report-table td { padding: 8px 10px; border-bottom: 1px solid var(--border); }
.report-table tr:last-child td { border-bottom: none; }
.report-table tr.group-header td { background: var(--accent-light); font-weight: 700; color: var(--accent); }
.report-table tr.group-total td { font-weight: 600; font-style: italic; color: var(--text-muted); }

/* ── Search input with clear button ─────────────────────── */
.search-wrap { position: relative; display: flex; align-items: center; }
.search-wrap input { flex: 1; padding-right: 28px !important; }
.search-wrap .x-btn {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  background: none; border: none; cursor: pointer; color: var(--text-muted);
  font-size: 16px; line-height: 1; padding: 0 2px; display: none;
}
.search-wrap input:not(:placeholder-shown) ~ .x-btn { display: block; }
.search-wrap .x-btn:hover { color: var(--text); }

/* ── Customer typeahead ─────────────────────────────────── */
.cust-typeahead { position: relative; }
.cust-typeahead-list {
  display: none; position: absolute; z-index: var(--z-popover); left: 0; right: 0; top: 100%;
  background: var(--card); border: 1px solid var(--border);
  border-top: none; border-radius: 0 0 var(--radius) var(--radius);
  max-height: 220px; overflow-y: auto; box-shadow: 0 4px 12px rgba(0,0,0,.12);
}
.cust-typeahead-list.open { display: block; }
.cust-typeahead-item {
  padding: 8px 12px; cursor: pointer; font-size: var(--fs-sm);
  border-bottom: 1px solid var(--border);
}
.cust-typeahead-item:last-child { border-bottom: none; }
.cust-typeahead-item:hover, .cust-typeahead-item.active { background: var(--accent-light); color: var(--accent); }
.cust-typeahead-item .cust-num { font-size: 11px; color: var(--text-muted); margin-left: 6px; }

/* Position-fixed variant of the typeahead dropdown for the scan-results
   table — escapes the parent's overflow-x clipping so the popup isn't
   cut off below the row. Coordinates come from JS via getBoundingClientRect. */
.scan-typeahead-list {
  display: none; position: fixed; z-index: 10000;
  background: var(--card); border: 1px solid var(--border);
  border-radius: 0 0 var(--radius) var(--radius);
  max-height: 220px; overflow-y: auto; box-shadow: 0 4px 12px rgba(0,0,0,.18);
}
.scan-typeahead-list.open { display: block; }

/* ── Invoice card "More ▾" action menu ─────────────────── */
.inv-actions-wrap { position: relative; display: inline-block; }
/* Anchored to the button's LEFT edge by default: on a card the "More ▾" button
   usually sits at the left of the action row, so right-aligning ran the menu off
   the left edge of a phone screen. _placeActionMenu() flips the modifier classes
   below on when the preferred side/direction would overflow the viewport. */
.inv-actions-menu {
  display: none; position: absolute; top: 100%; left: 0; right: auto; margin-top: 4px;
  z-index: var(--z-popover); min-width: 160px; max-width: calc(100vw - 16px);
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: 0 4px 12px rgba(0,0,0,.18);
  padding: 4px;
}
.inv-actions-menu.align-right { left: auto; right: 0; }
.inv-actions-menu.drop-up { top: auto; bottom: 100%; margin-top: 0; margin-bottom: 4px; }
.inv-actions-menu.open { display: block; }
.inv-actions-menu .inv-action-item {
  display: block; width: 100%; text-align: left;
  background: transparent; border: none; cursor: pointer;
  padding: 8px 12px; border-radius: 4px;
  font-size: var(--fs-sm); color: var(--text);
}
.inv-actions-menu .inv-action-item:hover { background: var(--accent-light); color: var(--accent); }
.inv-actions-menu .inv-action-item.danger { color: var(--danger); }
.inv-actions-menu .inv-action-item.danger:hover { background: rgba(220,38,38,0.1); color: var(--danger); }

/* ── Broadcasts tab ────────────────────────────────────── */
.bc-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; gap: 8px; }
.bc-h2 { font-size: var(--fs-lg); font-weight: 700; margin: 0; }
.bc-toolbar-right { display: flex; gap: 8px; }
.bc-grid {
  display: grid;
  grid-template-columns: 280px 1fr 1fr;
  gap: 14px;
}
@media (max-width: 1024px) {
  .bc-grid { grid-template-columns: 1fr; }
}
.bc-col {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px;
  min-height: 320px;
}
.bc-section-h {
  font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--text-muted); margin-bottom: 10px;
  display: flex; justify-content: space-between; align-items: center;
}
.bc-summary { font-weight: 600; color: var(--text); text-transform: none; letter-spacing: 0; font-size: 12px; }
.bc-filter-block { margin-bottom: 12px; }
.bc-label { display: block; font-size: 11px; font-weight: 600; color: var(--text-muted); margin-bottom: 4px; text-transform: uppercase; letter-spacing: .04em; }
.bc-chip-row { display: flex; flex-wrap: wrap; gap: 4px; }
.bc-recip-add { position: relative; margin-bottom: 8px; }
.bc-add-results {
  position: absolute; top: 100%; left: 0; right: 0;
  background: var(--card); border: 1px solid var(--border); border-radius: 6px;
  z-index: 5; max-height: 280px; overflow: auto;
}
.bc-add-row {
  display: flex; flex-direction: column; gap: 2px; align-items: flex-start;
  width: 100%; text-align: left; background: transparent; border: none;
  padding: 6px 10px; cursor: pointer; font: inherit; color: var(--text);
}
.bc-add-row:hover { background: var(--accent-light); }
.bc-add-empty { padding: 6px 10px; color: var(--text-muted); font-size: 12px; }
.bc-recip-list { max-height: 460px; overflow: auto; display: flex; flex-direction: column; gap: 4px; }
.bc-detail-list { max-height: 600px; }
.bc-recip-empty { padding: 18px; color: var(--text-muted); text-align: center; font-size: 13px; }
.bc-recip-row {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 7px 8px; border-radius: 6px; font-size: 13px;
  border: 1px solid transparent;
}
.bc-recip-row > input[type="checkbox"] { flex-shrink: 0; margin-top: 3px; }
.bc-recip-body  { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.bc-recip-line1 { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; min-width: 0; }
.bc-recip-row:hover { background: var(--accent-light); border-color: var(--border); }
.bc-recip-row.bc-skip { opacity: 0.55; }
.bc-recip-name  { font-weight: 600; }
.bc-recip-email { color: var(--text-muted); overflow-wrap: anywhere; }
.bc-recip-meta  { color: var(--text-muted); font-size: 12px; line-height: 1.35; overflow-wrap: anywhere; }
.bc-reason { background: rgba(220,53,69,0.12); color: var(--danger); border-radius: 4px; font-size: 11px; font-weight: 600; padding: 1px 6px; }
.bc-pill   { background: var(--blue-light); color: var(--blue); border-radius: 4px; font-size: 11px; font-weight: 600; padding: 1px 6px; }
/* On the list only because a one-time move landed them on the filtered day. */
.bc-pill-moved { background: rgba(255,159,10,0.15); color: #b26b00; }
.bc-template-row { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; }
.bc-template-row select { flex: 1; }
.bc-subject { margin-bottom: 8px; font-weight: 600; }
.bc-token-row { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; margin-bottom: 6px; }
.bc-tokens-label { font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; margin-right: 4px; }
.bc-token-btn {
  font: 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  background: var(--bg); border: 1px solid var(--border); color: var(--text);
  border-radius: 4px; padding: 3px 6px; cursor: pointer;
}
.bc-token-btn.property { border-color: var(--accent); color: var(--accent); }
.bc-token-btn:hover { background: var(--accent-light); }
/* Quill is the .ql-container itself once initialized — give it an explicit
   height so its internal `.ql-editor { height: 100% }` resolves cleanly
   instead of inheriting whatever parent the percentage walks up to. The
   editor's own overflow-y handles longer drafts. */
.bc-editor { background: var(--bg); height: 280px; flex: 0 0 280px; }
@media (min-width: 720px) { .bc-editor { height: 320px; flex-basis: 320px; } }
.bc-editor-fallback textarea { width: 100%; box-sizing: border-box; min-height: 240px; }
/* Composer column lays out top-down so the action buttons always sit right
   below the editor, never pushed off the page by Quill's layout. */
.bc-composer { display: flex; flex-direction: column; }
.bc-composer > .bc-section-h,
.bc-composer > .bc-template-row,
.bc-composer > .bc-subject,
.bc-composer > .bc-token-row,
.bc-composer > .bc-compose-actions { flex: 0 0 auto; }
.bc-compose-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.bc-preview-subject { padding: 8px 10px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; margin-bottom: 8px; }
.bc-preview-body { padding: 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; max-height: 60vh; overflow: auto; }
.bc-history { display: flex; flex-direction: column; gap: 6px; }
.bc-history-row {
  display: block; width: 100%; text-align: left;
  background: var(--card); border: 1px solid var(--border); border-radius: 8px;
  padding: 10px 12px; cursor: pointer; font: inherit; color: var(--text);
}
.bc-history-row:hover { border-color: var(--accent); }
.bc-history-subj { font-size: 14px; display: flex; align-items: center; gap: 8px; }
.bc-history-meta { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.bc-history-status { font-size: 11px; font-weight: 700; padding: 2px 7px; border-radius: 4px; text-transform: uppercase; letter-spacing: .04em; }
.bc-status-queued, .bc-status-pending  { background: var(--blue-light); color: var(--blue); }
.bc-status-sending                     { background: var(--accent-light); color: var(--accent); }
.bc-status-sent, .bc-status-done       { background: rgba(34,197,94,0.15); color: rgb(22,163,74); }
.bc-status-failed                      { background: rgba(220,53,69,0.15); color: var(--danger); }
.bc-status-canceled, .bc-status-skipped { background: var(--border); color: var(--text-muted); }
.bc-detail-summary { margin-bottom: 10px; padding: 10px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; font-size: 14px; display: flex; flex-direction: column; gap: 4px; }
.bc-detail-actions { display: flex; gap: 8px; margin-bottom: 14px; }

/* Per-row Options dropdown (equipment-catalog part rows; also reusable
   anywhere a row wants to consolidate edit/move/duplicate/archive). */
details.row-opts { position: relative; display: inline-block; }
details.row-opts > summary { list-style: none; cursor: pointer; }
details.row-opts > summary::-webkit-details-marker { display: none; }
details.row-opts > summary::marker { display: none; }
details.row-opts > .row-opts-menu {
  position: absolute; right: 0; top: calc(100% + 4px);
  background: var(--bg-elev, #fff); border: 1px solid var(--border); border-radius: 6px;
  box-shadow: 0 6px 18px rgba(0,0,0,0.12);
  min-width: 170px; padding: 4px 0; z-index: 20;
}
details.row-opts > .row-opts-menu button {
  display: block; width: 100%; text-align: left;
  padding: 7px 12px; background: transparent; border: none; cursor: pointer;
  font: inherit; font-size: 13px; color: var(--text);
}
details.row-opts > .row-opts-menu button:hover { background: var(--bg); }

/* ═══════════════════════════════════════════════════════════
   Dial input mode (Settings → Input Style → Dial)
   ═══════════════════════════════════════════════════════════ */

/* Header row: name + close on the left, blue tag (value) in the center,
   last reading + MAN on the right. align-items:end so left/right drop
   to the bottom edge of the tag. */
.rb-row { display: grid; grid-template-columns: 1fr auto 1fr;
          align-items: start; gap: 10px; }
.rb-row .rb-head-left  { display: flex; flex-direction: column;
                         align-items: flex-start; gap: 2px;
                         min-width: 0; line-height: 20px; }
.rb-row .rb-title { display: flex; align-items: center; gap: 6px;
                    min-width: 0; line-height: 20px; }
.rb-row .rb-head-right { display: flex; align-items: center;
                         justify-content: flex-end; gap: 6px; min-width: 0;
                         line-height: 20px; align-self: end; }
.rb-row .rb-x { background: transparent; border: none; color: var(--danger);
                font-size: 18px; font-weight: 700; padding: 0 4px;
                line-height: 20px; cursor: pointer;
                height: 20px; display: inline-flex; align-items: center; }
.rb-row .rb-name { font-size: 13px; font-weight: 600; line-height: 20px;
                   white-space: nowrap; overflow: hidden;
                   text-overflow: ellipsis; min-width: 0; color: var(--text); }
.rb-row .rb-last { font-size: 10px; line-height: 14px;
                   color: var(--text-muted); white-space: nowrap;
                   overflow: hidden; text-overflow: ellipsis;
                   max-width: 100%; padding-left: 22px; }
.rb-row .rb-man  { flex: none; font-size: 10px; font-weight: 700;
                   padding: 0 5px; letter-spacing: .03em; background: transparent;
                   border: 1px solid var(--text-muted); border-radius: 4px;
                   color: var(--text-muted); cursor: pointer;
                   height: 18px; line-height: 16px; display: inline-flex;
                   align-items: center; }

/* Blue tag — sits in the center grid column */
.rb-tag { background: var(--accent); color: #fff;
          padding: 5px 14px 6px; border-radius: 7px;
          font-size: 20px; font-weight: 800; line-height: 1.1;
          white-space: nowrap; text-align: center;
          box-shadow: 0 4px 8px color-mix(in srgb, var(--accent) 45%, transparent),
                      0 1px 2px rgba(0,0,0,.15); }
.rb-tag .rb-value { color: inherit; }
.rb-tag .rb-tag-unit { font-size: 11px; opacity: .85; margin-left: 2px;
                       font-weight: 700; }

/* Stem connecting the tag to the dial */
.dial-stage { position: relative; padding-top: 14px; }
.dial-tag-stem { position: absolute; left: 50%; top: 0; width: 2px;
                 height: 14px; margin-left: -1px;
                 background: var(--accent);
                 box-shadow: 0 0 6px  color-mix(in srgb, var(--accent) 85%, transparent),
                             0 0 12px color-mix(in srgb, var(--accent) 40%, transparent);
                 z-index: 4; }

/* The dial itself — polished steel cylinder in side profile */
.dial-wrap { position: relative; width: 100%; height: 58px;
             touch-action: pan-y; user-select: none; cursor: grab;
             overflow: visible; }
.dial-wrap:active { cursor: grabbing; }
.dial-body { position: absolute; inset: 0; overflow: hidden; border-radius: 6px;
  box-shadow:
    /* Slot ring — theme-colored outline so the dial reads as fitted
       into a recessed slot in the reading-box card. */
    0 0 0 3px var(--border),
    0 4px 7px  rgba(0,0,0,.25),
    0 1px 2px  rgba(0,0,0,.15),
    /* Sharp 1px top chamfer + softer secondary catch-line just below,
       so the top edge reads as a rounded bevel rather than a flat cap. */
    inset 0  1px 0 rgba(255,255,255,1),
    inset 0  2px 0 rgba(255,255,255,.55),
    inset 0  4px 5px -2px rgba(0,0,0,.38),
    inset 0 -1px 0 rgba(255,255,255,.55),
    inset 0 -3px 4px -2px rgba(0,0,0,.30),
    inset  1px 0 0 rgba(255,255,255,.22),
    inset -1px 0 0 rgba(255,255,255,.22); }
/* Steel cylinder tinted with the active theme accent so the dial recolors
   with the palette instead of staying pool-blue. The gray base stays the
   dominant tone; the accent just shifts the cool steel slightly toward
   the theme. Edge stops are softened (lighter darks) so the dial reads
   as dimensional without looking like a hard black bar against the page. */
.dial-frame { position: absolute; inset: 0;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent) 22%, #62686f)  0%,
    color-mix(in srgb, var(--accent) 24%, #7a8089) 10%,
    color-mix(in srgb, var(--accent) 22%, #a8b1bb) 28%,
    color-mix(in srgb, var(--accent) 16%, #cdd3da) 46%,
    color-mix(in srgb, var(--accent) 10%, #e0e5eb) 50%,
    color-mix(in srgb, var(--accent) 16%, #cdd3da) 54%,
    color-mix(in srgb, var(--accent) 22%, #a8b1bb) 72%,
    color-mix(in srgb, var(--accent) 24%, #7a8089) 90%,
    color-mix(in srgb, var(--accent) 22%, #62686f) 100%); }
/* Specular reflection overlay — directional, not diffuse. Two horizontal
   white bands (no color) over the steel gradient simulate the look of a
   polished cylinder catching light: a bright primary streak in the upper
   third (sky/light reflection) and a softer secondary band below center
   (bounce light from the surface below). White-only so it composites
   neutrally over any theme accent. */
.dial-highlight { position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg,
    rgba(255,255,255,0)    22%,
    rgba(255,255,255,.55)  30%,
    rgba(255,255,255,.14)  40%,
    rgba(255,255,255,0)    48%,
    rgba(255,255,255,0)    58%,
    rgba(255,255,255,.22)  66%,
    rgba(255,255,255,.05)  74%,
    rgba(255,255,255,0)    80%); }
.dial-grooves { position: absolute; inset: 0; display: block;
                width: 100%; height: 100%; pointer-events: none;
                /* Above .dial-highlight so the dark groove rects cut
                   through the specular streak instead of being washed
                   out by it — keeps the grooves reading as recessed
                   incisions, not painted-on stripes flush with the rim. */
                z-index: 1;
                /* SVG rects use fill="currentColor" for the boundary marker,
                   so the limit bar tracks the theme accent. */
                color: var(--accent); }
.dial-center { position: absolute; left: 50%; top: -4px; bottom: -3px;
               width: 2px; margin-left: -1px; background: var(--accent);
               box-shadow: 0 0 7px  color-mix(in srgb, var(--accent) 90%, transparent),
                           0 0 14px color-mix(in srgb, var(--accent) 45%, transparent);
               z-index: 5; }
.dial-center::after { content: ""; position: absolute; left: 50%;
                      width: 9px; height: 9px; background: var(--accent);
                      border-radius: 50%; transform: translateX(-50%);
                      box-shadow: 0 0 7px color-mix(in srgb, var(--accent) 95%, transparent);
                      bottom: -5px; }

/* Per-groove "ratchet" pulse: triggered from JS each time the value snaps
   to a new groove. Widens the indicator and brightens its accent glow
   for ~160ms so the user gets a clear visual click on top of the haptic
   pulse. */
@keyframes dial-tick {
  0%   { transform: scaleX(2.4);
         box-shadow: 0 0 14px color-mix(in srgb, var(--accent) 100%, transparent),
                     0 0 28px color-mix(in srgb, var(--accent)  75%, transparent); }
  100% { transform: scaleX(1);
         box-shadow: 0 0 7px  color-mix(in srgb, var(--accent) 90%, transparent),
                     0 0 14px color-mix(in srgb, var(--accent) 45%, transparent); }
}
.dial-center.is-tick { animation: dial-tick 160ms ease-out; }


/* ── Server-unreachable banner (App.net) ─────────────────────
   The shell-level outage surface. Lives as the first child of <main> in normal
   flow rather than fixed to the viewport: a fixed top bar would cover the
   header and its hamburger button, and a fixed bottom one collides with the
   mobile bottom-nav and the toast stack. In flow it also inherits the page's
   safe-area padding for free.

   📡 is already the app's icon for "no connection" (UI.offlineBanner,
   routes.js), so the palette follows the same convention: warm amber while
   we're only slow or offline, red once the server is genuinely unreachable or
   answering badly. */
.net-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 14px;
  padding: 11px 13px;
  border-radius: 10px;
  border: 1px solid var(--warning);
  background: var(--warning-light);
  color: var(--warning);
  font-size: var(--fs-sm);
}
.net-banner[data-state="network"],
.net-banner[data-state="server"],
.net-banner[data-state="session"] {
  border-color: var(--danger);
  background: var(--danger-light);
  color: var(--danger);
}
.net-banner-icon { font-size: 17px; line-height: 1; flex: none; }
.net-banner-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.net-banner-text strong { font-weight: 700; }
.net-banner-text span   { opacity: .85; }
.net-banner-btn {
  flex: none;
  background: transparent;
  border: 1px solid currentColor;
  color: inherit;
}
@media (max-width: 420px) {
  /* Keep the button on its own line rather than squeezing the message to two
     words per line on a phone. */
  .net-banner { flex-wrap: wrap; }
  .net-banner-text { flex-basis: 100%; order: 2; }
  .net-banner-btn  { order: 3; margin-left: auto; }
}

/* ── Invoice line-item editor ──────────────────────────────────────────
   Compact on purpose. Each line used to be four full-height stacked rows
   (description / qty × price / discount / detail textarea), so a phone
   screen held barely one and a half items and a three-item invoice was all
   scrolling. Discount + detail now live in a per-line drawer behind the ⋯
   button — opened automatically for a line that already carries either, so
   nothing that has been set is ever hidden. */
.li-card {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 6px 7px;
  margin-bottom: 6px;
  background: var(--card);
}
.li-row { display: flex; gap: 6px; align-items: center; }
.li-row + .li-row { margin-top: 5px; }
.li-card input, .li-card select, .li-card textarea {
  padding: 6px 8px;
  font-size: 13px;
  background: var(--card);
}
.li-card textarea { min-height: 0; font-size: 12px; color: var(--text-muted); }
.li-x {
  flex: none; background: none; border: none; color: var(--danger);
  font-size: 18px; line-height: 1; padding: 2px 6px; cursor: pointer;
}
.li-more {
  flex: none; background: none; border: 1px solid var(--border);
  border-radius: var(--radius-sm); color: var(--text-muted);
  font-size: 13px; line-height: 1; padding: 5px 8px; cursor: pointer;
}
.li-more.on { color: var(--accent); border-color: var(--accent); }
.li-note { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
.li-drawer { margin-top: 6px; padding-top: 6px; border-top: 1px dashed var(--border); }

/* ── Update-available bar ────────────────────────────────
   Shown by App.update when the server is serving a newer front-end build than
   the one this window loaded. Deliberately has no × : until the user reloads,
   the app on screen really is out of date, and dismissing the bar would leave
   nothing on screen saying so. Sits above the bottom nav on mobile and below
   modals everywhere, so it never covers what the user is working in. */
#update-banner {
  position: fixed;
  left: 12px; right: 12px;
  bottom: calc(12px + env(safe-area-inset-bottom));
  max-width: 676px;
  margin: 0 auto;
  z-index: var(--z-nav);
  display: flex; align-items: center; gap: 10px;
  background: var(--header-bg);
  color: #f0f4ff;
  border-radius: var(--radius);
  box-shadow: 0 6px 20px rgba(0,0,0,0.28);
  padding: 10px 12px;
  font-size: var(--fs-sm);
}
#update-banner .ub-text { flex: 1; line-height: 1.35; }
#update-banner .ub-btn {
  background: var(--accent); color: #fff; border: none;
  padding: 8px 14px; border-radius: 6px;
  font-weight: 600; font-size: var(--fs-sm); cursor: pointer;
  white-space: nowrap;
}
@media (max-width: 720px) {
  /* Clear the bottom nav (see .app-main's matching padding). */
  #update-banner { bottom: calc(76px + env(safe-area-inset-bottom)); }
}

/* ── Fingerprint sign-in (WebAuthn) ──────────────────────────────────────
   Two surfaces: a divider + button under the login form, and the device list
   in the sidebar panel. The login half must stand on its own — it renders
   before the app shell exists, so it can only lean on the login card's own
   type scale, not on anything a tab sets up. */
.login-or {
  display: flex; align-items: center; gap: 10px;
  margin: 16px 0 12px;
  color: var(--text-muted);
  font-size: 12px; text-transform: uppercase; letter-spacing: .08em;
}
.login-or::before, .login-or::after {
  content: ''; flex: 1; height: 1px; background: var(--border);
}

.wa-note {
  margin: 0 0 14px;
  color: var(--text-muted);
  font-size: 13px; line-height: 1.5;
}
.wa-setup {
  padding: 12px;
  background: var(--accent-light);
  border-radius: var(--radius-md);
  margin-bottom: 18px;
}
.wa-label {
  display: block; margin-bottom: 6px;
  font-size: 12px; font-weight: 700; color: var(--text);
}
.wa-setup input[type="text"], .wa-setup input[type="password"] {
  width: 100%; margin-bottom: 10px;
  padding: 9px 10px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--card); color: var(--text);
  font-size: 15px;   /* 16px-ish floor: anything smaller and iOS zooms the page */
}
.wa-note-tight { margin: -4px 0 10px; font-size: 12px; }
.wa-list-head {
  margin-bottom: 8px;
  font-size: 12px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--text-muted);
}
.wa-device {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "name button" "meta button";
  align-items: center; gap: 2px 10px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
}
.wa-device-name {
  grid-area: name;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: transparent; color: var(--text);
  padding: 3px 6px; margin-left: -6px;
  font-size: 15px; font-weight: 600;
  min-width: 0;   /* a grid item will not shrink below its content without this */
}
.wa-device-name:focus {
  border-color: var(--border); background: var(--card); outline: none;
}
.wa-device-meta { grid-area: meta; font-size: 12px; color: var(--text-muted); padding-left: 0; }
.wa-device .btn { grid-area: button; }
