/* Smart DeonTech — design tokens
   Palette: ink #0B1220, paper #F3F6FC, primary royal blue #0B5FBF, accent sky #0EA5E9, black #0B1220, white #FFFFFF
   Type: Geist (headings & body — modern, crisp at UI sizes), Geist Mono (numbers/SKUs)
*/
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400..800&family=Geist+Mono:wght@500;600&display=swap');

:root {
  --ink: #0B1220;
  --paper: #F3F6FC;
  --panel: #FFFFFF;
  --primary: #0B5FBF;
  --primary-dark: #073E82;
  --primary-light: #3B82F6;
  --accent: #0EA5E9;
  --primary-gradient: linear-gradient(135deg, #073E82 0%, #0B5FBF 55%, #0EA5E9 100%);
  --amber: #E08A1E;
  --red: #D6362B;
  --rule: #E3E9F4;
  --muted: #64748B;
  --radius: 12px;
  --shadow-sm: 0 1px 2px rgba(11, 18, 32, 0.06);
  --shadow-md: 0 8px 24px rgba(11, 34, 66, 0.10);
  --shadow-glow: 0 0 0 4px rgba(11, 95, 191, 0.12);
  --hover-surface: #F7FAFF;
  --toast-bg: #0B1220;
}

/* ---- Dark theme ----
   Toggled by app.js, which sets data-theme="dark" on <html> automatically
   in the evening (default 7 PM–6 AM local time) or when the user picks
   "Dark" from the sidebar theme toggle. */
html[data-theme="dark"] {
  --ink: #E7ECF5;
  --paper: #0B1220;
  --panel: #131C2E;
  --primary-light: #5EA3F5;
  --rule: #232F45;
  --muted: #93A2BC;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.45);
  --hover-surface: #1A2438;
  --toast-bg: #E7ECF5;
}
html[data-theme="dark"] body { color-scheme: dark; }
html[data-theme="dark"] input,
html[data-theme="dark"] select,
html[data-theme="dark"] textarea { background: var(--panel); }
html[data-theme="dark"] .modal,
html[data-theme="dark"] .sheet,
html[data-theme="dark"] .tabbar { background: var(--panel); }
html[data-theme="dark"] .login-card { background: rgba(19, 28, 46, 0.98); }
html[data-theme="dark"] .toast { color: #0B1220; }
/* Filter chips, the cash/accrual toggle, item thumbnails, and the
   secondary/danger button hover states all had a hardcoded LIGHT
   background paired with text that uses --ink/--muted (which flip to a
   light color in dark mode) — that combination goes light-text-on-light-
   background and disappears. Overriding just the backgrounds here fixes it
   without touching the light-theme look at all. */
html[data-theme="dark"] .chip:not(.active) { background: var(--panel); }
html[data-theme="dark"] .basis-toggle { background: var(--rule); }
html[data-theme="dark"] .basis-toggle button.active { background: var(--panel); }
html[data-theme="dark"] .item-thumb,
html[data-theme="dark"] .item-thumb-placeholder { background: var(--panel); }
html[data-theme="dark"] .btn-secondary:hover { background: var(--hover-surface); border-color: var(--primary-light); }
html[data-theme="dark"] .btn-danger { background: var(--panel); }
html[data-theme="dark"] .btn-danger:hover { background: rgba(214, 54, 43, 0.18); }
/* Same issue as above, found 2026-07-10: the dashboard's Total Receivables
   and Overdue Invoices cards, plus disabled form fields, all had a
   hardcoded light background paired with --ink/--muted text — the figures
   were washing out in dark mode. The colored count inside Overdue (red, or
   green when zero) already uses a fixed non-flipping color, so that signal
   survives even once the card background goes neutral here. */
html[data-theme="dark"] .receivables-card { background: var(--panel); border-color: var(--rule); }
html[data-theme="dark"] .overdue-card { background: var(--panel); border-color: var(--rule); }
html[data-theme="dark"] .overdue-card.zero { background: var(--panel); border-color: var(--rule); }
html[data-theme="dark"] input:disabled,
html[data-theme="dark"] select:disabled,
html[data-theme="dark"] textarea:disabled { background: var(--hover-surface); }

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: 'Geist', sans-serif;
  font-size: 15.5px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  transition: background 0.2s ease, color 0.2s ease;
}

h1, h2, h3, .display {
  font-family: 'Geist', sans-serif;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 0;
}

.mono { font-family: 'Geist Mono', monospace; }

a { color: var(--primary); text-decoration: none; }

/* Media never overflows its container. Elements that need a fixed size
   (logos, thumbnails, avatars) set it with higher specificity, so this only
   acts as a safety cap. */
img { max-width: 100%; height: auto; }

/* Global loading bar (driven by api() in app.js). */
.app-loading-bar {
  position: fixed; top: env(safe-area-inset-top, 0); left: 0; right: 0;
  height: 3px; z-index: 300; overflow: hidden; pointer-events: none;
  opacity: 0; transition: opacity 0.2s ease;
}
.app-loading-bar.on { opacity: 1; }
.app-loading-bar .bar {
  height: 100%; width: 40%; border-radius: 0 3px 3px 0;
  background: var(--primary-gradient);
  animation: appLoadSlide 1.1s ease-in-out infinite;
}
@keyframes appLoadSlide {
  0% { margin-left: -40%; }
  100% { margin-left: 100%; }
}

/* Layout shell */
.shell { display: flex; min-height: 100vh; }

.sidebar {
  width: 226px;
  flex-shrink: 0;
  background: linear-gradient(180deg, #0B1220 0%, #0F1B33 100%);
  color: #EDEDE8;
  padding: 24px 16px;
  display: flex;
  flex-direction: column;
}

.sidebar .side-logo {
  height: 42px;
  width: auto;
  margin-bottom: 12px;
  align-self: flex-start;
}
.sidebar .brand {
  font-family: 'Geist', sans-serif;
  font-weight: 800;
  font-size: 17px;
  color: #fff;
  margin-bottom: 2px;
}
.sidebar .brand-sub {
  font-size: 11px;
  color: #8DA3C7;
  margin-bottom: 28px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.nav-link {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 10px 12px;
  border-radius: 9px;
  color: #B9C6DE;
  font-size: 13.5px;
  font-weight: 500;
  margin-bottom: 3px;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, transform 0.1s ease;
  border-left: 3px solid transparent;
}
.nav-link svg { width: 19px; height: 19px; flex-shrink: 0; }
.nav-link:hover { background: rgba(255,255,255,0.07); color: #fff; }
.nav-link.active {
  background: linear-gradient(90deg, rgba(11,95,191,0.35), rgba(14,165,233,0.12));
  color: #fff;
  border-left: 3px solid var(--accent);
}

/* Mobile bottom tab bar (hidden on desktop) */
.tabbar { display: none; }

.sidebar .spacer { flex: 1; }
.theme-toggle-btn {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 9px 12px; margin-bottom: 12px; border-radius: 9px;
  background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1);
  color: #B9C6DE; font-size: 12.5px; font-weight: 600; font-family: inherit;
  cursor: pointer; transition: background 0.15s ease, color 0.15s ease;
}
.theme-toggle-btn svg { width: 16px; height: 16px; flex-shrink: 0; }
.theme-toggle-btn:hover { background: rgba(255,255,255,0.12); color: #fff; }
.sidebar .user-box {
  border-top: 1px solid rgba(255,255,255,0.1);
  padding-top: 14px;
  font-size: 12.5px;
  color: #8DA3C7;
}
.sidebar .user-box .u-name { color: #fff; font-weight: 600; }
.sidebar .logout-btn {
  margin-top: 8px;
  font-size: 12px;
  color: #5EC5F5;
  cursor: pointer;
  background: none;
  border: none;
  padding: 0;
}
.sidebar .logout-btn:hover { color: #fff; }

/* margin-inline:auto centres the capped column on wide monitors instead of
   pinning it to the left with dead space on the right. min-width:0 lets a
   too-wide table inside scroll within its own card rather than forcing the
   whole page wider (flex items default to min-width:auto). */
.main { flex: 1; padding: 32px 40px; max-width: 1200px; margin-inline: auto; min-width: 0; }

/* flex-wrap here for the same reason as .modal-actions: the button cluster on
   the right (Procurement's "+ Record RMB swap / + Transfer between accounts",
   Inventory's "Stock log / Export / + Add product") is made of nowrap buttons
   that can't shrink. The ≤820px rules already stacked this; between 821px and
   ~1000px — a small laptop or landscape tablet — it had no escape valve and
   would push the page sideways. */
.page-header {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 24px;
}
.page-header > div:last-child { flex-wrap: wrap; }
.page-header h1 { font-size: 26px; }
.page-header .sub { color: var(--muted); font-size: 14px; margin-top: 4px; }

/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 9px 17px;
  border-radius: 9px;
  border: 1px solid transparent;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  font-family: inherit;
  transition: transform 0.08s ease, box-shadow 0.15s ease, background 0.15s ease;
}
.btn-primary { background: var(--primary-gradient); color: #fff; box-shadow: var(--shadow-sm); }
.btn-primary:hover { box-shadow: var(--shadow-glow); transform: translateY(-1px); }
.btn-secondary { background: var(--panel); border-color: var(--rule); color: var(--ink); }
.btn-secondary:hover { background: #EEF3FC; border-color: var(--primary-light); }
.btn-danger { background: #fff; border-color: var(--red); color: var(--red); }
.btn-danger:hover { background: #FCEBEA; }
.btn-sm { padding: 5px 11px; font-size: 12.5px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* Table cells set word-break/overflow-wrap so long emails and invoice numbers
   can wrap — but those properties are INHERITED, so a button squeezed into a
   narrow action column would shatter one letter per line ("E d i t"). Pin the
   text behaviour on small inline controls so a label can never break mid-word
   on any page. */
.btn, .badge, .chip, .tab, .pill {
  white-space: nowrap;
  word-break: normal;
  overflow-wrap: normal;
}

/* Cards / tables */
.card {
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 20px;
  box-shadow: var(--shadow-sm);
}

/* Wide-table safety net at ALL widths (this used to apply only ≤820px, so on
   desktop and tablet a too-wide table — users, procurement, audit log — could
   push the page itself sideways). The table now scrolls inside its own card
   instead. Scoped with :has(> table) so it never clips cards holding pop-over
   menus or dropdowns; the ≤640px rule below sets it back to visible, where
   tables stack into labelled cards and there is nothing left to scroll. */
.main .card:has(> table) {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* auto-fit rather than a hard 4 columns: any number of cards (4, 5, 6…)
   distributes evenly at every width, so a 5th card never drops to a
   half-width "orphan" on its own row. Replaces the old 820px/460px
   column-count overrides, which are now redundant. */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 16px; margin-bottom: 28px; }
.stat-card { background: var(--panel); border: 1px solid var(--rule); border-radius: var(--radius); padding: 18px 20px; box-shadow: var(--shadow-sm); }
.stat-card .label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 8px; }
.stat-card .value { font-family: 'Geist', sans-serif; font-size: 27px; font-weight: 800; }
.stat-card .value.warn { color: var(--amber); }

/* ---- Role-aware landing (dashboard top): greeting hero + tiles + quick actions ---- */
.landing { margin-bottom: 20px; }
.landing-top { display: grid; grid-template-columns: 1fr; gap: 14px; margin-bottom: 14px; }
@media (min-width: 760px) { .landing-top { grid-template-columns: 1.05fr 1.95fr; align-items: stretch; } }
.landing-hero {
  background: var(--primary-gradient); color: #fff;
  border-radius: 16px; padding: 22px; box-shadow: var(--shadow-sm);
  display: flex; flex-direction: column; justify-content: center; min-height: 128px;
}
.landing-hero .lh-label { font-size: 13px; opacity: 0.88; margin-bottom: 8px; }
.landing-hero .lh-value { font-family: 'Geist', sans-serif; font-size: 34px; font-weight: 800; line-height: 1.05; }
.landing-hero .lh-sub { font-size: 12.5px; opacity: 0.85; margin-top: 8px; }
.landing-tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.landing-tile {
  background: var(--panel); border: 1px solid var(--rule); border-radius: 14px;
  padding: 14px 16px; box-shadow: var(--shadow-sm);
}
.landing-tile .lt-label { font-size: 11.5px; color: var(--muted); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.03em; }
.landing-tile .lt-value { font-family: 'Geist', sans-serif; font-size: 20px; font-weight: 700; }
.landing-tile .lt-value.warn { color: var(--amber); }
.landing-tile .lt-value.danger { color: var(--red); }
.landing-attn { margin-bottom: 14px; }
.landing-attn-label { font-size: 11.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 8px; }
.attn-item {
  display: flex; align-items: center; gap: 12px;
  background: var(--panel); border: 1px solid var(--rule); border-radius: 12px;
  padding: 12px 14px; margin-bottom: 8px; color: var(--ink);
  transition: border-color .15s ease;
}
.attn-item:hover { border-color: var(--primary-light); }
.attn-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; background: var(--muted); }
.attn-dot.danger { background: var(--red); }
.attn-dot.warn { background: var(--amber); }
.attn-text { display: flex; flex-direction: column; gap: 1px; flex: 1 1 auto; min-width: 0; }
.attn-title { font-size: 13.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.attn-meta { font-size: 12px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.attn-item svg { width: 16px; height: 16px; color: var(--muted); flex: 0 0 auto; }

.landing-quick { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
@media (min-width: 760px) { .landing-quick { grid-template-columns: repeat(4, 1fr); } }
.landing-action {
  display: flex; align-items: center; gap: 10px;
  background: var(--panel); border: 1px solid var(--rule); border-radius: 12px;
  padding: 13px 14px; color: var(--ink); font-weight: 600; font-size: 13.5px;
  transition: border-color .15s ease, transform .1s ease;
}
.landing-action:hover { border-color: var(--primary-light); }
.landing-action:active { transform: scale(0.985); }
.landing-action svg { width: 20px; height: 20px; color: var(--primary); flex: 0 0 auto; }

table { width: 100%; border-collapse: collapse; }
th {
  text-align: left;
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  font-weight: 700;
  padding: 10px 12px;
  border-bottom: 1px solid var(--rule);
}
td { padding: 12px; border-bottom: 1px solid var(--rule); font-size: 14px; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: var(--hover-surface); }

.badge {
  display: inline-block;
  padding: 3px 9px;
  border-radius: 100px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
.badge-paid { background: #E1F0FE; color: var(--primary); }
.badge-partial { background: #EFE6FB; color: #6B3FA0; }
.badge-unpaid { background: #FBF1DE; color: var(--amber); }
.badge-overdue { background: #FCEAE9; color: var(--red); }
.badge-void { background: #EEF1F6; color: #7A8699; }
.badge-low { background: #FCEAE9; color: var(--red); }
.badge-ok { background: #E1F0FE; color: var(--primary); }

/* A wrapping row of badges (e.g. a staff member's permissions). Keeps many
   chips from running off-screen — they flow onto new lines instead. */
.perm-list { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
@media (max-width: 640px) {
  /* In the stacked mobile card view the cell is a flex row (label | value);
     let the badge list take the value side, hug the right, and wrap. */
  table.responsive td .perm-list { flex: 1 1 auto; min-width: 0; justify-content: flex-end; }
}

/* Forms */
label { display: block; font-size: 12.5px; font-weight: 600; color: var(--muted); margin-bottom: 5px; }
input, select, textarea {
  width: 100%;
  padding: 9px 11px;
  border: 1px solid var(--rule);
  border-radius: 8px;
  font-family: inherit;
  font-size: 14px;
  background: #fff;
  color: var(--ink);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--primary); box-shadow: var(--shadow-glow); }
input:disabled, select:disabled, textarea:disabled { background: #F3F4F6; color: var(--muted); cursor: not-allowed; }
.field { margin-bottom: 14px; }
/* Flexible wrapping row instead of a hard 2-column grid. A normal two-field
   row still splits into equal halves, but rows with 3–6 fields (payment
   plans, procurement shipment details, settings) now flow and wrap evenly
   instead of stacking lopsidedly two-per-line. Inline max-widths on a single
   field still cap it; min-width:0 stops a long value forcing overflow. */
.field-row { display: flex; flex-wrap: wrap; gap: 14px; }
/* Only actual fields get a growth basis. Buttons and other loose controls in
   a row (the ✕ remove button on procurement/invoice line items, photo
   pickers) keep their natural size — giving them `flex: 1 1 200px` blew the
   ✕ up into a full-width box. */
.field-row > .field,
.field-row > .field-row { flex: 1 1 200px; min-width: 0; }
.field-row > * { min-width: 0; }

/* Procurement / receive line-item rows carry inline `flex:2`, `flex:1`… on
   their fields, which sets flex-basis to 0 — so with 7 fields plus a photo
   picker and the ✕ in one row, the numeric inputs collapsed to nothing but
   their spinner arrows and the labels ran into each other. Give each field a
   floor it can't shrink past; the row then wraps onto a second line on a
   narrow window instead of crushing everything. */
.proc-item .field-row > .field { min-width: 92px; }
.proc-item .field-row > .field:has(.it-name) { min-width: 170px; }
.proc-item .field-row > .field:has(.it-product) { min-width: 150px; }
.proc-item .field-row > .field:has(.it-sku) { min-width: 110px; }
.proc-item .field-row > .field:has(.it-usd),
.proc-item .field-row > .field:has(.it-cost) { min-width: 100px; }
/* Escape hatch: invoice line-item rows, job-card rows and a few procurement
   rows hand-tune their own track ratios inline (e.g. "2fr 0.7fr auto", where
   `auto` is a small ✕ button). Those keep grid behaviour — under flex the ✕
   would grow to a 200px basis like every other child. Attribute selector so
   it needs no HTML/JS change and stays in sync automatically. */
.field-row[style*="grid-template-columns"] { display: grid; }
.field-row[style*="grid-template-columns"] > * { flex: none; }

/* Hold-to-reveal password toggle — wrapper + button are injected around
   every input[type=password] by wirePasswordReveal() in app.js. */
.pw-field { position: relative; }
.pw-field input { padding-right: 40px; }
.pw-toggle {
  position: absolute;
  top: 50%;
  right: 4px;
  transform: translateY(-50%);
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: none;
}
.pw-toggle:hover { background: rgba(0,0,0,0.05); color: var(--ink); }
.pw-toggle:active, .pw-toggle.revealing { color: var(--primary); background: rgba(0,0,0,0.06); }
.pw-toggle svg { width: 18px; height: 18px; pointer-events: none; }

/* Searchable product picker (invoice / recurring-invoice line items) — a
   text input that filters the product list live instead of a giant
   <select>, which becomes unusable once the catalog grows past a
   screenful. See wireProductSearch() in page-invoices.js. */
.product-search-results {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 4px;
  max-height: 220px;
  overflow-y: auto;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 8px;
  box-shadow: var(--shadow-md);
  z-index: 20;
  display: none;
}
.product-search-results.open { display: block; }
.product-search-item {
  padding: 8px 11px;
  font-size: 13px;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
}
.product-search-item:hover, .product-search-item.highlighted { background: var(--hover-surface); }
.debrief-prod-item:hover { background: var(--hover-surface); }

/* Reusable wheel time picker (hour + minute 00–59 + AM/PM, plus typing).
   Built by initTimeSelect() in app.js: a typeable .tw-input with an
   iOS-style scroll-wheel .tw-pop popup underneath. TW_ITEM_H in app.js
   (38px) must match .tw-item's height, and .tw-pad's height must stay at
   exactly 2 rows (76px) so scrollTop = index * 38 centers that index under
   the .tw-center band — see the comment above initTimeSelect for the math.
   The native-iOS-picker look (numbers shrinking/fading as they move away
   from center, plus the `mask-image` fade at the column's top/bottom
   edges) is driven live by JS (updateArc in app.js) via per-item
   `scale`/`opacity` — flat 2D only, no 3D rotation. Gideon compared a flat
   version against a real rotateX+translateZ 3D drum in a live side-by-side
   demo and picked flat, so don't reintroduce 3D here without the same kind
   of live comparison — tuning 3D transforms from code alone (no visual
   feedback) produced a worse result last time.
   IMPORTANT: .tw-pop's visibility is controlled by the `hidden` attribute
   (JS toggles pop.hidden). Do NOT give .tw-pop an unconditional `display`
   value without also pairing it with a `.tw-pop[hidden]` override below —
   author CSS beats the browser's default `[hidden]{display:none}` rule
   regardless of source order, so an unconditional `display` here would
   make the popup impossible to close.
   IMPORTANT: .tw-center (the highlight band) is `position:absolute`, which
   always paints above plain in-flow content in the same stacking context
   regardless of DOM order — so .tw-col must carry its own `position` +
   `z-index` (below) to lift the item text above the band, or the selected
   number disappears behind the highlight. */
.time-wheel { position: relative; display: inline-block; }
.tw-input { width: 130px; }
.tw-input:disabled { opacity: .6; cursor: not-allowed; background: var(--hover-surface); }
.tw-pop {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  padding: 10px 10px 8px;
}
.tw-pop[hidden] { display: none; }
.tw-cols { position: relative; display: flex; gap: 8px; height: 190px; }
.tw-col {
  position: relative;
  z-index: 1;
  width: 60px;
  height: 190px;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  scrollbar-width: none;
  -ms-overflow-style: none;
  touch-action: pan-y;
  overscroll-behavior: contain;
  cursor: grab;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 22%, #000 78%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 22%, #000 78%, transparent 100%);
}
.tw-col::-webkit-scrollbar { display: none; width: 0; height: 0; }
.tw-col.dragging { cursor: grabbing; }
.tw-ap { width: 56px; }
.tw-pad { height: 76px; flex-shrink: 0; }
.tw-item {
  height: 38px;
  line-height: 38px;
  text-align: center;
  font-size: 16px;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  cursor: pointer;
  scroll-snap-align: center;
  user-select: none;
  transition: color .1s;
}
.tw-item.on { color: var(--ink); font-weight: 700; font-size: 21px; }
/* Native iOS picker look: just two thin full-width divider lines around the
   selected row, no filled/rounded highlight pill. */
.tw-center {
  position: absolute;
  top: 76px;
  left: 0;
  right: 0;
  height: 38px;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  pointer-events: none;
}
.tw-actions { text-align: right; margin-top: 8px; }
.product-search-item .sub { font-size: 11px; color: var(--muted); white-space: nowrap; }
.product-search-empty { padding: 10px 11px; font-size: 12.5px; color: var(--muted); }

.modal-overlay {
  position: fixed; inset: 0; background: rgba(7,16,32,0.55); backdrop-filter: blur(2px);
  display: flex; align-items: center; justify-content: center; z-index: 100;
}
.modal {
  background: #fff; border-radius: var(--radius); padding: 24px;
  width: 480px; max-width: 90vw; max-height: 85vh; overflow-y: auto;
  box-shadow: var(--shadow-md);
}

/* In-app PDF viewer (invoice/receipt preview). Keeps the PDF inside the app so
   the Back button in .pdf-viewer-bar is always reachable — the installed PWA
   has no browser chrome/back button, so a full-page PDF would trap the user. */
.pdf-viewer {
  background: #fff; border-radius: var(--radius);
  width: min(96vw, 900px); height: 92vh; max-height: 92vh;
  display: flex; flex-direction: column; overflow: hidden;
  box-shadow: var(--shadow-md);
}
.pdf-viewer-bar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 10px 12px; border-bottom: 1px solid var(--rule);
  background: var(--panel);
}
.pdf-viewer-title {
  font-weight: 600; font-size: 14px; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The body scrolls; PDF.js appends one <canvas class="pdf-page"> per page. */
.pdf-viewer-body {
  position: relative; flex: 1; min-height: 0; background: #525659;
  overflow: auto; -webkit-overflow-scrolling: touch;
}
.pdf-doc { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 12px; }
.pdf-doc .pdf-page {
  max-width: 100%; background: #fff; border-radius: 2px;
  box-shadow: 0 1px 6px rgba(0,0,0,0.45);
}
.pdf-doc .pdf-image {
  max-width: 100%; height: auto; border-radius: 4px;
  box-shadow: 0 1px 6px rgba(0,0,0,0.45);
}
/* Centered status ("Loading…") / fallback message overlay. */
.pdf-msg {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  text-align: center; color: #E7ECF5; font-size: 13.5px; padding: 0 20px; line-height: 1.5;
}
.pdf-msg a { color: #fff; text-decoration: underline; }
@media (max-width: 820px) {
  .pdf-viewer { width: 100vw; height: 100dvh; max-height: 100dvh; border-radius: 0; }
}

/* Bulk multi-select toolbar (Invoices / Job cards / Customers / Inventory) */
.bulk-bar {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(20px + env(safe-area-inset-bottom));
  display: flex; align-items: center; gap: 14px;
  background: var(--toast-bg, #10233f); color: #fff;
  padding: 10px 14px; border-radius: 12px; box-shadow: var(--shadow-md);
  z-index: 150; max-width: min(94vw, 720px); flex-wrap: wrap;
}
.bulk-bar[hidden] { display: none; }
.bulk-bar .bulk-count { font-size: 13.5px; font-weight: 600; white-space: nowrap; }
.bulk-bar .bulk-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.bulk-cb, .bulk-all { width: 16px; height: 16px; cursor: pointer; }
td:has(> .bulk-cb), th:has(> .bulk-all) { width: 34px; text-align: center; }
/* Sit above the mobile bottom tab bar so it never hides the actions. */
@media (max-width: 820px) { .bulk-bar { bottom: calc(76px + env(safe-area-inset-bottom)); width: auto; } }

/* "or" divider (e.g. between password login and biometric login) */
.or-divider { display: flex; align-items: center; gap: 12px; margin: 16px 0; color: var(--muted); font-size: 12px; }
.or-divider::before, .or-divider::after { content: ''; flex: 1; height: 1px; background: var(--border); }

.modal h2 { font-size: 18px; margin-bottom: 18px; }

/* Floating "Done" bar shown above the on-screen keyboard on phones so you can
   drop the keyboard when finished typing. Positioned by --app-vvh (the live
   visible height kept in sync by app.js), so it hugs the top of the keyboard.
   Toggled via the [hidden] attribute from app.js; only wired up on mobile. */
#kbDoneBar {
  position: fixed;
  top: calc(var(--app-vvh, 100dvh) - 50px);
  right: 12px;
  z-index: 300;
  pointer-events: none;
}
#kbDoneBar[hidden] { display: none; }
.kb-done-btn {
  pointer-events: auto;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 14px; border: none; border-radius: 999px;
  background: var(--primary); color: #fff;
  font-family: inherit; font-size: 14px; font-weight: 600;
  box-shadow: var(--shadow-md); cursor: pointer;
}
.kb-done-btn svg { display: block; }
/* flex-wrap is essential here. Buttons are white-space:nowrap (so labels can't
   shatter mid-word), which means a three-button footer like the New invoice
   modal's "Cancel / Create invoice and send email / Create invoice" can't
   shrink to fit a phone. Without wrapping, the row overflows — and because
   it's justify-content:flex-end the overflow spills off the LEFT edge, hiding
   Cancel entirely with no way to scroll to it. */
.modal-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; margin-top: 20px; }

.toast {
  position: fixed; bottom: 24px; right: 24px;
  background: var(--toast-bg); color: #fff; padding: 12px 18px; border-radius: 9px;
  font-size: 13.5px; z-index: 200; max-width: 320px; box-shadow: var(--shadow-md);
}
.toast.error { background: var(--red); }

.empty-state { text-align: center; padding: 50px 20px; color: var(--muted); }
.empty-state .signature { font-size: 32px; margin-bottom: 8px; font-family: 'Geist Mono', monospace; letter-spacing: 0.1em; }

.search-bar { display: flex; gap: 10px; margin-bottom: 18px; }
.search-bar input { flex: 1; }

/* ---- Login ---- */
.login-shell {
  min-height: 100vh; display: flex; align-items: stretch; justify-content: center;
  background: var(--primary-gradient);
  position: relative;
  overflow: hidden;
}
.login-shell::before, .login-shell::after {
  content: ''; position: absolute; border-radius: 50%; opacity: 0.18; background: #fff;
}
.login-shell::before { width: 480px; height: 480px; top: -180px; left: -140px; }
.login-shell::after { width: 360px; height: 360px; bottom: -160px; right: -100px; background: var(--accent); opacity: 0.28; }

.login-wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; padding: 24px; z-index: 1; }
.login-card {
  background: rgba(255,255,255,0.98); width: 400px; max-width: 100%; padding: 40px 34px; border-radius: 18px;
  box-shadow: 0 20px 60px rgba(4, 16, 38, 0.35);
}
.login-card .brand { font-family: 'Geist', sans-serif; font-weight: 800; font-size: 21px; margin-bottom: 2px; color: var(--ink); }
.login-card .brand-sub { color: var(--muted); font-size: 12.5px; margin-bottom: 26px; }
.login-error { color: var(--red); font-size: 13px; margin-top: 10px; min-height: 18px; }
.login-links { display: flex; justify-content: space-between; margin-top: 14px; font-size: 12.5px; }
.login-links a { color: var(--primary); font-weight: 600; cursor: pointer; }
.login-links a:hover { text-decoration: underline; }

.role-tag { font-size: 11px; text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); font-weight: 600; }

@media (max-width: 820px) {
  /* .stat-grid needs no override here — the auto-fit track sizing above
     already yields 1–2 columns at phone/tablet widths. */

  /* Stop accidental sideways scrolling at the root — the page still scrolls
     vertically the normal way, so no content gets clipped (unlike putting
     this on .main). */
  html { overflow-x: hidden; }

  /* Hide the sidebar, show a bottom tab bar instead */
  .sidebar { display: none; }
  /* Do NOT put overflow-x on .main. It's a stretched flex item under
     .shell{min-height:100vh}; any non-visible overflow-x makes iOS compute
     overflow-y:auto and clip the column to ~one screen, hiding everything
     lower down (Finance ledger, dashboard Job cards). Horizontal overflow is
     instead guarded at the root (html) below, which scrolls vertically the
     normal way and never clips page content. */
  .main { padding: 16px 16px 90px; max-width: 100%; min-width: 0; }

  /* Button clusters in a page header (Inventory's Stock log / Export /
     + Add product, etc.) must wrap instead of running off the right edge
     and getting clipped. */
  .page-header > div:last-child { flex-wrap: wrap; }
  .page-header > div:last-child > .btn { flex: 1 1 auto; }

  /* Keep wide tables from stretching the whole page (which made mobile
     browsers zoom the page out — everything tiny, prices spilling, dead
     whitespace below). Instead each table scrolls sideways *inside* its own
     card, so the page itself never grows wider than the screen. */
  .shell { min-width: 0; }
  /* Table stays inside its card and scrolls sideways only if it genuinely
     can't fit — no more stretching the whole page. */
  .main .card { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .main .card > table { width: 100%; }

  /* Let long text columns wrap so the table shrinks to the screen, but keep
     amounts, dates and status badges on a single line so they read cleanly. */
  td { white-space: normal; word-break: break-word; }
  td.mono, .badge { white-space: nowrap; }

  /* Action-button columns (invoices/jobs/users/etc.) can hold up to ~7
     buttons. Let them wrap onto multiple lines and shrink a touch so a row
     stays compact instead of forcing a very wide, swipe-forever table.
     !important overrides the inline `white-space:nowrap` set in the JS. */
  table td:last-child { white-space: normal !important; }
  table td:last-child .btn { margin: 2px 3px 2px 0; }

  /* Touch targets: every tappable element is at least 44px tall with room to
     breathe between neighbours. */
  .btn { min-height: 44px; }
  .btn-sm { min-height: 44px; padding: 8px 14px; font-size: 13px; }
  .tab { min-height: 44px; }
  .chip { min-height: 40px; }

  /* 16px inputs stop iOS from auto-zooming the page when a field is focused
     (a big part of why the app felt "zoomed/tiny" before). */
  input, select, textarea { min-height: 44px; font-size: 16px; }
  label { font-size: 13px; }

  /* Respect notch / rounded-corner safe areas on the sides. */
  .main {
    padding-left: max(16px, env(safe-area-inset-left));
    padding-right: max(16px, env(safe-area-inset-right));
  }
  .tabbar { padding-left: max(4px, env(safe-area-inset-left)); padding-right: max(4px, env(safe-area-inset-right)); }
  .fab { right: max(24px, env(safe-area-inset-right)); }

  /* Long, unbreakable strings (emails, invoice numbers) must never force
     horizontal overflow. */
  td, .cust-sub, .cust-name, .mono { overflow-wrap: anywhere; }

  .tabbar {
    display: flex;
    position: fixed;
    bottom: 0; left: 0; right: 0;
    background: #fff;
    border-top: 1px solid var(--rule);
    padding: 6px 4px calc(6px + env(safe-area-inset-bottom, 0px));
    z-index: 50;
    justify-content: space-around;
    box-shadow: 0 -4px 16px rgba(11,18,32,0.06);
  }
  .tabbar .tab {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 4px 2px;
    color: var(--muted);
    font-size: 10.5px;
    font-weight: 500;
    text-decoration: none;
  }
  .tabbar .tab svg { width: 22px; height: 22px; }
  .tabbar .tab.active { color: var(--primary); }
  /* The "More" tab is a <button>, so strip the default button chrome. */
  .tabbar button.tab { background: none; border: none; font-family: inherit; cursor: pointer; }

  /* Modal footers: one full-width button per line rather than a cramped row
     that half-fits. Keeps DOM order, so Cancel sits at the top and the
     primary action lands nearest the thumb. */
  .modal-actions > .btn { flex: 1 1 100%; justify-content: center; }

  /* Stack two-column layouts on mobile */
  .page-header { flex-direction: column; align-items: stretch; gap: 12px; }
  .dash-two-col { grid-template-columns: 1fr !important; }
  .field-row > .field,
  .field-row > .field-row { flex: 1 1 100%; }

  /* Keyboard-safe modals: on phones the on-screen keyboard shrinks the visible
     viewport but NOT a position:fixed inset:0 overlay, so a vertically-centred
     modal hides its inputs / Save button behind the keyboard. Instead: size the
     overlay to the *visual* viewport height (--app-vvh, kept in sync by app.js),
     top-align it, and let it scroll — so the browser can bring the focused
     field above the keyboard and every button stays reachable by scrolling. */
  .modal-overlay {
    align-items: flex-start;
    height: var(--app-vvh, 100dvh);
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    padding: 12px 0 max(12px, env(safe-area-inset-bottom));
  }
  .modal {
    width: 94vw;
    max-height: none;
    margin: auto 0;
  }
  table { font-size: 12.5px; }
  th, td { padding: 9px 8px; }
}

/* (The old 460px .stat-grid override is gone — auto-fit already collapses to
   a single column once the viewport can't fit two 190px tracks.) */

/* ---- Tables become stacked cards on phones ----
   app.js tags each <td> with data-label="<column header>" and marks the
   header-less action column .rt-actions. Here we drop the header row and lay
   each row out as a labelled card, so nothing needs a horizontal swipe. */
@media (max-width: 640px) {
  /* !important overrides the inline overflow-x:auto on finance/inventory
     cards — once tables stack there's no sideways overflow to contain, and a
     leftover scroll container would needlessly clip content vertically too. */
  .main .card { overflow-x: visible !important; }

  table.responsive thead {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
  }
  table.responsive,
  table.responsive tbody { display: block; width: 100%; }
  table.responsive tr {
    display: block;
    padding: 12px 16px;
    border-bottom: 1px solid var(--rule);
  }
  table.responsive tr:last-child { border-bottom: none; }
  table.responsive tr:hover td { background: transparent; }

  table.responsive td {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: baseline;
    gap: 14px;
    padding: 5px 0;
    border: none;
    text-align: right;
    white-space: normal;
    overflow-wrap: anywhere;
  }

  /* A stacked cell is a flex ROW, so any <div> sub-lines inside it (the
     procurement journey "→ packager 18 Jul 2026 · ZTO7912…", invoice/customer
     sub-labels, outstanding amounts) were being laid out BESIDE the badge
     rather than under it. Squeezed to a few px and combined with the
     `overflow-wrap: anywhere` above, the text shattered one letter per line.
     Forcing every direct-child div onto its own full-width line restores the
     desktop reading order: value on the first line, sub-lines beneath it. */
  table.responsive td > div {
    flex: 1 1 100%;
    min-width: 0;
    text-align: right;
  }
  table.responsive td.mono { white-space: normal; }
  table.responsive td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 40%;
    text-align: left;
    color: var(--muted);
    font-weight: 600;
    font-size: 11.5px;
    text-transform: uppercase;
    letter-spacing: 0.03em;
  }

  /* Action buttons: full-width-ish, two per row, comfortably tappable. */
  table.responsive td.rt-actions {
    justify-content: flex-start;
    flex-wrap: wrap;
    gap: 8px;
    padding-top: 12px;
    text-align: left;
  }
  table.responsive td.rt-actions .btn {
    flex: 1 1 auto;
    min-width: calc(50% - 8px);
    margin: 0;
    justify-content: center;
  }

  /* Empty-state row keeps its centred look. */
  table.responsive td.rt-fullrow { display: block; text-align: center; }
  table.responsive td.rt-fullrow::before { content: none; }
}

/* ---- Dashboard components ---- */
.dash-two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px; }
.finance-tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 20px; }
@media (max-width: 900px) { .finance-tiles { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .finance-tiles { grid-template-columns: 1fr; } }

.receivables-card {
  background: linear-gradient(135deg, #EAF2FE 0%, #E1F0FE 100%); border: 1px solid #D3E4FB; border-radius: var(--radius); padding: 20px 22px;
}
.receivables-card .label { font-size: 13px; color: var(--muted); margin-bottom: 6px; }
.receivables-card .value { font-family: 'Geist', sans-serif; font-size: 27px; font-weight: 800; color: var(--ink); }

.overdue-card {
  background: #FDEEEC; border: 1px solid #F5D2CE; border-radius: var(--radius); padding: 20px 22px;
  display: flex; flex-direction: column; justify-content: center; cursor: pointer;
  transition: transform 0.1s ease;
}
.overdue-card:hover { transform: translateY(-1px); }
.overdue-card .count { font-family: 'Geist', sans-serif; font-size: 27px; font-weight: 800; color: var(--red); }
.overdue-card .label { font-size: 13px; color: var(--muted); }
.overdue-card.zero { background: #EAF7F1; border-color: #CDEBDD; }
.overdue-card.zero .count { color: #1B8A57; }

.quick-create { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.quick-tile {
  background: var(--panel); border: 1px solid var(--rule); border-radius: var(--radius);
  padding: 16px 8px; text-align: center; cursor: pointer; text-decoration: none; color: var(--ink);
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease;
}
.quick-tile:hover { border-color: var(--primary); box-shadow: var(--shadow-sm); transform: translateY(-2px); }
.quick-tile svg { width: 26px; height: 26px; color: var(--primary); margin-bottom: 6px; }
.quick-tile .t-label { font-size: 12.5px; font-weight: 600; }

.section-title { display: flex; align-items: center; justify-content: space-between; margin: 8px 0 12px; }
.section-title h3 { font-size: 15.5px; }

.basis-toggle { display: inline-flex; background: #E9EEF7; border-radius: 100px; padding: 3px; }
.basis-toggle button {
  border: none; background: none; padding: 6px 16px; border-radius: 100px;
  font-size: 12.5px; font-weight: 600; cursor: pointer; color: var(--muted); font-family: inherit;
}
.basis-toggle button.active { background: #fff; color: var(--primary); box-shadow: 0 1px 3px rgba(11,18,32,0.12); }

.chart-wrap { width: 100%; height: 280px; }
/* The canvas's own box must be pinned by CSS (not left to its width/height
   attributes) — drawChart() in page-dashboard.js reads canvas.clientWidth/
   Height, multiplies by devicePixelRatio, and writes that back into the
   width/height attributes for sharpness. Without this rule those attributes
   ALSO define the canvas's on-screen size (nothing else does), so every
   redraw (every basis toggle click, every resize) re-reads its own already
   inflated size and grows it again — a runaway feedback loop, worst on any
   screen with devicePixelRatio > 1 (i.e. most phones and modern laptops).
   Pinning width/height to 100% here breaks that loop: clientWidth/Height
   always reflect .chart-wrap's fixed size, never the canvas's own buffer. */
.chart-wrap canvas { display: block; width: 100%; height: 100%; }

/* ---- Customers list (Zoho-style) ---- */
.filter-chips { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
.chip {
  border: 1px solid var(--rule); background: #fff; color: var(--ink);
  padding: 7px 16px; border-radius: 100px; font-size: 13px; font-weight: 600; cursor: pointer;
}
.chip.active { background: var(--primary-gradient); color: #fff; border-color: transparent; }
.chip-sort { margin-left: auto; }

.customer-list { background: var(--panel); border: 1px solid var(--rule); border-radius: var(--radius); overflow: hidden; }
.customer-row {
  display: flex; align-items: flex-start; gap: 14px; padding: 16px 18px;
  border-bottom: 1px solid var(--rule); cursor: pointer;
}
.customer-row:last-child { border-bottom: none; }
.customer-row:hover { background: var(--hover-surface); }

.avatar {
  width: 46px; height: 46px; border-radius: 50%; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-weight: 700; font-size: 15px; font-family: 'Geist', sans-serif;
}
.cust-body { flex: 1; min-width: 0; }
.cust-name { font-weight: 700; font-size: 15px; margin-bottom: 2px; }
.cust-sub { color: var(--muted); font-size: 13px; margin-bottom: 8px; }
.cust-figures { display: flex; gap: 40px; }
.cust-figures .fig-label { font-size: 12px; color: var(--muted); }
.cust-figures .fig-value { font-size: 14px; font-weight: 600; }
.cust-figures .fig-value.owed { color: var(--amber); }
.cust-figures .fig-value.overdue { color: var(--red); }

/* Sort sheet */
.sheet-overlay { position: fixed; inset: 0; background: rgba(7,16,32,0.55); z-index: 100; display: flex; align-items: flex-end; justify-content: center; }
.sheet {
  background: #fff; width: 100%; max-width: 520px; border-radius: 16px 16px 0 0; padding: 20px 20px 28px;
}
.sheet h3 { text-align: center; margin-bottom: 16px; }
.sort-opt {
  padding: 14px 12px; border-radius: 8px; cursor: pointer; font-size: 14px;
  display: flex; justify-content: space-between; align-items: center; border: 1px solid transparent;
}
.sort-opt:hover { background: var(--hover-surface); }
.sort-opt.active { border-color: var(--primary); color: var(--primary); font-weight: 600; }

/* ---- Mobile "More" sheet ---- */
.more-sheet {
  max-height: 82vh;
  overflow-y: auto;
  padding-bottom: calc(24px + env(safe-area-inset-bottom, 0px));
}
.more-grabber { width: 40px; height: 4px; border-radius: 100px; background: var(--rule); margin: 0 auto 16px; }
.more-user { text-align: center; margin-bottom: 16px; }
.more-user-name { font-family: 'Geist', sans-serif; font-weight: 700; font-size: 16px; margin-bottom: 4px; }
.more-list { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px; }
.more-item {
  display: flex; align-items: center; gap: 11px;
  padding: 14px; min-height: 52px;
  border: 1px solid var(--rule); border-radius: 10px;
  background: var(--panel); color: var(--ink);
  font-size: 14px; font-weight: 600; font-family: inherit;
  text-decoration: none; cursor: pointer; width: 100%; text-align: left;
}
.more-item + .more-item { margin-top: 8px; }
.more-list .more-item + .more-item { margin-top: 0; }
.more-item svg { width: 20px; height: 20px; flex-shrink: 0; color: var(--primary); }
.more-item:hover { background: var(--hover-surface); }
.more-item.active { border-color: var(--primary); color: var(--primary); background: var(--hover-surface); }
.more-item.danger { color: var(--red); }
.more-item.danger svg { color: var(--red); }

.fab {
  position: fixed; right: 24px; bottom: 24px; width: 56px; height: 56px; border-radius: 50%;
  background: var(--primary-gradient); color: #fff; border: none; font-size: 28px; cursor: pointer; z-index: 40;
  box-shadow: 0 6px 18px rgba(11,95,191,0.4); display: none;
}
@media (max-width: 820px) {
  .fab { display: block; bottom: 84px; }
  .cust-figures { gap: 28px; }
}

/* ---- Inventory thumbnails & profit ---- */
.item-thumb {
  width: 44px; height: 44px; border-radius: 8px; object-fit: cover;
  background: #EEF3FC; border: 1px solid var(--rule); flex-shrink: 0;
}
.item-thumb-placeholder {
  width: 44px; height: 44px; border-radius: 8px; background: #EEF3FC; border: 1px solid var(--rule);
  display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 18px; flex-shrink: 0;
}
.profit-pos { color: var(--primary); }
.profit-neg { color: var(--red); }
.img-drop {
  border: 1.5px dashed var(--rule); border-radius: 8px; padding: 14px; text-align: center;
  color: var(--muted); font-size: 13px; cursor: pointer;
}
.img-drop:hover { border-color: var(--primary); }
.img-drop.drag-over { border-color: var(--primary); background: #EAF2FE; color: var(--primary); }
.img-preview { max-width: 100%; max-height: 140px; border-radius: 8px; display: block; }
.img-preview-wrap { position: relative; display: inline-block; margin-top: 8px; }
.img-remove-btn {
  position: absolute; top: -8px; right: -8px; width: 24px; height: 24px; border-radius: 50%;
  background: var(--red); color: #fff; border: 2px solid #fff; cursor: pointer;
  font-size: 16px; line-height: 1; display: flex; align-items: center; justify-content: center;
  padding: 0; box-shadow: 0 1px 3px rgba(0,0,0,.25);
}
.img-remove-btn:hover { background: #C0392B; }

/* ---- Password strength meter ---- */
.pw-meter-bar { height: 6px; border-radius: 100px; background: var(--rule); overflow: hidden; }
.pw-meter-fill { height: 100%; border-radius: 100px; transition: width 0.15s ease, background 0.15s ease; }
.pw-meter-fill.weak { background: var(--red); }
.pw-meter-fill.okay { background: var(--amber); }
.pw-meter-fill.strong { background: var(--primary); }
.pw-meter-label { font-size: 12px; font-weight: 700; margin-top: 5px; }
.pw-meter-label.weak { color: var(--red); }
.pw-meter-label.okay { color: var(--amber); }
.pw-meter-label.strong { color: var(--primary); }
.pw-rules { list-style: none; padding: 0; margin: 8px 0 0; font-size: 12px; color: var(--muted); }
.pw-rules li { padding: 2px 0; }
.pw-rules li.ok { color: var(--primary); font-weight: 600; }

/* ---- Permission checklist ---- */
.perm-checklist { border: 1px solid var(--rule); border-radius: 8px; padding: 12px 14px; margin-top: 4px; max-height: 220px; overflow-y: auto; }

/* ---- App footer ---- */
.app-footer {
  margin-top: 40px; padding-top: 16px; border-top: 1px solid var(--rule);
  font-size: 12px; color: var(--muted); text-align: center;
}
.app-footer a { color: var(--muted); text-decoration: underline; }
.app-footer a:hover { color: var(--primary); }
@media (max-width: 820px) {
  .app-footer { margin-bottom: 70px; }
}

/* ============================ Row actions menu ============================
   Dropdown behind each table row's "Actions ▾" button (showActionsMenu in
   app.js). Fixed-position and body-appended so the table card's overflow
   scrolling never clips it. */
.actions-menu {
  position: fixed; z-index: 250; min-width: 176px;
  background: var(--panel); border: 1px solid var(--rule);
  border-radius: 10px; box-shadow: var(--shadow-md); padding: 5px;
  display: flex; flex-direction: column;
}
.actions-menu-item {
  display: block; width: 100%; text-align: left; background: none; border: none;
  padding: 8px 12px; border-radius: 7px; font: inherit; font-size: 13px;
  font-weight: 600; color: var(--ink); cursor: pointer; text-decoration: none;
}
.actions-menu-item:hover { background: var(--hover-surface); }
.actions-menu-item.primary { color: var(--primary); }
.actions-menu-item.danger { color: var(--red); }
.actions-menu-divider { height: 1px; background: var(--rule); margin: 4px 6px; }

/* ============================================================
   2026 polish pass — view transitions, skeletons, command
   palette, typography & micro-interaction refinements
   ============================================================ */

/* ---- Smooth cross-fade between pages (View Transitions API, MPA mode).
   Browsers without support simply ignore this — no fallback needed. ---- */
@view-transition { navigation: auto; }
::view-transition-old(root) { animation-duration: 0.14s; }
::view-transition-new(root) { animation-duration: 0.18s; }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
}

/* ---- Typography refinements ---- */
h1, h2, h3 { text-wrap: balance; }
.page-header h1 { letter-spacing: -0.02em; }
td.mono, .stat-card .value, .receivables-card .value { font-variant-numeric: tabular-nums; }

/* ---- Micro-interactions ---- */
.btn { transition: transform 0.12s ease, box-shadow 0.15s ease, background 0.15s ease, border-color 0.15s ease; }
.btn:active:not(:disabled) { transform: translateY(0) scale(0.97); }
.btn:focus-visible, .nav-link:focus-visible, .tab:focus-visible, .more-item:focus-visible,
a:focus-visible, .chip:focus-visible {
  outline: 2px solid var(--primary-light);
  outline-offset: 2px;
  border-radius: 6px;
}
::selection { background: rgba(11, 95, 191, 0.18); }
html[data-theme="dark"] ::selection { background: rgba(94, 163, 245, 0.35); }

/* Slim, theme-aware scrollbars */
* { scrollbar-width: thin; scrollbar-color: var(--rule) transparent; }
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-thumb { background: var(--rule); border-radius: 8px; }
*::-webkit-scrollbar-thumb:hover { background: var(--muted); }
*::-webkit-scrollbar-track { background: transparent; }

/* ---- Skeleton loaders (shown in list tables while data loads) ---- */
.skel {
  position: relative; overflow: hidden;
  background: var(--rule); border-radius: 6px;
  height: 13px; width: 70%;
}
.skel.w40 { width: 40%; } .skel.w55 { width: 55%; } .skel.w85 { width: 85%; }
.skel::after {
  content: ''; position: absolute; inset: 0; transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.6), transparent);
  animation: skel-shimmer 1.3s infinite;
}
html[data-theme="dark"] .skel::after {
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.09), transparent);
}
@keyframes skel-shimmer { 100% { transform: translateX(100%); } }
tr.skel-row td { border-bottom: 1px solid var(--rule); }
@media (prefers-reduced-motion: reduce) { .skel::after { animation: none; } }

/* ---- Command palette (Ctrl/Cmd + K) ---- */
.cp-overlay {
  position: fixed; inset: 0; z-index: 400;
  background: rgba(11, 18, 32, 0.45);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  display: flex; justify-content: center; align-items: flex-start;
  padding: 12vh 16px 16px;
  opacity: 0; pointer-events: none; transition: opacity 0.15s ease;
}
.cp-overlay.open { opacity: 1; pointer-events: auto; }
.cp-panel {
  width: 100%; max-width: 560px;
  background: var(--panel); border: 1px solid var(--rule);
  border-radius: 14px; box-shadow: var(--shadow-md);
  overflow: hidden; display: flex; flex-direction: column;
  transform: translateY(-8px) scale(0.98); transition: transform 0.15s ease;
}
.cp-overlay.open .cp-panel { transform: none; }
.cp-input-row { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--rule); }
.cp-input-row svg { width: 18px; height: 18px; stroke: var(--muted); flex-shrink: 0; }
.cp-input {
  flex: 1; border: none !important; outline: none !important; background: transparent !important;
  font-family: 'Geist', sans-serif; font-size: 15px; color: var(--ink);
  padding: 0 !important; box-shadow: none !important;
}
.cp-results { max-height: 46vh; overflow-y: auto; padding: 6px; }
.cp-group { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); padding: 10px 12px 4px; }
.cp-item {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 12px; border-radius: 9px; cursor: pointer;
  font-size: 13.5px; color: var(--ink);
}
.cp-item svg { width: 16px; height: 16px; stroke: var(--muted); flex-shrink: 0; }
.cp-item .cp-sub { color: var(--muted); font-size: 12px; margin-left: auto; padding-left: 12px; white-space: nowrap; }
.cp-item.sel { background: var(--hover-surface); box-shadow: inset 0 0 0 1px var(--rule); }
.cp-item.sel svg { stroke: var(--primary); }
.cp-empty { padding: 22px 12px; text-align: center; color: var(--muted); font-size: 13px; }
.cp-foot {
  display: flex; gap: 14px; padding: 9px 16px; border-top: 1px solid var(--rule);
  font-size: 11.5px; color: var(--muted);
}
.cp-foot kbd, .kbd {
  font-family: 'Geist Mono', monospace; font-size: 10.5px;
  background: var(--paper); border: 1px solid var(--rule); border-bottom-width: 2px;
  border-radius: 5px; padding: 1px 5px; color: var(--muted);
}
.sidebar-search-btn {
  display: flex; align-items: center; gap: 9px;
  margin: 0 14px 10px; padding: 8px 12px;
  background: rgba(255, 255, 255, 0.07); border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 9px; cursor: pointer; width: calc(100% - 28px);
  color: #B9C6DE; font-family: inherit; font-size: 12.5px; text-align: left;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.sidebar-search-btn:hover { background: rgba(255, 255, 255, 0.12); border-color: rgba(255, 255, 255, 0.25); }
.sidebar-search-btn svg { width: 15px; height: 15px; stroke: currentColor; }
.sidebar-search-btn .kbd { margin-left: auto; background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.18); color: #B9C6DE; }

/* ---- Aesthetic pass 2: entrance animations & animated numbers ---- */
/* Modals scale-fade in; the overlay fades with them. */
.modal-overlay { animation: overlay-in 0.16s ease; }
.modal { animation: modal-in 0.2s cubic-bezier(0.16, 1, 0.3, 1); }
@keyframes overlay-in { from { opacity: 0; } }
@keyframes modal-in {
  from { opacity: 0; transform: translateY(10px) scale(0.97); }
}

/* Toasts slide up on arrival and fade out just before removal (JS removes
   at 3200ms; the exit animation starts at 2850ms and holds via forwards). */
.toast { animation: toast-in 0.25s cubic-bezier(0.16, 1, 0.3, 1), toast-out 0.3s ease 2.85s forwards; }
@keyframes toast-in { from { opacity: 0; transform: translateY(14px) scale(0.97); } }
@keyframes toast-out { to { opacity: 0; transform: translateY(6px); } }

/* Login card floats up on load */
.login-card { animation: modal-in 0.35s cubic-bezier(0.16, 1, 0.3, 1); }

/* Mobile "More" sheet slides up */
.more-sheet { animation: sheet-in 0.22s cubic-bezier(0.16, 1, 0.3, 1); }
@keyframes sheet-in { from { transform: translateY(24px); opacity: 0.6; } }

@media (prefers-reduced-motion: reduce) {
  .modal-overlay, .modal, .login-card, .more-sheet { animation: none; }
  .toast { animation: toast-out 0.3s ease 2.85s forwards; }
}


/* Dark theme: swap the full-color logo (dark text) for the knockout version
   on the standalone pages (login, reset, review, about). */
html[data-theme="dark"] .brand-logo { content: url('/img/logo-white.png'); }


/* ============================================================
   Notification bell + inbox panel  (public/js/app.js)
   Injected into <body> by initNotificationBell() so it appears on every
   page without editing each HTML file.
   ============================================================ */
.bell-wrap { position: fixed; top: 14px; right: 16px; z-index: 90; }

.bell-btn {
  position: relative; width: 42px; height: 42px; border-radius: 50%;
  border: 1px solid var(--rule); background: var(--panel); color: var(--ink);
  display: grid; place-items: center; cursor: pointer; box-shadow: var(--shadow-sm);
  transition: background 0.15s ease, box-shadow 0.15s ease;
}
.bell-btn:hover { background: var(--hover-surface); }
.bell-btn svg { width: 20px; height: 20px; }
/* An approval queue is someone else blocked on you — worth a different
   colour from ordinary unread items. */
.bell-btn.has-approvals { border-color: var(--amber); color: var(--amber); }

.bell-badge {
  position: absolute; top: -4px; right: -4px; min-width: 18px; height: 18px;
  padding: 0 5px; border-radius: 9px; background: var(--red); color: #fff;
  font-size: 11px; font-weight: 700; line-height: 18px; text-align: center;
}
/* Any element shown/hidden via the `hidden` attribute must pair its display
   rule with a [hidden] override — an unconditional `display` beats the
   browser's default [hidden]{display:none} on a specificity tie, and the
   element would then never hide. (Same bug that broke the time picker.) */
.bell-badge[hidden] { display: none; }

.bell-panel {
  display: flex; flex-direction: column;
  position: absolute; top: 50px; right: 0; width: 340px; max-width: calc(100vw - 32px);
  max-height: min(70vh, 520px);
  background: var(--panel); border: 1px solid var(--rule); border-radius: var(--radius);
  box-shadow: var(--shadow-md); overflow: hidden;
}
.bell-panel[hidden] { display: none; }

.bell-head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 12px 14px; border-bottom: 1px solid var(--rule);
}
.bell-list { overflow-y: auto; flex: 1; }
.bell-empty { padding: 22px 14px; text-align: center; }

.bell-item { padding: 11px 14px; border-bottom: 1px solid var(--rule); cursor: pointer; }
.bell-item:last-child { border-bottom: none; }
.bell-item:hover { background: var(--hover-surface); }
/* Unread marked with a left rule rather than a background tint, so it still
   reads clearly in dark mode where a tint would be nearly invisible. */
.bell-item.unread { border-left: 3px solid var(--primary); padding-left: 11px; }
.bell-item-title { font-weight: 600; font-size: 13px; }
.bell-item-body { font-size: 12.5px; color: var(--muted); margin-top: 2px; }
.bell-item-time { font-size: 11px; margin-top: 3px; }

.bell-foot {
  padding: 10px 14px; text-align: center; font-size: 13px; font-weight: 600;
  color: var(--primary); text-decoration: none; border-top: 1px solid var(--rule);
}
.bell-foot:hover { background: var(--hover-surface); }

@media (max-width: 640px) {
  /* Clear of the page header on phones, and never wider than the viewport. */
  .bell-wrap { top: 10px; right: 10px; }
  .bell-panel { width: calc(100vw - 20px); }
}


/* ============================================================
   Technician portal  (public/technician.html)
   Mobile-first: this is used one-handed, outdoors, on a phone.
   ============================================================ */
.tech-section { margin-bottom: 22px; }
.tech-section h3 { margin: 0 0 8px; font-size: 15px; }

.tech-job {
  background: var(--panel); border: 1px solid var(--rule); border-radius: var(--radius);
  padding: 14px; margin-bottom: 10px; box-shadow: var(--shadow-sm);
}
.tech-job.is-today { border-left: 4px solid var(--primary); }
.tech-job.is-overdue { border-left: 4px solid var(--red); }
.tech-job.is-submitted { border-left: 4px solid var(--amber); }

.tech-job-top { display: flex; justify-content: space-between; gap: 10px; align-items: flex-start; }
.tech-job-num { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--muted); }
.tech-job-cust { font-weight: 700; font-size: 16px; margin-top: 2px; }
.tech-job-when { font-size: 14px; font-weight: 600; color: var(--primary); margin-top: 6px; }
.tech-job-meta { font-size: 13px; color: var(--muted); margin-top: 4px; }
.tech-job-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
/* Field buttons are deliberately large — these are tapped with one thumb,
   often in bright sunlight, sometimes wearing gloves. */
.tech-job-actions .btn { min-height: 42px; flex: 1 1 auto; }

.tech-callout {
  border-radius: var(--radius); padding: 12px 14px; margin-bottom: 14px; font-size: 13.5px;
  border: 1px solid var(--amber); background: color-mix(in srgb, var(--amber) 10%, transparent);
}

.photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: 8px; margin-top: 10px; }
.photo-thumb { position: relative; aspect-ratio: 1; border-radius: 8px; overflow: hidden; border: 1px solid var(--rule); }
.photo-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photo-thumb button {
  position: absolute; top: 3px; right: 3px; width: 22px; height: 22px; border-radius: 50%;
  border: none; background: rgba(0,0,0,0.62); color: #fff; font-size: 14px; line-height: 1; cursor: pointer;
}
.photo-count { font-size: 12px; color: var(--muted); margin-top: 6px; }


/* ============================================================
   Messages  (public/messages.html)
   ============================================================ */
.msg-layout { display: grid; grid-template-columns: 260px 1fr; gap: 16px; align-items: start; }
.msg-threads { max-height: 70vh; overflow-y: auto; }
.msg-thread-item {
  padding: 11px 12px; border-bottom: 1px solid var(--rule); cursor: pointer; display: block; width: 100%;
  text-align: left; background: none; border-left: none; border-right: none; border-top: none; color: inherit; font: inherit;
}
.msg-thread-item:hover { background: var(--hover-surface); }
.msg-thread-item.active { background: var(--hover-surface); font-weight: 600; }
.msg-thread-name { display: flex; justify-content: space-between; gap: 8px; align-items: center; }
.msg-thread-snippet { font-size: 12px; color: var(--muted); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-unread-dot { min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; background: var(--primary); color: #fff; font-size: 11px; line-height: 18px; text-align: center; }

.msg-pane { display: flex; flex-direction: column; min-height: 420px; max-height: 70vh; }
.msg-scroll { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; }
.msg-bubble { max-width: 78%; padding: 9px 12px; border-radius: 14px; font-size: 14px; line-height: 1.4; white-space: pre-wrap; word-break: break-word; }
.msg-bubble.them { align-self: flex-start; background: var(--hover-surface); border: 1px solid var(--rule); border-bottom-left-radius: 4px; }
.msg-bubble.me { align-self: flex-end; background: var(--primary); color: #fff; border-bottom-right-radius: 4px; }
.msg-bubble .msg-job { display: block; font-size: 11px; opacity: 0.8; margin-bottom: 3px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.msg-bubble .msg-time { display: block; font-size: 10.5px; opacity: 0.7; margin-top: 3px; }
.msg-composer { display: flex; gap: 8px; padding: 10px; border-top: 1px solid var(--rule); }
.msg-composer textarea { flex: 1; resize: none; min-height: 44px; max-height: 120px; }

@media (max-width: 760px) {
  .msg-layout { grid-template-columns: 1fr; }
  /* One pane at a time on a phone: the list, or the open conversation. */
  .msg-layout.viewing-thread .msg-threads-card { display: none; }
  .msg-layout:not(.viewing-thread) .msg-pane-card { display: none; }
  .msg-pane { max-height: none; min-height: 60vh; }
}
