/* AccountsERP — glassmorphic violet design system (Phase 3 redesign).
   Same custom-property NAMES as the original token layer throughout —
   only their values changed — so every class rule and every inline
   `style="...var(--x)..."` across the app picks up the new look without
   any usage site needing to be touched. New variables (marked below)
   were added for things the old system didn't have: the page-background
   gradient, glass blur, and a properly-defined --accent-100 (previously
   referenced in ~12 files but never defined, so those always silently
   rendered their hardcoded fallback instead). */

:root {
  --surface-0: #FFFFFF; --surface-50: #F5F3FC; --surface-100: #EFEBFA; --surface-200: rgba(108,92,231,.16);
  /* §dashboard-a11y-contrast — the previous #948DA8 measured 3.1-3.2:1
     against --surface-0/#fbfcff (confirmed live via the Impeccable
     detector overlay on the dashboard), well under WCAG AA's 4.5:1 for
     normal text. #736E83 keeps the same violet-gray hue family as
     ink-700/ink-900 while clearing 4.5:1+ against every background this
     token is actually used on. This is a :root token, so it updates
     every secondary-text consumer app-wide (stat labels, card headings,
     row meta text, tile captions), not just the dashboard — the same
     fix everywhere it was already the same defect. */
  --ink-900: #181428; --ink-700: #5B5470; --ink-500: #736E83;
  --brand-700: #4B3FC9; --brand-600: #6C5CE7; --brand-100: #EFECFF;
  --danger-600: #D8394B; --danger-100: #FDEAEC;
  --success-600: #12925B; --success-100: #E7F9EF;
  --warning-600: #B4740E; --warning-100: #FCF0DA;
  --radius-sm: 10px; --radius-md: 14px; --radius-lg: 22px;
  --shadow-md: 0 16px 40px -14px rgba(108,92,231,.24), 0 6px 16px -8px rgba(108,92,231,.12);
  --font-display: -apple-system, "Segoe UI Semibold", "Segoe UI", sans-serif;
  --font-body: -apple-system, "Segoe UI", sans-serif;

  /* New in Phase 3 */
  --accent-100: #EFECFF;
  --shadow-shell: 0 30px 70px -24px rgba(90,70,190,.28), 0 10px 26px -12px rgba(90,70,190,.14);
  --bg-1: #EEF1FF; --bg-2: #F6EEFC; --bg-3: #EAF4FF;
  --blur: 20px;
}

/* §mobile-theme-preference — a real request: light/dark/auto on Menu →
   Profile, mobile only for now — deliberately scoped to body.m-body's
   own subtree (every mobile page's <body> class), never :root, so a
   desktop user whose OS happens to be in dark mode never sees any of
   this: app.css's desktop pages have no .m-body ancestor at all, so
   neither rule below can ever match there regardless of system theme
   or this user's own theme_preference. 'auto' (the default — see the
   theme_preference migration) sets no data-theme attribute at all, so
   only the @media block below applies, live, straight from the
   device's own current prefers-color-scheme; an explicit 'light'
   choice is excluded from that block by the :not() guard (stays light
   even under a dark system setting), and an explicit 'dark' choice
   applies unconditionally via the plain [data-theme="dark"] rule
   beneath it, regardless of the system setting either way. Same
   token NAMES as :root above — every existing class/inline
   var(--x) usage across mobile.css picks this up for free, no
   component-level dark-mode work needed. */
@media (prefers-color-scheme: dark) {
  html:not([data-theme="light"]) body.m-body {
    --surface-0: #211D33; --surface-50: #15121F; --surface-100: #2B2645; --surface-200: rgba(255,255,255,.14);
    --ink-900: #F1EEFA; --ink-700: #B7B0CC; --ink-500: #8A8299;
    --brand-700: #AC9BFF; --brand-600: #8D7CF0; --brand-100: #2E2856;
    --danger-600: #FF7A8A; --danger-100: #3D2029;
    --success-600: #3EDDA0; --success-100: #163826;
    --warning-600: #F5BD5C; --warning-100: #3D2F12;
  }
}
html[data-theme="dark"] body.m-body {
  --surface-0: #211D33; --surface-50: #15121F; --surface-100: #2B2645; --surface-200: rgba(255,255,255,.14);
  --ink-900: #F1EEFA; --ink-700: #B7B0CC; --ink-500: #8A8299;
  --brand-700: #AC9BFF; --brand-600: #8D7CF0; --brand-100: #2E2856;
  --danger-600: #FF7A8A; --danger-100: #3D2029;
  --success-600: #3EDDA0; --success-100: #163826;
  --warning-600: #F5BD5C; --warning-100: #3D2F12;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--font-body);
  color: var(--ink-900);
  background:
    radial-gradient(1200px 700px at 15% 10%, var(--bg-1), transparent 60%),
    radial-gradient(1000px 700px at 85% 20%, var(--bg-2), transparent 55%),
    radial-gradient(1200px 800px at 50% 100%, var(--bg-3), transparent 60%),
    var(--bg-1);
}

/* §dialog-double-scrollbar — showModal() puts a <dialog> in the top
   layer and blocks interaction with the rest of the page, but it does
   NOT stop the page behind it from still being scrollable — the
   background page's own scrollbar stayed visible and usable the whole
   time a dialog was open, alongside the dialog's own .modal-body
   scrollbar, showing as two separate scrollbars at once (worst on
   Quick Invoice, where the background Invoices list is a long table).
   :has() lets one rule cover every dialog.modal in the app — opened
   from dozens of different onclick handlers across many files — without
   adding open/close bookkeeping to each of them. */
body:has(dialog[open]) { overflow: hidden; }

.auth-shell {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.auth-card {
  width: 100%;
  max-width: 380px;
  background: var(--surface-0);
  border: 1px solid var(--surface-200);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-shell);
  padding: 32px 28px;
}

.auth-brand {
  text-align: center;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 16px;
  margin-bottom: 22px;
}
.auth-brand .dot { display:inline-block; width:9px; height:9px; border-radius:50%; background:var(--brand-600); margin-right:7px; }

.field { margin-bottom: 14px; display: flex; flex-direction: column; gap: 5px; }
.field label { font-size: 12px; font-weight: 700; color: var(--ink-700); }
.field input, .field select {
  border: 1px solid var(--surface-200);
  border-radius: var(--radius-sm);
  padding: 9px 11px;
  font-size: 14px;
  font-family: inherit;
  color: var(--ink-900);
  background: var(--surface-0);
}
.field input:focus, .field select:focus { outline: 2px solid var(--brand-600); outline-offset: 1px; }

.btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 100%; padding: 10px 14px; border-radius: var(--radius-md);
  border: 1px solid var(--brand-600); background: var(--brand-600); color: #fff;
  font-weight: 700; font-size: 14px; font-family: inherit; cursor: pointer;
}
.btn:hover { background: var(--brand-700); border-color: var(--brand-700); }
.btn.secondary { background: var(--surface-0); color: var(--ink-900); border-color: var(--surface-200); }

.auth-links { text-align: center; font-size: 12.5px; margin-top: 16px; display:flex; flex-direction:column; gap:8px; }
.auth-links a { color: var(--brand-700); font-weight: 600; text-decoration: none; }
.auth-links a:hover { text-decoration: underline; }

.error-banner {
  background: var(--danger-100); color: var(--danger-600); border-radius: var(--radius-sm);
  padding: 9px 12px; font-size: 12.5px; margin-bottom: 14px; line-height: 1.5;
}
.status-banner {
  background: var(--success-100); color: var(--success-600); border-radius: var(--radius-sm);
  padding: 9px 12px; font-size: 12.5px; margin-bottom: 14px; line-height: 1.5;
}

/* Global flash-message toast (layouts/app.blade.php) — replaces the old
   always-inline status/error banner, which pages then duplicated with
   their own copy. Fixed to the viewport so it never fights page layout,
   auto-dismisses after data-toast-timeout ms (default 15s) or on click. */
.toast-stack {
  position: fixed; top: 20px; right: 20px; z-index: 1000;
  display: flex; flex-direction: column; gap: 10px; max-width: 360px;
}
.toast {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 12px 14px; border-radius: var(--radius-md); font-size: 13px; line-height: 1.5;
  box-shadow: var(--shadow-md); animation: toast-in .18s ease-out;
}
.toast-success { background: var(--success-100); color: var(--success-600); }
.toast-error { background: var(--danger-100); color: var(--danger-600); }
.toast span { flex: 1; }
.toast-close {
  background: none; border: none; cursor: pointer; font-size: 16px; line-height: 1;
  color: inherit; opacity: .6; padding: 0 2px;
}
.toast-close:hover { opacity: 1; }
.toast-hide { animation: toast-out .2s ease-in forwards; }
@keyframes toast-in { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: translateX(0); } }
@keyframes toast-out { from { opacity: 1; } to { opacity: 0; transform: translateX(16px); } }

.otp-input { display:flex; gap:8px; justify-content:center; }
.otp-input input {
  width: 40px; height: 46px; text-align: center; font-size: 20px; font-weight:700;
  border: 1px solid var(--surface-200); border-radius: var(--radius-sm);
}

.method-tabs { display:flex; gap:6px; margin-bottom:16px; justify-content:center; }
.method-tabs button {
  font-size: 11.5px; font-weight: 700; padding: 6px 12px; border-radius: 999px;
  border: 1px solid var(--surface-200); background: var(--surface-0); color: var(--ink-700); cursor:pointer;
}
.method-tabs button.active { background: var(--brand-600); border-color: var(--brand-600); color: #fff; }

.company-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(160px,1fr)); gap:12px; }
.company-card {
  border: 1px solid var(--surface-200); border-radius: var(--radius-md); padding: 16px;
  text-align:center; background: var(--surface-0); text-decoration:none; color:var(--ink-900); cursor:pointer;
}
.company-card:hover { border-color: var(--brand-600); }
.company-card.create { border-style: dashed; color: var(--ink-500); }

.recovery-codes { display:grid; grid-template-columns: 1fr 1fr; gap:8px; font-family: ui-monospace, monospace; font-size:13px; background:var(--surface-100); border-radius:var(--radius-sm); padding:14px; margin-bottom:16px; }

.qr-box { text-align:center; margin-bottom: 16px; }
.qr-box img { border: 1px solid var(--surface-200); border-radius: var(--radius-sm); }
.secret-key { font-family: ui-monospace, monospace; font-size: 12px; text-align:center; color: var(--ink-500); margin-bottom:16px; word-break:break-all; }

/* Page-local content width/heading styles — now rendered inside
   .app-content (the sidebar app shell below, Component-Library §17). */
.page-shell { max-width: 760px; margin: 0 auto; padding: 40px 24px 80px; }
.page-shell h1 { font-family: var(--font-display); font-size: 22px; margin: 0 0 4px; }
.page-shell .sub { color: var(--ink-500); font-size: 13px; margin: 0 0 28px; }

/* Breadcrumb-style back link, e.g. "← Customers" atop a form/show page —
   used across ~45 views but had no rule at all before Phase 3. */
.topnav { display:flex; align-items:center; gap:12px; margin-bottom:14px; font-size:12.5px; }
.topnav a { color: var(--ink-500); text-decoration:none; font-weight:600; }
.topnav a:hover { color: var(--brand-700); }

/* Sidebar app shell (Component-Library §17, Sitemap.md §4/§5) — a
   floating glass card, per the approved design, not edge-to-edge. */
.app-shell {
  display: flex;
  min-height: calc(100vh - 40px);
  max-width: 1400px;
  margin: 20px auto;
  background: rgba(255,255,255,.78);
  backdrop-filter: blur(var(--blur));
  -webkit-backdrop-filter: blur(var(--blur));
  border: 1px solid var(--surface-200);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-shell);
  overflow: hidden;
}

.app-sidebar {
  width: 240px; flex-shrink: 0; background: rgba(108,92,231,.05);
  border-right: 1px solid var(--surface-200); padding: 20px 14px;
  overflow-y: auto;
}
.app-brand {
  font-family: var(--font-display); font-weight: 800; font-size: 15px;
  margin-bottom: 20px; padding: 0 8px; display: flex; align-items: center;
}
.app-brand .dot {
  display:inline-flex; align-items:center; justify-content:center; width:26px; height:26px;
  border-radius: 8px; background:var(--brand-600); margin-right:9px; color:#fff; font-size:12px; font-weight:800;
}

.nav-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.nav-single, .nav-group summary, .nav-group a {
  display: flex; align-items:center; gap:8px; padding: 8px 10px; border-radius: var(--radius-sm);
  color: var(--ink-700); text-decoration: none; font-size: 13.5px;
  cursor: pointer;
}
.nav-single svg, .nav-group summary svg { width:15px; height:15px; flex-shrink:0; color: var(--ink-500); }
.nav-group summary { font-weight: 700; list-style: none; }
.nav-group summary::-webkit-details-marker { display: none; }
.nav-group summary::after { content: '▸'; margin-left:auto; font-size:10px; color: var(--ink-500); }
.nav-group[open] summary::after { content: '▾'; }
.nav-group ul { list-style: none; margin: 2px 0 4px; padding: 0 0 0 20px; display: flex; flex-direction: column; gap: 1px; }
.nav-group ul a { font-size: 13px; padding: 6px 10px; }
.nav-single:hover, .nav-group ul a:hover { background: var(--surface-100); }
.nav-single.active, .nav-group ul a.active {
  background: var(--brand-600); color: #fff; font-weight: 700;
}
.nav-single.active svg { color: #fff; }

.app-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.app-topbar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 28px; border-bottom: 1px solid var(--surface-200);
  background: rgba(255,255,255,.5); gap: 16px;
}
.app-topbar-title { font-family: var(--font-display); font-weight: 700; font-size: 15px; }
.app-topbar-right { display: flex; align-items: center; gap: 14px; font-size: 13px; }
.app-topbar-company { color: var(--ink-500); }

.user-menu { position: relative; }
.user-menu summary { list-style: none; cursor: pointer; font-weight: 700; color: var(--ink-900); }
.user-menu summary::-webkit-details-marker { display: none; }
/* .user-menu-portal is added by action-menu.js — same box, but once the
   panel is moved to <body> on open (see that file for why), `position`
   is overridden inline to `fixed` with JS-computed coordinates, which
   beats this rule's `absolute` regardless of source order/specificity.
   The plain `.user-menu ul` selector stays as the no-JS fallback. */
.user-menu ul, .user-menu-portal {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 20;
  list-style: none; margin: 0; padding: 6px; min-width: 180px;
  background: var(--surface-0); border: 1px solid var(--surface-200);
  border-radius: var(--radius-md); box-shadow: var(--shadow-md);
  display: flex; flex-direction: column; gap: 1px;
}
.user-menu-portal { z-index: 500; }
.user-menu ul li a, .user-menu ul li .link-button,
.user-menu-portal li a, .user-menu-portal li .link-button {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; padding: 7px 9px;
  border-radius: var(--radius-sm); color: var(--ink-900); text-decoration: none;
  font-size: 13px; border: none; background: none; cursor: pointer; font-family: inherit;
}
.user-menu ul li a svg, .user-menu ul li .link-button svg,
.user-menu-portal li a svg, .user-menu-portal li .link-button svg { width: 15px; height: 15px; flex-shrink: 0; color: var(--ink-500); }
.user-menu ul li a:hover, .user-menu ul li .link-button:hover,
.user-menu-portal li a:hover, .user-menu-portal li .link-button:hover { background: var(--surface-100); }
.user-menu ul li .link-button.active, .user-menu-portal li .link-button.active { color: var(--brand-700); font-weight: 700; }

.app-content { flex: 1; padding: 0; }
.app-content .status-banner, .app-content .error-banner { margin: 20px 28px 0; }

/* Tile grid — Settings Home (S-T01) and Dashboard quick links share this */
.tile-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap:12px; }
.tile { display:block; background:var(--surface-0); border:1px solid var(--surface-200); border-radius:var(--radius-lg);
  padding:16px; text-decoration:none; color:var(--ink-900); box-shadow: var(--shadow-md); }
.tile:hover { border-color:var(--brand-600); }
.tile b { display:block; font-family:var(--font-display); font-size:13.5px; margin-bottom:3px; }
.tile span { font-size:11.5px; color:var(--ink-500); }
.tile-cat { font-size:11px; text-transform:uppercase; letter-spacing:.05em; color:var(--brand-700); font-weight:700; margin:22px 0 8px; }

.card-block { background: var(--surface-0); border: 1px solid var(--surface-200); border-radius: var(--radius-lg); padding: 24px; margin-bottom: 20px; }
.card-block h2 { font-family: var(--font-display); font-size: 15px; margin: 0 0 16px; }

/* AI Invoice Inbox — chat composer (§ai-async-invoice): a single
   always-visible send bar, no persisted message history. */
.ai-composer { padding: 14px 16px; border: 2px dashed transparent; border-radius: var(--radius-md); transition: border-color .1s, background .1s; }
.ai-composer.is-dragover { border-color: var(--brand-600); background: var(--brand-100); }
.ai-composer-row { display: flex; align-items: flex-end; gap: 8px; }
.ai-composer-row textarea {
  flex: 1; border: 1px solid var(--surface-200); border-radius: var(--radius-sm); padding: 10px 12px;
  font-size: 13.5px; font-family: inherit; color: var(--ink-900); background: var(--surface-0);
  resize: none; max-height: 120px; min-height: 40px;
}
.ai-composer-row textarea:focus { outline: 2px solid var(--brand-600); outline-offset: 1px; }
.ai-composer-row .btn-inline, .ai-composer-row .icon-btn { flex-shrink: 0; }
.ai-composer-files { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.ai-composer-chip {
  display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px;
  background: var(--brand-100); color: var(--brand-700); border-radius: 999px; font-size: 12px; font-weight: 600;
}
.ai-composer-chip button { border: none; background: none; color: inherit; cursor: pointer; font-size: 14px; line-height: 1; }
.ai-composer-status { font-size: 12px; color: var(--ink-500); margin: 8px 0 0; min-height: 1em; }
.ai-composer-status.is-active { color: var(--brand-700); font-weight: 600; }
.ai-composer-status.is-error { color: var(--danger-600, #c0392b); font-weight: 600; }

.form-grid { display:grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.form-grid .full { grid-column: 1 / -1; }
/* :not([type=radio|checkbox]) — those render as native small controls;
   without this exclusion they inherit width:100%+padding+border meant
   for text-like inputs and stretch into an unusable full-width box. */
.form-grid .field input:not([type="radio"]):not([type="checkbox"]), .form-grid .field select, .form-grid .field textarea {
  width: 100%; border: 1px solid var(--surface-200); border-radius: var(--radius-sm);
  padding: 8px 10px; font-size: 13.5px; font-family: inherit; color: var(--ink-900); background: var(--surface-0);
}
.form-grid .field textarea { resize: vertical; min-height: 60px; }
.btn-row { display:flex; gap: 10px; margin-top: 18px; }
.btn-inline { display:inline-flex; align-items:center; padding: 7px 13px; border-radius: var(--radius-md);
  font-size: 12.5px; font-weight:700; border:1px solid var(--surface-200); background: var(--surface-0); color: var(--ink-900); cursor:pointer; }
.btn-inline.primary { background: var(--brand-600); border-color: var(--brand-600); color: #fff; }
.btn-inline.danger { border-color: var(--danger-600); color: var(--danger-600); }
.btn-inline:disabled { opacity:.5; cursor:not-allowed; }

/* §contra-two-column - the "Invoices to Settle"/"Purchases to Settle"
   pair on the Contra create screen (resources/views/accounting/contra/
   form.blade.php). Explicit min-width:0 on children is required so an
   unconstrained table.dt inside a grid child can't push past its
   column share and spill into its neighbor - same fix .split-detail
   already documents above, applied to a plain two-up layout instead
   of the split-shell's list/detail pair. */
.contra-allocations { display:grid; grid-template-columns: 1fr 1fr; gap:20px; margin-top:16px; }
.contra-allocations .card-block { min-width:0; }
@media (max-width: 860px) { .contra-allocations { grid-template-columns: 1fr; } }

table.dt { width:100%; border-collapse: collapse; font-size: 13px; background: var(--surface-0); border: 1px solid var(--surface-200); border-radius: var(--radius-md); overflow:hidden; }
table.dt th { text-align:left; font-size: 10.5px; text-transform:uppercase; letter-spacing:.04em; color: var(--ink-500); background: var(--surface-100); padding: 9px 12px; }
table.dt td { padding: 9px 12px; border-top: 1px solid var(--surface-200); }
table.dt td.num, table.dt th.num { text-align: right; }
/* §invoices-list-a11y-polish — a real B2B/wholesale customer name (long
   compound company names are routine in this domain, not an edge case)
   otherwise wraps a row to 5-7 lines with no containment, wrecking the
   scanning rhythm of every neighboring row. Reusable across any table.dt
   consumer, not invented for this one column. */
table.dt td.truncate { max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pill { display:inline-flex; align-items:center; gap:4px; padding:2px 9px; border-radius:999px; font-size:10.5px; font-weight:700; white-space:nowrap; }
.pill.active { background: var(--success-100); color: var(--success-600); }
.pill.archived { background: var(--surface-100); color: var(--ink-500); }
.pill.warning { background: var(--warning-100); color: var(--warning-600); }
.pill.danger { background: var(--danger-100); color: var(--danger-600); }
.pill.pending { background: var(--brand-100); color: var(--brand-700); }

/* §repeat-last-order — a quantity carried over from a past invoice, not yet reviewed by the user (invoice-form.js's invLoadLastOrder()/submitAs()). */
.qty-needs-review { border-color: var(--warning-600) !important; background: var(--warning-100); }

/* §price-exception-highlight — an invoice line that's also a pending
   Closing → Price Exceptions entry (zero price, or a price that doesn't
   match this product's usual one), flagged right on the invoice's own
   line-items table. Every matching line gets this, not just the first. */
tr.price-exception-row { background: var(--warning-100); }
tr.price-exception-row td:first-child { border-left: 3px solid var(--warning-600); }
.price-exception-badge {
  display: inline-flex; align-items: center; margin-left: 8px;
  padding: 1px 8px; border-radius: 999px; font-size: 11px; font-weight: 700;
  color: var(--warning-600); background: #fff; border: 1px solid var(--warning-600);
  cursor: default; white-space: nowrap;
}

/* §line-rows-ssel-width-fix — see the comment above <colgroup> in sales/invoices/_form_fields.blade.php: without this, .ssel-trigger's global 160px min-width can exceed a narrow fixed-layout column and visually spill into the next one. */
#line-rows .ssel-trigger { min-width: 0; width: 80%; }
/* §unit-select-empty-space-fix — a real live report: Unit's trigger was
   landing at exactly the 100px floor below regardless of how much wider
   its own column actually was (Purchase's own Unit column, a different
   shape than Sales Invoice's, left ~50px of visibly dead space after
   it) — because .ssel (this trigger's parent) is inline-block with no
   width of its own, so .ssel-trigger's 80% above has no real basis to
   resolve against and collapses toward its min-width floor instead of
   the column's actual width. Making .ssel itself a block spanning the
   full cell gives that percentage something real to fill against, for
   both this and the Purchase-specific table without needing a second,
   separately-tuned copy of this rule. */
#line-rows td[data-label="Unit"] .ssel { display: block; width: 100%; }
#line-rows td[data-label="Unit"] .ssel-trigger { width: 100%; min-width: 0; }
/* Sales Order's own line-rows table has no data-label attributes at all
   (it never needed the mobile-responsive label mechanism the shared
   Invoice/Purchase partial does), so its Qty Unit/Pricing Unit columns
   need the same fix scoped by class instead — via :has(), since .ssel
   is the select's own direct PARENT (searchable-select.js wraps the
   <select> and its generated trigger button together inside one .ssel
   div), not a preceding sibling the way a plain adjacent-sibling
   selector would assume. */
#line-rows .ssel:has(> select.line-qty-unit),
#line-rows .ssel:has(> select.line-pricing-unit) { display: block; width: 100%; }
#line-rows .ssel:has(> select.line-qty-unit) .ssel-trigger,
#line-rows .ssel:has(> select.line-pricing-unit) .ssel-trigger { width: 100%; min-width: 0; }
/* §product-select-full-name — scoped to the select's own line-product class (via the sibling combinator, since .ssel-trigger is the JS-inserted sibling that follows it), never the bare .ssel/.ssel-trigger classes — Unit (and Sales Order's separate Pricing Unit column) reuse those exact same classes and must not inherit this. .ssel is inline-block and shrink-to-fits its trigger child, so sizing the trigger alone is enough to size the wrapper too. A chosen product's name should never be cut off, so the label wraps instead of ellipsis-truncating. */
#line-rows select.line-product + .ssel-trigger { width: 95%; min-width: 230px; }
#line-rows select.line-product + .ssel-trigger .ssel-trigger-label { white-space: normal; overflow: visible; text-overflow: clip; }

/* §combined-ledger — marks a ledger entry's narration as clickable
   (opens #closing-entry-detail-modal via closing-entry-detail.js). */
.entry-detail-trigger { cursor: pointer; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 2px; }
.entry-detail-trigger:hover { color: var(--brand-700); }

/* Small square icon-only row actions (Users list, S-T04) — a compact
   sibling of .btn-inline for rows with several actions where text
   labels would crowd the table; title="" attribute carries the a11y
   label instead of visible text. */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: var(--radius-sm);
  border: 1px solid var(--surface-200); background: var(--surface-0);
  color: var(--ink-700); cursor: pointer; flex-shrink: 0;
}
.icon-btn svg { width: 15px; height: 15px; }
.icon-btn:hover { background: var(--surface-100); }
.icon-btn.danger { border-color: var(--danger-600); color: var(--danger-600); }
.icon-btn.danger:hover { background: var(--danger-100); }
.icon-btn.success { border-color: var(--success-600); color: var(--success-600); }
.icon-btn.primary { border-color: var(--brand-600); background: var(--brand-600); color: #fff; }
.icon-btn.primary:hover { background: var(--brand-700); border-color: var(--brand-700); }

/* §customer-last-price - custom hover tooltip for the invoice line
   "last price for this customer" icon-btn. Deliberately NOT the native
   title="" attribute other icon-btns use: the browser's own tooltip has
   a built-in OS hover delay that can't be shortened, and this one needs
   to appear the instant the mouse arrives (the underlying data is
   preloaded, so there's no reason left to wait).
   Shown/positioned by invoice-form.js's initLastPriceTooltips(), not
   plain CSS :hover - table.dt sets overflow:hidden (for its
   border-radius), which clips any position:absolute descendant,
   exactly the bug action-menu.js already fixed for the row "..." menu.
   Same fix here: on hover, JS portals this element to document.body
   with position:fixed computed from the button's real coordinates. */
.line-last-price-wrap { display: inline-flex; flex-shrink: 0; }
.last-price-tooltip {
  display: none;
  background: var(--surface-0); border: 1px solid var(--surface-200);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-md);
  padding: 8px 10px; font-size: 11.5px; line-height: 1.5; font-weight: 400;
  color: var(--ink-900); white-space: pre-line; text-align: left;
  width: max-content; max-width: 220px; z-index: 500; pointer-events: none;
}
.icon-btn.success:hover { background: var(--success-100); }

/* Labeled sibling of .icon-btn (icon + visible text) — for actions worth
   a legible label rather than an icon-only square with a title tooltip. */
.icon-btn-labeled {
  display: inline-flex; align-items: center; gap: 6px;
  height: 32px; padding: 0 13px; border-radius: var(--radius-sm);
  border: 1px solid var(--surface-200); background: var(--surface-0);
  color: var(--ink-700); cursor: pointer; font-size: 13px; font-weight: 600;
  text-decoration: none; flex-shrink: 0;
}
.icon-btn-labeled svg { width: 15px; height: 15px; flex-shrink: 0; }
.icon-btn-labeled:hover { background: var(--surface-100); }
.icon-btn-labeled.primary { border-color: var(--brand-600); background: var(--brand-600); color: #fff; }
.icon-btn-labeled.primary:hover { background: var(--brand-700); border-color: var(--brand-700); }
.icon-btn-labeled:disabled { opacity: .6; cursor: default; }

/* §edited-invoices — Closing → Edited Invoices' card list (replaced a
   plain <table class="dt"> — a summary sentence and an old→new total
   don't fit a table cell as readably as a short card). */
.ei-list { display: flex; flex-direction: column; gap: 12px; }
.ei-card { background: var(--surface-0); border: 1px solid var(--surface-200); border-radius: var(--radius-lg); padding: 16px 18px; }
/* §edited-invoices-cancellation — a cancellation is the more serious of
   the two events this queue reviews, so its card gets a visibly
   different (danger-toned) left accent, not just a badge, to catch the
   eye while scanning a mixed list. */
.ei-card-cancelled { border-left: 3px solid var(--danger-600); }
.ei-card-pending { border-left: 3px solid var(--warning-600); }
/* §edited-invoices-row-indicator — a real request: an edited (not cancelled/refused/pending) row's own left stripe, matching .ei-total-new's warning color for a real total change, or a neutral gray when the edit left the total untouched. */
.ei-card-total-changed { border-left: 3px solid var(--warning-600); }
.ei-card-no-change { border-left: 3px solid var(--ink-500); }
.ei-card-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 4px; }
.ei-type-badge { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; margin-right: 8px; vertical-align: middle; }
.ei-type-badge.ei-type-edited { background: var(--brand-100); color: var(--brand-700); }
.ei-type-badge.ei-type-cancelled { background: var(--danger-100); color: var(--danger-600); }
/* §edited-invoices-delivery — a returned/refused delivery reads as a loss (danger), a still-open pending one as unresolved (warning). */
.ei-type-badge.ei-type-delivery_refused { background: var(--danger-100); color: var(--danger-600); }
.ei-type-badge.ei-type-delivery_pending { background: var(--warning-100); color: var(--warning-600); }
.ei-invoice-no { font-family: var(--font-display); font-size: 15px; font-weight: 700; color: var(--ink-900); }
.ei-customer { margin-left: 10px; color: var(--ink-500); font-size: 13px; }
.ei-timestamp { color: var(--ink-500); font-size: 12px; white-space: nowrap; }
.ei-invoice-date { color: var(--ink-700); font-size: 12.5px; margin-bottom: 10px; }
.ei-card-body { margin-bottom: 12px; }
.ei-total-change { display: inline-flex; align-items: center; gap: 8px; padding: 4px 10px; border-radius: var(--radius-sm); background: var(--warning-100); margin-bottom: 8px; font-weight: 700; font-size: 13.5px; }
.ei-total-change svg { width: 14px; height: 14px; color: var(--warning-600); flex-shrink: 0; }
.ei-total-old { color: var(--ink-500); font-weight: 500; text-decoration: line-through; text-decoration-color: var(--ink-500); }
.ei-total-new { color: var(--warning-600); }
.ei-total-change.ei-total-unchanged { background: var(--surface-100); font-weight: 600; }
.ei-total-change.ei-total-unchanged span:first-child { color: var(--ink-900); }
.ei-summary { margin: 0; font-size: 13px; color: var(--ink-700); line-height: 1.5; white-space: pre-wrap; }
.ei-card-actions { display: flex; gap: 8px; }

/* Product Units grid (S-N02) — Unit Type badges, the Unit Rules
   checklist sidebar, and the Conversion Preview strip. */
.unit-type-badge { display:inline-flex; align-items:center; gap:5px; padding:3px 9px; border-radius:999px; font-size:11px; font-weight:700; white-space:nowrap; }
.unit-type-badge.primary_sku { background: var(--brand-100); color: var(--brand-700); }
.unit-type-badge.secondary_unit { background: #E0EAFF; color: #3554C9; }
.unit-type-badge.additional_unit { background: var(--warning-100); color: var(--warning-600); }

.units-layout { display:grid; grid-template-columns: 1fr 300px; gap:18px; align-items:start; }
@media (max-width: 860px) { .units-layout { grid-template-columns: 1fr; } }

.rule-check-list { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:14px; }
.rule-check-list li { display:flex; gap:10px; align-items:flex-start; font-size:12.5px; }
.rule-check-list .rule-icon { flex-shrink:0; width:18px; height:18px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; margin-top:1px; }
.rule-check-list .rule-icon.ok { background: var(--success-100); color: var(--success-600); }
.rule-check-list .rule-icon.pending { background: var(--surface-100); color: var(--ink-500); }
.rule-check-list .rule-title { font-weight:700; color: var(--ink-900); display:block; }
.rule-check-list .rule-desc { color: var(--ink-500); }

.conversion-preview { display:flex; align-items:center; gap:14px; flex-wrap:wrap; margin-top:12px; }
.conversion-chip { display:flex; align-items:center; gap:10px; background: var(--surface-0); border:1px solid var(--surface-200); border-radius: var(--radius-md); padding:10px 16px; }
.conversion-chip .cc-icon { width:34px; height:34px; border-radius: var(--radius-sm); display:flex; align-items:center; justify-content:center; font-size:16px; flex-shrink:0; }
.conversion-chip.primary_sku .cc-icon { background: var(--brand-100); color: var(--brand-700); }
.conversion-chip.secondary_unit .cc-icon { background: #E0EAFF; color: #3554C9; }
.conversion-chip.additional_unit .cc-icon { background: var(--warning-100); color: var(--warning-600); }
.conversion-chip .cc-value { font-family: var(--font-display); font-size: 17px; font-weight:700; color: var(--ink-900); line-height:1.1; }
.conversion-chip .cc-unit { font-size:11.5px; color: var(--ink-500); }
.conversion-eq { font-size:16px; color: var(--ink-500); font-weight:700; }

/* Product Images field — drag & drop / click / paste-from-clipboard. */
.image-dropzone {
  border: 2px dashed var(--surface-200); border-radius: var(--radius-md);
  padding: 28px 16px; text-align:center; cursor:pointer; color: var(--ink-700);
  background: var(--surface-0);
}
.image-dropzone:hover, .image-dropzone:focus, .image-dropzone.is-dragover {
  border-color: var(--brand-600); background: var(--brand-100); outline: none;
}
.image-dropzone .image-dropzone-icon { font-size:22px; color: var(--brand-600); margin-bottom:6px; }

.ai-info-btn {
  display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px;
  border-radius: 50%; color: var(--ink-500); font-size: 14px; cursor: pointer; flex-shrink: 0;
}
.ai-info-btn:hover { background: var(--surface-100); color: var(--brand-700); }

/* Compact inline add/edit rows used by simple master-list screens
   (Categories & Brands, Units) — a lightweight sibling of .form-grid for
   forms that live inside a table row or a toolbar strip rather than a
   full field grid. */
.inline-edit-form { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.inline-edit-form input[type="text"], .inline-edit-form select {
  border: 1px solid var(--surface-200); border-radius: var(--radius-sm);
  padding: 7px 9px; font-size: 13px; font-family: inherit; color: var(--ink-900); background: var(--surface-0);
}
.inline-edit-form input[type="text"]:focus, .inline-edit-form select:focus {
  outline: none; border-color: var(--brand-600);
}

.logo-preview { display:flex; align-items:center; gap:12px; margin-bottom: 10px; }
.logo-preview img { width:48px; height:48px; object-fit:contain; border:1px solid var(--surface-200); border-radius:var(--radius-sm); background:var(--surface-50); }

/* Shared utility classes — Phase 3B sweep replaces per-file inline
   `style="..."` totals blocks and muted empty-state text with these. */
.totals-block { background: var(--accent-100); border-radius: var(--radius-sm); padding: 14px; margin-top: 14px; max-width: 320px; margin-left: auto; }
.totals-row { display:flex; justify-content:space-between; }
.totals-row.grand { font-weight: 700; border-top: 1px solid var(--surface-200); margin-top: 6px; padding-top: 6px; }
.muted { color: var(--ink-500); }

/* §33 Installer (S-I01…S-I09) */
.install-card { max-width: 480px; }
.progress-rail { display:flex; flex-wrap:wrap; gap: 4px 10px; margin-bottom: 22px; padding-bottom: 16px; border-bottom: 1px solid var(--surface-200); }
.progress-rail .step { font-size: 10.5px; font-weight: 700; color: var(--ink-500); white-space: nowrap; }
.progress-rail .step.active { color: var(--brand-700); }
.progress-rail .step.done { color: var(--success-600); }

.check-list { display:flex; flex-direction:column; gap:10px; margin-bottom:20px; }
.check-row { display:flex; align-items:flex-start; gap:9px; font-size:13px; line-height:1.4; }
.check-row .icon { flex-shrink:0; font-weight:800; }
.check-row.pass .icon { color: var(--success-600); }
.check-row.fail .icon { color: var(--danger-600); }
.check-row .remediation { color: var(--ink-500); font-size:12px; margin-top:2px; }

.skip-note { font-size:12px; color: var(--ink-500); margin: 4px 0 16px; line-height:1.5; }
.test-result { font-size:12.5px; margin: 8px 0; padding: 8px 10px; border-radius: var(--radius-sm); display:none; }
.test-result.ok { display:block; background: var(--success-100); color: var(--success-600); }
.test-result.error { display:block; background: var(--danger-100); color: var(--danger-600); }

.review-block { margin-bottom: 14px; }
.review-block h3 { font-size: 11px; text-transform:uppercase; letter-spacing:.04em; color: var(--ink-500); margin: 0 0 6px; }
.review-block dl { display:grid; grid-template-columns: auto 1fr; gap: 3px 10px; font-size: 13px; margin:0; }
.review-block dt { color: var(--ink-500); }
.review-block dd { margin:0; }

.install-progress { display:flex; flex-direction:column; gap:8px; margin: 16px 0; }
.install-progress .step-row { font-size:13px; color: var(--ink-500); display:flex; align-items:center; gap:8px; }
.install-progress .step-row .icon { color: var(--success-600); font-weight:800; }

.complete-warning { background: var(--danger-100); color: var(--danger-600); border-radius: var(--radius-sm); padding: 12px 14px; font-size: 12.5px; line-height:1.5; margin: 16px 0; }

/* §5 Executive Dashboard — stat tiles, list cards, inline SVG graphs.
   Colors reuse the existing brand/success/danger/ink tokens rather than
   a new palette; each graph card is single-hue, so cross-series CVD
   distinction doesn't apply — the card title carries the identity. */
.stat-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap:12px; margin: 16px 0; }
.stat-card { background: var(--surface-0); border:1px solid var(--surface-200); border-radius: var(--radius-lg); padding: 16px 18px; box-shadow: var(--shadow-md); }
.stat-card .stat-label { font-size: 11px; text-transform:uppercase; letter-spacing:.04em; color: var(--ink-500); margin-bottom: 6px; }
.stat-card .stat-value { font-family: var(--font-display); font-size: 22px; font-weight: 700; color: var(--ink-900); }
.stat-card.accent-brand .stat-value { color: var(--brand-700); }
.stat-card.accent-success .stat-value { color: var(--success-600); }
.stat-card.accent-danger .stat-value { color: var(--danger-600); }
.stat-card.accent-warning .stat-value { color: var(--warning-600); }

/* §5 Executive Dashboard — KPI grouping. The stat-grid above is still
   used by every other .stat-card consumer (Products, Customers,
   Suppliers, AI review, etc.); the dashboard alone needed its 8 cards
   split into named sub-groups instead of one flat, undifferentiated
   grid, so this is a page-scoped addition, not a change to .stat-grid
   itself. .kpi-group-label reuses .tile-cat's exact type treatment
   (uppercase, brand-700, tracked) — the label style already
   established lower on this same page for "Quick Links" — rather than
   inventing a second label convention. */
.dash-shell { max-width: 1180px; }
.kpi-section { margin: 16px 0; }
.kpi-group + .kpi-group { margin-top: 22px; }
.kpi-group-label { font-size: 11px; text-transform:uppercase; letter-spacing:.05em; color: var(--brand-700); font-weight:700; margin: 0 0 8px; }
.kpi-row { display:grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap:12px; }

.dash-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap:16px; margin: 16px 0; }
.dash-card { background: var(--surface-0); border:1px solid var(--surface-200); border-radius: var(--radius-lg); padding: 16px 18px; }
.dash-card h3 { margin: 0 0 12px; font-size: 13px; text-transform:uppercase; letter-spacing:.03em; color: var(--ink-500); }
.dash-list { display:flex; flex-direction:column; gap:0; }
.dash-list .row { display:flex; justify-content:space-between; align-items:center; padding: 8px 0; border-top: 1px solid var(--surface-100); font-size: 13px; }
.dash-list .row:first-child { border-top:none; }
.dash-list .row .name { color: var(--ink-900); }
.dash-list .row .meta { color: var(--ink-500); font-size: 11.5px; }
.dash-list .empty { color: var(--ink-500); font-size: 13px; padding: 8px 0; }
.dash-chart svg { display:block; }
.dash-chart text { font-family: var(--font-body); }

/* Customer module UI redesign — list+detail split view, Add/Edit modal,
   invoice drawer, empty state. Composed entirely from existing tokens,
   no new palette. Not a .page-shell (760px) page — a split view needs
   more room, so this gets its own wider container, scoped to this one
   screen rather than raising the cap app-wide. */
.customers-page { max-width: 1320px; margin: 0 auto; padding: 32px 24px 60px; }
.customers-page-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:18px; gap:16px; flex-wrap:wrap; }
.customers-page-head h1 { font-family: var(--font-display); font-size: 22px; margin: 0 0 4px; }
.customers-page-head .sub { color: var(--ink-500); font-size: 13px; margin: 0; }

.customers-toolbar { display:flex; align-items:center; gap:10px; margin-bottom:16px; flex-wrap:wrap; }
.customers-toolbar .search-field { flex:1; min-width:220px; }
.customers-toolbar input[type="search"], .customers-toolbar input[type="date"], .customers-toolbar select {
  border:1px solid var(--surface-200); border-radius:var(--radius-sm);
  padding:8px 11px; font-size:13.5px; font-family:inherit; color:var(--ink-900); background:var(--surface-0);
}
.customers-toolbar input[type="search"] { width:100%; }

/* Product detail — Stock Ledger toolbar (date range + type filter +
   column picker), stock-level bar, insight cards, and the balance
   trend sparkline, all inside the analytics block above the product's
   own identity strip. */
.ledger-toolbar { display:flex; align-items:center; gap:10px; margin-bottom:16px; flex-wrap:wrap; }
.ledger-toolbar .dp-range { display:flex; align-items:center; gap:6px; }
.ledger-toolbar .dp-wrapper { flex: 0 0 auto; }
.ledger-toolbar .dp-wrapper .dp-display { min-width:120px; }
.ledger-toolbar select { border:1px solid var(--surface-200); border-radius:var(--radius-sm); padding:8px 11px; font-size:13px; font-family:inherit; background:var(--surface-0); color:var(--ink-900); }

.stock-level-bar { margin-bottom:20px; }
.stock-level-track { position:relative; height:10px; border-radius:999px; background:var(--surface-100); overflow:hidden; }
.stock-level-fill { height:100%; border-radius:999px; background:var(--success-600); transition:width .2s ease; }
.stock-level-fill.low { background:var(--danger-600); }
.stock-level-marker { position:absolute; top:-3px; width:2px; height:16px; background:var(--ink-700); }
.stock-level-labels { position:relative; display:flex; justify-content:space-between; margin-top:6px; font-size:11px; color:var(--ink-500); }
.stock-level-labels .reorder-label { position:absolute; transform:translateX(-50%); white-space:nowrap; }

.insights-row { display:grid; grid-template-columns:repeat(auto-fit, minmax(160px, 1fr)); gap:12px; margin-bottom:20px; }
.insight-card { display:flex; align-items:center; gap:10px; background:var(--surface-50); border:1px solid var(--surface-200); border-radius:var(--radius-md); padding:12px 14px; }
.insight-icon { font-size:18px; }
.insight-label { font-size:11px; color:var(--ink-500); text-transform:uppercase; letter-spacing:.03em; }
.insight-value { font-family:var(--font-display); font-size:15px; font-weight:700; color:var(--ink-900); margin-top:2px; }

.balance-trend h3 { font-family:var(--font-display); font-size:13px; margin:0 0 8px; }
.balance-trend h3 .muted { font-weight:400; font-size:12px; }

/* Small KPI-style chips atop a list view (e.g. Invoices' Outstanding/
   Overdue/Draft summary) — hidden once the split view opens, per
   §invoice-actions ("not when entering the invoice detail view"). */
.stat-chip-row { display:flex; gap:12px; margin-bottom:16px; flex-wrap:wrap; }
.stat-chip {
  display:flex; flex-direction:column; gap:2px; background:var(--surface-0);
  border:1px solid var(--surface-200); border-radius:var(--radius-md); padding:10px 16px; min-width:140px;
}
.stat-chip-label { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--ink-500); }
.stat-chip-value { font-family:var(--font-display); font-size:18px; font-weight:700; font-variant-numeric:tabular-nums; }
.stat-chip-danger .stat-chip-value { color:var(--danger-600); }

/* Small round-square initials badge — list rows and the detail header
   share one treatment (single brand tint) rather than a new categorical
   palette this app's token system doesn't otherwise have. */
.avatar {
  display:flex; align-items:center; justify-content:center; flex-shrink:0;
  border-radius: 8px; background: var(--brand-100); color: var(--brand-700);
  font-weight:800; font-family: var(--font-display);
  width:32px; height:32px; font-size:12px;
}
.avatar.lg { width:48px; height:48px; font-size:16px; border-radius: var(--radius-md); }

.empty-state { text-align:center; padding:90px 20px; background:var(--surface-0); border:1px solid var(--surface-200); border-radius:var(--radius-lg); }
.empty-state .icon { font-size:40px; margin-bottom:14px; }
.empty-state h2 { font-family:var(--font-display); font-size:17px; margin:0 0 6px; }
.empty-state p { color:var(--ink-500); font-size:13.5px; margin:0 0 22px; }

.split-shell { display:grid; grid-template-columns: minmax(260px, 30%) 1fr; gap:16px; align-items:start; }
.split-shell.split-shell-3col { grid-template-columns: minmax(220px, 22%) minmax(220px, 26%) 1fr; }

.split-list-header { display:flex; align-items:center; padding:10px 12px; border-bottom:1px solid var(--surface-100); }

.split-list {
  background:var(--surface-0); border:1px solid var(--surface-200); border-radius:var(--radius-lg);
  /* §split-list-height-match — this max-height is only the pre-JS/no-JS
     fallback now. On the invoices split view, invSyncSplitListHeight()
     (invoices.js) overwrites it inline to match .split-detail's actual
     rendered height (whichever tab is showing), so the list's own
     scrollbar lines up with the detail panel instead of an unrelated
     viewport-relative box. */
  max-height: calc(100vh - 230px); overflow-y:auto; min-width:0;
}
.split-list .customer-row {
  display:flex; align-items:center; gap:10px; width:100%; text-align:left; padding:12px 16px;
  border:none; border-bottom:1px solid var(--surface-100); background:none; cursor:pointer; font-family:inherit; color:var(--ink-900);
}
.split-list .customer-row:last-child { border-bottom:none; }
.split-list .customer-row:hover { background:var(--surface-50); }
.split-list .customer-row.active { background: var(--brand-100); }
.split-list .customer-row .row-body { display:flex; flex-direction:column; min-width:0; flex:1; }
.split-list .customer-row .row-name { font-weight:700; font-size:13.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.split-list .customer-row .row-meta { font-size:11.5px; color:var(--ink-500); margin-top:2px; }
/* A real report: the date (e.g. "01-Sep-2026") could break across the
   hyphen onto two lines in the list row's narrow column — nowrap keeps
   it one unbreakable token; the rest of the meta line still wraps
   normally if it needs to. */
.split-list .customer-row .row-meta .date-value { white-space:nowrap; }
/* §purchases-list-row — an optional first line above .row-name, for a
   date + status pill (Purchases' 3-line row layout). */
.split-list .customer-row .row-top { display:flex; align-items:center; gap:8px; font-size:11px; color:var(--ink-500); margin-bottom:2px; }

/* min-width:0 overrides the flex/grid-item default of "never shrink below
   content's intrinsic width" — without it, a wide child (the Transactions
   table below) forces this column past its grid track and the whole page
   scrolls horizontally instead of the table scrolling within its own box. */
.split-detail { display:flex; flex-direction:column; gap:16px; min-width:0; }

/* Wraps any wide table.dt inside the detail column so it scrolls within
   its own box instead of forcing .split-detail (and the page) wider. */
.table-scroll { overflow-x:auto; }
.table-scroll table.dt { min-width:600px; }
.tbl-paginate-nav { display:flex; align-items:center; justify-content:center; gap:14px; padding:12px 0 4px; }
.tbl-paginate-nav .btn-inline:disabled { opacity:.4; cursor:not-allowed; }
.tbl-paginate-info { font-size:12.5px; color:var(--ink-500); font-weight:600; }
.detail-header { display:flex; align-items:flex-start; justify-content:space-between; background:var(--surface-0); border:1px solid var(--surface-200); border-radius:var(--radius-lg); padding:20px 24px; gap:16px; flex-wrap:wrap; }
.detail-header .identity { display:flex; align-items:center; gap:14px; }
.detail-header .name-row { display:flex; align-items:center; gap:10px; }
.detail-header h2 { margin:0; font-family:var(--font-display); font-size:18px; }
.detail-header .location { color:var(--ink-500); font-size:12.5px; margin-top:3px; }
.detail-header .head-actions { display:flex; gap:8px; }

.detail-info-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px 24px; }
.detail-info-grid dt { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--ink-500); margin-bottom:3px; }
.detail-info-grid dd { margin:0; font-size:13.5px; white-space:pre-line; }

/* §party-ledger — lightweight secondary-metadata strip (Purchase/
   Invoice Count, Last Transaction, Last Ledger Reviewed, Net Position,
   etc.) below the primary stat-grid KPI row on the Supplier and
   Customer detail panels. A plain .detail-info-grid card, deliberately
   not styled as heavy .stat-cards — keeping the KPI row to just its 3
   primary numbers avoids the uneven 4-then-3 wrapped layout a 6-7 card
   grid was producing. */
.party-meta-strip { background: var(--surface-0); border:1px solid var(--surface-200); border-radius: var(--radius-lg); padding: 14px 18px; margin: 0 0 16px; }
.party-meta-strip dd .pill { margin-left: 6px; vertical-align: middle; }

/* Customer/Invoice detail tabs — plain show/hide panels, no routing. */
.detail-tabs { display:flex; gap:4px; background:var(--surface-0); border:1px solid var(--surface-200); border-radius:var(--radius-lg); padding:6px; }
.detail-tab {
  flex:1; border:none; background:none; cursor:pointer; font-family:inherit; font-weight:700; font-size:12.5px;
  color:var(--ink-500); padding:9px 14px; border-radius:var(--radius-md);
  text-decoration:none; display:inline-flex; align-items:center; justify-content:center; text-align:center;
}
.detail-tab:hover { background:var(--surface-50); color:var(--ink-900); }
.detail-tab.active { background:var(--brand-100); color:var(--brand-700); }
.tab-panel { display:none; }
.tab-panel.active { display:block; }

/* Searchable select — progressive enhancement over a native <select>
   (see public/js/searchable-select.js). The select stays in the DOM,
   visually hidden, so forms/JS reading .value keep working unchanged;
   this only adds a styled trigger + filterable option panel in front. */
.ssel { position:relative; display:inline-block; }
.form-grid .field .ssel, .card-block .field .ssel { width:100%; }
.ssel select { position:absolute; inset:0; width:100%; height:100%; opacity:0; pointer-events:none; }
.ssel-trigger {
  display:flex; align-items:center; justify-content:space-between; gap:8px; min-width:160px;
  border:1px solid var(--surface-200); border-radius:var(--radius-sm);
  padding:8px 11px; font-size:13.5px; font-family:inherit; color:var(--ink-900); background:var(--surface-0);
  cursor:pointer; text-align:left;
}
.form-grid .field .ssel-trigger, .card-block .field .ssel-trigger { width:100%; }
.ssel-trigger:hover { border-color:var(--brand-600); }
.ssel-trigger.open { border-color:var(--brand-600); }
.ssel-trigger-label { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ssel-trigger-arrow { color:var(--ink-500); font-size:10px; flex-shrink:0; }
/* §invoice-type-suggestion — a brief attention-getting pulse when the
   Invoice Type control's value was just auto-set from the chosen
   customer's history (invoice-form.js's flashInvoiceTypeSuggested()),
   so a silent auto-selected change doesn't slip by unnoticed. */
@keyframes type-suggested-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(108,92,231,0.35); border-color: var(--brand-600); }
  50% { box-shadow: 0 0 0 5px rgba(108,92,231,0); border-color: var(--brand-600); }
}
.ssel-trigger.type-suggested { animation: type-suggested-pulse 0.9s ease-out 2; }
.ssel-panel {
  position:absolute; top:calc(100% + 4px); left:0; right:0; z-index:40; min-width:180px;
  background:var(--surface-0); border:1px solid var(--surface-200); border-radius:var(--radius-md);
  box-shadow:var(--shadow-shell); padding:8px; flex-direction:column;
  display:none;
}
/* [hidden] is an attribute selector, same specificity as a class — an
   unconditional `display:flex` above would beat the browser's default
   `[hidden]{display:none}` UA rule (author styles always win over UA
   styles), so the panel would render permanently open. Gating display
   behind :not([hidden]) is what actually lets JS's `panel.hidden = true`
   close it. */
.ssel-panel:not([hidden]) { display:flex; }
.ssel-search {
  width:100%; border:1px solid var(--surface-200); border-radius:var(--radius-sm);
  padding:7px 10px; font-size:13px; font-family:inherit; color:var(--ink-900); margin-bottom:6px;
}
.ssel-search:focus { outline:2px solid var(--brand-600); outline-offset:1px; }
.ssel-options { list-style:none; margin:0; padding:0; overflow-y:auto; max-height:220px; }
.ssel-options li[role="option"] { padding:8px 10px; border-radius:var(--radius-sm); cursor:pointer; font-size:13px; }
.ssel-options li[role="option"]:hover, .ssel-options li[role="option"]:focus { background:var(--surface-50); outline:none; }
.ssel-options li[role="option"].active { background:var(--brand-100); color:var(--brand-700); font-weight:700; }
.ssel-empty { padding:8px 10px; color:var(--ink-500); font-size:12.5px; }
.ssel-options li.ssel-create { color:var(--brand-700); font-weight:600; border-top:1px solid var(--surface-200); margin-top:4px; padding-top:10px; }
.ssel-options li.ssel-create:hover, .ssel-options li.ssel-create:focus { background:var(--brand-100); outline:none; }

/* Column-visibility picker (see public/js/column-picker.js) — same
   trigger + floating panel language as .ssel above, and the same
   [hidden]-must-win CSS shape (display:none by default, display:flex
   only via :not([hidden])) so the panel actually closes. */
.colpick { position:relative; display:inline-block; }
.colpick-panel {
  position:absolute; top:calc(100% + 4px); right:0; z-index:40; min-width:190px;
  background:var(--surface-0); border:1px solid var(--surface-200); border-radius:var(--radius-md);
  box-shadow:var(--shadow-shell); padding:8px; flex-direction:column; gap:1px;
  display:none;
}
.colpick-panel:not([hidden]) { display:flex; }
.colpick-option { display:flex; align-items:center; gap:8px; padding:7px 8px; border-radius:var(--radius-sm); font-size:13px; cursor:pointer; }
.colpick-option:hover { background:var(--surface-50); }
.colpick-option input[type="checkbox"] { accent-color:var(--brand-600); }
.colpick-empty { padding:7px 8px; color:var(--ink-500); font-size:12.5px; }

/* Date picker (see public/js/date-picker.js) — replaces Safari's native
   type="date" UI (which has no direct month/year jump, only single-step
   prev/next) with Month/Year <select>s + a day grid. Same trigger +
   floating panel language and [hidden]-must-win CSS shape as .ssel/
   .colpick above. */
.dp-wrapper { position:relative; display:inline-block; }
/* Full-width only inside a stacked form field — inside a flex-wrap
   toolbar (search/filters row) a 100%-wide item forces itself onto its
   own row and stretches edge-to-edge, which is the opposite of what a
   From/To pair should do; there it just sizes like the other filter
   controls via .dp-display's own min-width below. */
.form-grid .field .dp-wrapper { width:100%; }
.customers-toolbar .dp-wrapper { flex: 0 0 auto; }
.customers-toolbar .dp-wrapper .dp-display { min-width:120px; }
.date-range-group { display:flex; align-items:center; gap:6px; flex:0 0 auto; }
.date-range-sep { color:var(--ink-500); font-size:12.5px; }
.dp-display {
  display:flex; align-items:center; min-width:150px;
  border:1px solid var(--surface-200); border-radius:var(--radius-sm);
  padding:8px 11px; font-size:13.5px; font-family:inherit; color:var(--ink-900); background:var(--surface-0);
  cursor:pointer; text-align:left;
}
.form-grid .field .dp-display { width:100%; }
.dp-display:hover { border-color:var(--brand-600); }
.dp-display.open { border-color:var(--brand-600); }
.dp-display.dp-empty { color:var(--ink-500); }
.dp-panel {
  position:absolute; top:calc(100% + 4px); left:0; z-index:40; width:260px;
  background:var(--surface-0); border:1px solid var(--surface-200); border-radius:var(--radius-md);
  box-shadow:var(--shadow-shell); padding:10px; flex-direction:column; gap:8px;
  display:none;
}
.dp-panel:not([hidden]) { display:flex; }
.dp-header { display:flex; align-items:center; gap:4px; }
.dp-nav {
  border:1px solid var(--surface-200); border-radius:var(--radius-sm); background:var(--surface-0);
  width:26px; height:26px; flex-shrink:0; cursor:pointer; font-size:14px; color:var(--ink-700);
}
.dp-nav:hover { border-color:var(--brand-600); color:var(--brand-700); }
.dp-month-select, .dp-year-select {
  flex:1; border:1px solid var(--surface-200); border-radius:var(--radius-sm);
  padding:5px 6px; font-size:12.5px; font-family:inherit; color:var(--ink-900); background:var(--surface-0);
}
.dp-year-select { flex:0 0 84px; }
.dp-grid { display:grid; grid-template-columns:repeat(7, 1fr); gap:2px; }
.dp-day-label { text-align:center; font-size:11px; color:var(--ink-500); padding:4px 0; }
.dp-day {
  border:none; background:none; border-radius:var(--radius-sm); padding:6px 0;
  font-size:12.5px; color:var(--ink-900); cursor:pointer; text-align:center;
}
.dp-day:hover { background:var(--surface-50); }
.dp-day.today { font-weight:700; color:var(--brand-700); }
.dp-day.active { background:var(--brand-600); color:#fff; }
.dp-footer { display:flex; gap:6px; border-top:1px solid var(--surface-100); padding-top:8px; }

/* Month picker (public/js/month-picker.js) — same trigger + floating
   panel language as .dp-* above, just a 12-month grid with a Year
   prev/next header instead of a day grid with Month/Year selects. */
.mp-wrapper { position:relative; display:inline-block; }
.mp-display {
  display:flex; align-items:center; min-width:150px;
  border:1px solid var(--surface-200); border-radius:var(--radius-sm);
  padding:8px 11px; font-size:13.5px; font-family:inherit; color:var(--ink-900); background:var(--surface-0);
  cursor:pointer; text-align:left;
}
.mp-display:hover { border-color:var(--brand-600); }
.mp-display.open { border-color:var(--brand-600); }
.mp-display.mp-empty { color:var(--ink-500); }
.mp-panel {
  position:absolute; top:calc(100% + 4px); left:0; z-index:40; width:220px;
  background:var(--surface-0); border:1px solid var(--surface-200); border-radius:var(--radius-md);
  box-shadow:var(--shadow-shell); padding:10px; flex-direction:column; gap:8px;
  display:none;
}
.mp-panel:not([hidden]) { display:flex; }
.mp-header { display:flex; align-items:center; justify-content:space-between; gap:4px; }
.mp-nav {
  border:1px solid var(--surface-200); border-radius:var(--radius-sm); background:var(--surface-0);
  width:26px; height:26px; flex-shrink:0; cursor:pointer; font-size:14px; color:var(--ink-700);
}
.mp-nav:hover { border-color:var(--brand-600); color:var(--brand-700); }
.mp-year-label { font-weight:700; font-size:13.5px; color:var(--ink-900); }
.mp-grid { display:grid; grid-template-columns:repeat(4, 1fr); gap:4px; }
.mp-month {
  border:none; background:none; border-radius:var(--radius-sm); padding:8px 0;
  font-size:12.5px; color:var(--ink-900); cursor:pointer; text-align:center;
}
.mp-month:hover { background:var(--surface-50); }
.mp-month.current { font-weight:700; color:var(--brand-700); }
.mp-month.active { background:var(--brand-600); color:#fff; }
.mp-footer { display:flex; gap:6px; border-top:1px solid var(--surface-100); padding-top:8px; }

/* Add/Edit Customer modal — native <dialog> for free focus-trap/Esc-to-
   close/backdrop, styled to match the card language elsewhere. */
dialog.modal {
  border:none; border-radius:var(--radius-lg); padding:0; width:min(640px, 92%);
  box-shadow:var(--shadow-shell); background:var(--surface-0); color:var(--ink-900);
}
/* §purchase-edit-popup — wider variant for forms with a multi-column
   line-items table (Purchases, Supplier Payments, Quick Invoice, AI
   Invoice review) that don't fit the default 640px modal width.
   max-height is raised close to the viewport (not just wider than the
   base .modal-body's 65vh) because the same wide, multi-column
   line-items table also needs real vertical room — squeezed into a
   short box it forced a second, cramped vertical scrollbar on top of
   the table's own horizontal one from .table-scroll.

   §vw-scrollbar-gutter-bug — width was previously capped with `92vw`/
   `95vw`. `vw` is defined relative to the initial containing block's
   width, and on a classic (non-overlay) scrollbar — the macOS default
   the instant a mouse instead of a trackpad is used, and the Windows
   default everywhere — that ICB width includes the ~15-17px scrollbar
   gutter, while the *visible* viewport does not. The dialog (a
   position:fixed top-layer box) ends up sized a scrollbar's-width wider
   than what's actually visible, which is exactly what shows up as an
   outer horizontal scrollbar on the whole page while the dialog is
   open. `%` doesn't have this gap — percentage width on a
   position:fixed box resolves against the containing block the CSS
   Position spec defines for it, which correctly excludes the gutter. */
dialog.modal.modal-lg { width:min(1200px, 95%); }
dialog.modal.modal-lg .modal-body { max-height:88vh; }
dialog.modal::backdrop { background: rgba(24,20,40,.45); backdrop-filter: blur(2px); }

/* §non-blocking-source-preview — AI Invoice review's "View source"
   dialog is opened via .show(), not .showModal() (see _review.blade.php),
   specifically so it never blocks interaction with the invoice form
   behind it. A non-modal dialog gets no ::backdrop and no top-layer
   promotion, but it still inherits dialog.modal's default UA-centered
   position — which would visually sit on top of the form even though
   it's no longer blocking it. Docked to a side panel instead, so the
   source (photo/PDF/voice note + transcript) and the editable invoice
   fields are genuinely visible and usable at the same time.
   §source-panel-fixed-trimming — a real live production report:
   position:fixed here was cutting the panel off (viewport-relative
   fixed positioning nested inside a top-layer dialog is exactly the
   kind of thing real browsers — mobile Safari especially — get wrong).
   position:absolute instead: its containing block is #ai-review-modal
   itself (native <dialog> elements are UA-styled position:absolute,
   so the outer review dialog already qualifies as one — no extra CSS
   needed to make it a valid anchor), so top/right below are now
   relative to the review dialog's own box rather than the viewport.
   Drag-to-move and the resize handle are known not to work correctly
   under this change (the drag JS still does viewport-relative math) —
   left as-is for now, not fixed in this pass.
   §source-panel-always-visible-bug — a real live regression: `display`
   here was set unconditionally, and an author-stylesheet rule always
   wins over the UA stylesheet's own `dialog:not([open]) { display:none }`
   regardless of specificity — so the panel rendered visible at all
   times regardless of its actual open state, .close() removed the
   `open` attribute but nothing visually changed, and every fresh
   dialog (even before "View source" was ever clicked) showed up
   immediately. Scoped to [open] so the UA's own open/close visibility
   toggling is only overridden once the dialog is actually open. */
dialog.modal.source-panel[open] {
  position:absolute; top:90px; right:24px; left:auto; bottom:auto; margin:0;
  width:min(420px, 92vw); height:min(520px, calc(100vh - 120px));
  min-width:300px; min-height:220px; max-width:92vw; max-height:calc(100vh - 40px);
  resize:both; overflow:auto;
  display:flex; flex-direction:column;
}
dialog.modal.source-panel .modal-header { cursor:move; flex:0 0 auto; }
dialog.modal.source-panel .modal-body { flex:1 1 auto; max-height:none; overflow-y:auto; }

/* §source-image-magnifier — a real request: the AI Invoice/Purchase
   review's source photo already fits this panel's own width
   (max-width:100%) and scrolls internally if taller than the panel
   (.modal-body's own overflow-y:auto above) — exactly Daybook's own
   "fit + scroll" approach (see .daybook-image-zoom-wrap) — but reading
   fine print on a phone-photographed bill still means either shrinking
   the panel's own text-scale or relying on the OS's native pinch-zoom.
   Hovering the image now shows a magnified lens instead, same concept
   as Daybook's own §row-magnifier but adapted to this much smaller,
   user-resizable panel: `position:absolute` within the image's own
   wrap (not `position:fixed` to the viewport, which app.css's own
   §source-panel-fixed-trimming already found unreliable nested inside
   a top-layer <dialog>) and true 2-D (X+Y) panning at a fixed zoom
   factor, since this panel is nowhere near full-viewport width the
   way Daybook stretches its own row-only magnifier — see
   source-image-magnifier.js. */
.source-image-zoom-wrap { position:relative; }
.source-image-zoom-wrap img.zoomable-source-img { width:100%; display:block; border-radius:var(--radius-sm); cursor:zoom-in; }
.source-image-magnifier-lens { position:absolute; left:0; right:0; height:170px; display:none; pointer-events:none; border:2px solid var(--brand-600); background-color:#fff; background-repeat:no-repeat; box-shadow:var(--shadow-lg); border-radius:var(--radius-sm); z-index:5; }

/* §source-panel-reserved-space — a real live report: once the review
   popup's own body was scrolled down, the fixed-position source panel
   sat ON TOP of the Line Items table's right-hand columns (Price,
   Total), permanently hiding them behind the panel rather than merely
   sitting beside the form. Reserves the panel's own width as padding on
   the form's content area instead, so the table (already wrapped in
   .table-scroll) reflows/scrolls within its own narrower box — nothing
   ever renders underneath the panel. :has() keys off the dialog's own
   `open` attribute, which the browser keeps in sync with .show()/
   .close() — no JS bookkeeping needed.
   §source-panel-body-id-bug — a real live regression: this used to
   target `.modal-body` by class, which ALSO matched the source panel's
   own internal `.modal-body` (itself a descendant of #ai-review-modal
   once portalled), squeezing its content down to near-zero width —
   audio controls collapsed, images stopped rendering. Scoped to the
   outer review body's own unique id instead, which the source panel's
   internal body never carries. */
#ai-review-modal:has(> dialog.source-panel[open]) #ai-review-modal-body {
  padding-right: 460px;
}
.modal-header { display:flex; align-items:center; justify-content:space-between; padding:18px 24px; border-bottom:1px solid var(--surface-200); }
.modal-header h2 { margin:0; font-family:var(--font-display); font-size:16px; }
.modal-close { border:none; background:none; font-size:20px; color:var(--ink-500); cursor:pointer; line-height:1; padding:4px; }
.modal-close:hover { color:var(--ink-900); }
.modal-body { padding:22px 24px; max-height:65vh; overflow-y:auto; }
.modal-footer { display:flex; justify-content:flex-end; gap:10px; padding:16px 24px; border-top:1px solid var(--surface-200); }

/* §delivery-actions-picker — a checklist of claimable line items (one
   invoice can have several), each with its own editable quantity, so
   Delivery Refused/Pending/Credit Note can be applied to more than one
   line in a single confirm instead of forcing the modal to be reopened
   per line. */
.delivery-picker-items-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:6px; }
.delivery-picker-select-all { display:flex; align-items:center; gap:6px; font-size:12.5px; font-weight:600; color:var(--ink-700); }
.delivery-picker-qty-head { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.03em; color:var(--ink-500); }
.delivery-picker-items { display:flex; flex-direction:column; gap:6px; max-height:240px; overflow-y:auto; border:1px solid var(--surface-200); border-radius:var(--radius-md); padding:8px; }
.delivery-picker-item-row { display:flex; align-items:center; gap:10px; padding:6px 4px; border-radius:var(--radius-sm); }
.delivery-picker-item-row:hover { background:var(--surface-50); }
.delivery-picker-item-row .dpi-name { flex:1 1 auto; font-size:13px; color:var(--ink-900); }
.delivery-picker-item-row .dpi-qty { width:90px; flex:0 0 auto; }
.delivery-picker-item-row .dpi-qty:disabled { opacity:.5; }
.delivery-picker-settlement-options { display:flex; gap:14px; flex-wrap:wrap; }
.delivery-picker-settlement-options label { display:flex; align-items:center; gap:5px; font-size:13px; font-weight:500; color:var(--ink-700); }

/* §global-tooltip — dark rounded bubble, portalled to body by
   tooltip.js (position:fixed) so it's never clipped by an
   overflow:hidden ancestor. Any element with data-tooltip="..." gets
   this automatically once initGlobalTooltips() runs. */
.gt-tooltip {
  position: fixed;
  z-index: 3000;
  pointer-events: none;
  background: var(--ink-900);
  color: #fff;
  font-size: 11.5px;
  font-weight: 600;
  line-height: 1.4;
  padding: 6px 11px;
  border-radius: 8px;
  box-shadow: var(--shadow-md);
  max-width: 240px;
  text-align: center;
  opacity: 0;
  visibility: hidden;
  transform: translateY(4px);
  transition: opacity .12s ease, transform .12s ease;
}
.gt-tooltip.visible { opacity: 1; transform: translateY(0); }
.gt-tooltip::after {
  content: '';
  position: absolute;
  left: var(--gt-arrow-x, 50%);
  margin-left: -5px;
  width: 10px; height: 10px;
  background: var(--ink-900);
  transform: rotate(45deg);
  border-radius: 2px;
}
.gt-tooltip[data-placement="top"]::after { bottom: -4px; }
.gt-tooltip[data-placement="bottom"]::after { top: -4px; }

