/* ============================ TOKENS ============================ */
:root{
  /* Light — brand: Sellevry blue accent (the se. logo tile), its violet as the
     second colour, Trimble Gray text on a cool Gray-Light canvas, Trimble Yellow
     as the signal accent.
     Palette: Gray Light #eef0f3 · Sellevry blue #0350a6 · Blue Dark #023f86 ·
     Violet #6337ad · Trimble Gray #283740 · Trimble Yellow #f6a31c. */
  --accent:#0350a6;
  --accent-soft:#0350a614;
  --accent-text:#ffffff;
  --logo-plate:#ffffff;
  --bg:#eef0f3;
  --surface:#ffffff;
  --surface-2:#e7eaef;
  --elev:#ffffff;
  --border:#d3d8e0;
  --border-strong:#b4bcc8;
  --text:#283740;
  --text-2:#4e5a65;
  --muted:#5c6772;
  --shadow:0 1px 2px rgba(40,55,64,.06), 0 8px 24px rgba(40,55,64,.10);
  --shadow-lg:0 12px 40px rgba(40,55,64,.18);
  --radius:14px;
  --radius-sm:10px;
  --green:#1a7a4e; --green-bg:#1a7a4e22;
  --blue:#1f88c7;  --blue-bg:#1f88c722;
  --amber:#b97e09; --amber-bg:#f6a31c2b;
  --red:#d23b2e;   --red-bg:#d23b2e20;
  --gray:#5f6b75;  --gray-bg:#5f6b7520;
  --violet:#6337ad;--violet-bg:#6337ad1f;
  --magenta:#b01480;--magenta-bg:#d81b9a1f;
  /* chrome — branded topbar + dark sidebar so color frames the content */
  --topbar-bg:#0350a6; --topbar-fg:#ffffff;
  --sidebar-bg:#ffffff; --sidebar-border:var(--border);
  --sidebar-fg:#4e5a65; --sidebar-strong:#283740; --sidebar-hover:var(--surface-2);
  --sidebar-active-bg:var(--accent-soft); --sidebar-active-fg:var(--accent);
  --sidebar-indicator:#ec9a3a; --sidebar-badge-bg:var(--accent); --sidebar-badge-fg:#ffffff;  /* indicator = the logo's orange dot */
  --th-bg:#023f86; --th-fg:#ffffff; --th-accent:#7cc0ee;  /* see theme.css for why the accent can't be used raw here */
}
[data-theme="dark"]{
  --accent:#e6a32e;
  --accent-soft:#e6a32e1f;
  --accent-text:#1a1407;
  --logo-plate:#eef0f3;
  --bg:#0f1216;
  --surface:#171b21;
  --surface-2:#1b2027;
  --elev:#1c222a;
  --border:#262c35;
  --border-strong:#323a45;
  --text:#e7eaee;
  --text-2:#a9b2bf;
  --muted:#aab4c2;
  --shadow:0 1px 2px rgba(0,0,0,.3), 0 10px 30px rgba(0,0,0,.35);
  --shadow-lg:0 16px 50px rgba(0,0,0,.5);
  --green:#34d27b; --green-bg:#34d27b1a;
  --blue:#5aa2ff;  --blue-bg:#5aa2ff1a;
  --amber:#e6a32e; --amber-bg:#e6a32e1f;
  --red:#f87171;   --red-bg:#f871711a;
  --gray:#94a3b8;  --gray-bg:#94a3b81a;
  --violet:#a78bfa;--violet-bg:#a78bfa1a;
  --magenta:#f472b6;--magenta-bg:#d81b9a26;
  --topbar-bg:#10151b; --topbar-fg:#e7eaee;
  --sidebar-bg:#10151b; --sidebar-border:#262c35;
  --sidebar-fg:#8893a1; --sidebar-strong:#ffffff; --sidebar-hover:rgba(255,255,255,.07);
  --sidebar-active-bg:var(--accent-soft); --sidebar-active-fg:var(--accent);
  --sidebar-indicator:var(--accent); --sidebar-badge-bg:var(--accent); --sidebar-badge-fg:#1a1407;
  --th-bg:#28333f; --th-fg:#e7ecf1; --th-accent:var(--accent);
}
*{box-sizing:border-box}
html,body{margin:0;height:100%}

/* ── LUCIDE ICON PLACEHOLDERS ────────────────────────────────────────────────
   Every icon in the app ships as an EMPTY `<i data-lucide="…">` that the
   deferred Lucide bundle replaces with an `<svg>` on DOMContentLoaded. An empty
   inline `<i>` is 0x0, so until that swap every label beside an icon sat where
   the icon wasn't — and the whole sidebar rail and section tab strip slid
   sideways the moment the icons arrived. Same "reserve the box" rule as the
   loading skeletons and the server-rendered nav counts, applied to the icons.
   `display:inline-block` is what makes width/height apply at all on a
   non-replaced `<i>`; 24px is Lucide's own default size, so an icon no rule
   sizes reserves exactly the box it will take.
   SIZING AN ICON? Size the placeholder in the SAME rule — every `svg` sizing
   rule in this sheet carries an `i[data-lucide]` twin (`.navlink svg,.navlink
   i[data-lucide]{…}`) so the two can never drift apart. These rules stop
   applying the instant the real `<svg>` exists, so they only ever describe the
   pre-hydration box.
   `:where()` zeroes this rule's specificity so ANY twin wins on load order
   alone — edit-panels.css sizes every icon to 1em and loads BEFORE this sheet,
   and a plain `i[data-lucide]` selector here would have out-ranked it and
   reserved 24px for a 14px icon. */
:where(i[data-lucide]){display:inline-block;width:24px;height:24px}
/* Every page keeps its vertical scrollbar track, scrolling or not. The styled
   ::-webkit-scrollbar below always takes 10px, so a page with nothing to scroll (a
   short page, the viewport-locked CRM inbox) was 10px wider than one that scrolls
   and the fixed topbar jumped sideways between them. (scrollbar-gutter:stable can't
   do this — Chrome reserves no gutter while macOS uses overlay scrollbars.)
   Lock page scroll on <html>, never <body>: with this set, overflow on body no
   longer reaches the viewport — it clips body to one screen and jumps to the top. */
html{overflow-y:scroll}
body{
  background:var(--bg);color:var(--text);
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;
}
::selection{background:var(--accent-soft)}
h1,h2,h3,h4,.display{font-family:"Space Grotesk",sans-serif;letter-spacing:-.01em;margin:0}
.mono{font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace}
a{color:inherit;text-decoration:none}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:10px;border:2px solid var(--bg)}
::-webkit-scrollbar-track{background:transparent}

/* ============================ TOPBAR ============================ */
/* --scroll-lock-gap is the scrollbar width a page scroll lock takes away (x-modal.js
   while a modal is open; x-topbar's mobile nav drawer). Fixed chrome is sized by the
   viewport, not body, so body's padding can't hold it still: anything pinned to the
   right edge adds the gap itself (here, .toast-region, .sw-fab), or it jumps right. */
.topbar{
  position:fixed;top:0;left:0;right:0;height:60px;z-index:50;
  display:flex;align-items:center;gap:18px;padding:0 calc(22px + var(--scroll-lock-gap, 0px)) 0 22px;
  background:var(--topbar-bg);color:var(--topbar-fg);
  border-bottom:1px solid color-mix(in srgb,var(--topbar-bg) 75%,#000);
}
.brand{display:flex;align-items:center;gap:10px;font-family:"Space Grotesk";font-weight:700;font-size:18px;color:var(--topbar-fg);text-decoration:none;line-height:1.05}
.brand:hover{color:var(--topbar-fg)}
/* LOGO LOCKUP — the se. tile + the "sellevry" wordmark (markup: partials/logo-lockup).
   The wordmark is a mask painted in currentColor, so it takes the ink of the surface
   it sits on in both themes: --topbar-fg on the topbar, the text colour on a card.
   No plate behind it. --logo-h is the tile's size; everything else follows it. */
.logo-lockup{--logo-h:36px;display:inline-flex;align-items:center;gap:calc(var(--logo-h) * .26);flex-shrink:0;line-height:0;color:var(--text-strong,var(--text))}
.logo-lockup .logo-mark{display:block;flex:none;width:var(--logo-h);height:var(--logo-h);border-radius:23.4%}
/* height = the tile's 53% ascender height plus the y's descender. The box carries
   that descender, so it drops 11% to centre the letters (not the box) on the tile. */
.logo-lockup .logo-word{display:block;flex:none;height:calc(var(--logo-h) * .68);aspect-ratio:417/109;background:currentColor;
  -webkit-mask:url(/sellevry-wordmark.svg) center/contain no-repeat;mask:url(/sellevry-wordmark.svg) center/contain no-repeat;transform:translateY(11%)}
.logo-lockup.stacked{--logo-h:72px;flex-direction:column;gap:calc(var(--logo-h) * .16)}
.logo-lockup.stacked .logo-word{height:calc(var(--logo-h) * .5);transform:none}
/* the topbar shows the tile alone (markOnly), 44px of the 60px bar */
.brand .logo-lockup{--logo-h:44px;color:var(--topbar-fg)}
/* …centred over the 68px sidebar rail, in line with its icons, instead of starting
   at the bar's 22px padding (which left it flush with the rail's right edge) */
body:has(.sidebar) .brand .logo-lockup{margin-left:calc((68px - var(--logo-h)) / 2 - 22px)}
/* a hairline in the bar's own ink keeps the tile's blue corner from melting into
   the blue light-mode topbar */
.brand .logo-lockup .logo-mark{box-shadow:0 0 0 1px color-mix(in srgb,var(--topbar-fg) 30%,transparent)}
/* Tenant company logo (uploaded via Admin → Company Profile), shown on
   CUSTOMER-facing brand bars/letterheads + the admin preview. The plate keeps an
   arbitrary (often transparent-PNG) logo legible on any surface. */
.co-logo-plate{display:inline-flex;align-items:center;justify-content:center;background:var(--logo-plate);border:1px solid var(--border);border-radius:8px;padding:4px 8px}
.co-logo{height:28px;max-width:180px;width:auto;object-fit:contain;display:block}
.brand .mark{
  width:32px;height:32px;border-radius:9px;display:grid;place-items:center;
  background:rgba(255,255,255,.16);color:var(--topbar-fg);
}
.brand .mark svg,.brand .mark i[data-lucide]{width:19px;height:19px}
.brand small{font-weight:500;color:color-mix(in srgb,var(--topbar-fg) 70%,transparent);font-size:11px;font-family:"JetBrains Mono";letter-spacing:.02em}
.search{
  flex:1;max-width:460px;margin:0 auto;position:relative;
}
.search input{
  width:100%;height:38px;border-radius:10px;border:1px solid transparent;
  background:rgba(255,255,255,.14);color:var(--topbar-fg);padding:0 14px 0 38px;font-size:13.5px;outline:none;
  transition:.15s;font-family:inherit;
}
.search input::placeholder{color:color-mix(in srgb,var(--topbar-fg) 65%,transparent)}
.search input:focus{border-color:rgba(255,255,255,.5);box-shadow:0 0 0 3px rgba(255,255,255,.12);background:rgba(255,255,255,.2)}
.search .si{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:color-mix(in srgb,var(--topbar-fg) 70%,transparent);width:16px;height:16px}
.search .search-clear{
  position:absolute;right:8px;top:50%;transform:translateY(-50%);
  width:24px;height:24px;border:none;border-radius:6px;cursor:pointer;
  display:grid;place-items:center;background:transparent;
  color:color-mix(in srgb,var(--topbar-fg) 70%,transparent);transition:.15s;
}
.search .search-clear:hover{background:rgba(255,255,255,.18);color:var(--topbar-fg)}
.search .search-clear svg,.search .search-clear i[data-lucide]{width:15px;height:15px}
/* global typeahead results overlay (x-topbar.ejs) — wider than the field, grouped */
.search-results{position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%);width:min(640px,92vw);background:var(--surface);border:1px solid var(--border);border-radius:14px;box-shadow:var(--shadow-lg);padding:7px;z-index:60;max-height:min(72vh,520px);overflow-y:auto}
.search-results .sr-group{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);padding:10px 11px 4px}
.search-results .sr-group:first-child{padding-top:4px}
.search-results .sr-row{display:flex;align-items:center;gap:11px;padding:8px 11px;border-radius:9px;color:var(--text);text-decoration:none}
.search-results .sr-row:hover,.search-results .sr-row.active{background:var(--surface-2)}
.search-results .sr-thumb{flex-shrink:0;width:34px;height:34px;border-radius:7px;object-fit:cover;background:var(--surface-2);border:1px solid var(--border)}
.search-results .sr-main{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}
.search-results .sr-label{font-size:13.5px;font-weight:500;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.search-results .sr-sub{font-size:11.5px;color:var(--muted);font-family:"JetBrains Mono";white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.search-results .sr-pill{flex-shrink:0;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;padding:3px 9px;border-radius:20px;background:var(--amber-bg);color:var(--amber)}
.search-results .sr-empty{padding:18px;text-align:center;color:var(--muted);font-size:13px}
/* In-page typeahead (App.typeahead, /js/typeahead.js): the same overlay, but
   anchored to the LEFT edge of its field instead of centred under the topbar. */
.ta-host{position:relative}
.search-results.sr-anchor{left:0;transform:none;top:calc(100% + 6px);width:min(520px,90vw);min-width:100%;max-height:min(60vh,400px)}
/* full-page search results (GET /search) — reuse the overlay's grouped rows
   without the floating-overlay chrome, so the page and typeahead stay identical */
.search-results.sr-page{position:static;transform:none;width:100%;max-width:none;max-height:none;overflow:visible;box-shadow:none;padding:0;background:transparent;border:none;z-index:auto}
.search-results.sr-page .sr-group{padding:16px 2px 8px;font-size:11px}
.search-results.sr-page .sr-group:first-child{padding-top:2px}
.search-results.sr-page .sr-row{padding:11px 12px;border:1px solid var(--border);border-radius:10px;margin-bottom:8px;background:var(--surface)}
.search-results.sr-page .sr-label{font-size:14px}
.search-results.sr-page .sr-thumb{width:44px;height:44px;cursor:zoom-in}
/* In-card picker (`sr-page` + `sr-scroll`): an in-page results list that must NOT
   grow its card — bounded height with its own scroller, so a full page of hits
   doesn't push the rest of the composer off-screen. The full-page /search listing
   keeps plain `sr-page` and stays unbounded. */
.search-results.sr-page.sr-scroll{max-height:min(46vh,340px);overflow-y:auto;overscroll-behavior:contain;padding-right:4px}
/* the global thumb rings itself with --bg; inside a card it sits on --surface */
.search-results.sr-page.sr-scroll::-webkit-scrollbar-thumb{border-color:var(--surface)}
/* A picker row you SELECT rather than follow (the Link-to-product window,
   /js/order-line-fix.js): the same row as a <button>, the picked one ringed in the
   accent, and a status .pill at its end that never squeezes. */
.search-results button.sr-row{width:100%;font:inherit;text-align:left;cursor:pointer;border:0;background:transparent;-webkit-appearance:none;appearance:none}
.search-results.sr-page .sr-row[aria-selected="true"]{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}
.search-results .sr-row>.pill{flex-shrink:0}

/* ── Image picker (`.imgpick`) — a strip of selectable photo thumbnails ──────
   Shared chrome for "which of this item's photos go in the post". Modelled on
   the product page's thumbstrip, but in canonical tokens: unpicked tiles dim
   back, picked tiles carry the accent ring, a check, and their 1-based ORDER
   (the number is the sequence the photos are sent in, which is what a carousel
   shows). Tile is a <button> so it's keyboard-reachable. */
.imgpick{display:flex;flex-wrap:wrap;gap:8px;margin:2px 0 10px}
.imgpick-tile{position:relative;flex:0 0 auto;width:60px;height:60px;padding:0;border-radius:9px;overflow:hidden;
  border:1px solid var(--border);background:var(--surface-2);cursor:pointer;opacity:.4;
  transition:opacity .12s ease,border-color .12s ease,box-shadow .12s ease}
.imgpick-tile img{width:100%;height:100%;object-fit:cover;display:block}
.imgpick-tile:hover{opacity:.75;border-color:var(--border-strong)}
.imgpick-tile:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.imgpick-tile.is-on{opacity:1;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}
.imgpick-tile.is-on:hover{opacity:1}
.imgpick-badge{position:absolute;top:3px;right:3px;width:17px;height:17px;border-radius:50%;display:none;
  place-items:center;background:var(--accent);color:var(--on-brand);font-size:9.5px;font-weight:700;line-height:1}
.imgpick-tile.is-on .imgpick-badge{display:grid}
.imgpick-badge svg,.imgpick-badge i[data-lucide]{width:11px;height:11px}
/* Over-cap tiles stay pickable but flag that the platform will drop them. */
.imgpick-tile.is-over .imgpick-badge{background:var(--pill-amber-bg);color:var(--pill-amber-text);box-shadow:inset 0 0 0 1px var(--pill-amber-border)}
.imgpick-empty{font-size:12.5px;color:var(--muted)}
/* Drag-to-reorder (SortableJS). The strip's left-to-right order IS the post
   order, so the tiles advertise that they move. */
.imgpick.is-draggable .imgpick-tile{cursor:grab}
.imgpick.is-draggable .imgpick-tile:active{cursor:grabbing}
.imgpick-tile.sortable-chosen{cursor:grabbing}
.imgpick-tile.sortable-ghost{opacity:.25;border-style:dashed;border-color:var(--accent)}
.imgpick-tile.sortable-ghost img{visibility:hidden}
.imgpick-tile img{pointer-events:none;-webkit-user-drag:none;user-select:none}
/* Search results tables (mirror the inventory products grid). `.sr-table` is the
   shared base for every result-type table; `.sr-ptable` adds the fixed column
   layout that lets Products + Orders line up column-for-column down the page. */
.sr-table td{vertical-align:middle;overflow:hidden;text-overflow:ellipsis}
.sr-table tbody tr[data-href]{cursor:pointer}
.sr-table .sr-col-icon{width:52px;padding-right:0}
.sr-table .sr-thumb{width:40px;height:40px;border-radius:7px;object-fit:cover;background:var(--surface-2);border:1px solid var(--border);cursor:zoom-in;display:block}
.sr-table .sr-rowicon{width:22px;height:22px;color:var(--muted)}
.sr-table .sr-tlink{color:var(--text);text-decoration:none;font-weight:500}
.sr-table .sr-tlink:hover{text-decoration:underline}
.sr-table .num-center{text-align:center;font-variant-numeric:tabular-nums}
/* Name / item / email columns: cap width and ellipsis the inner text. */
.sr-table .sr-col-name{max-width:260px}
.sr-table .sr-col-name>a,.sr-table .sr-col-name>span{display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Secondary muted line under a name (consignment item, customer email). */
.sr-table .sr-sub2{font-size:11.5px;color:var(--muted);font-weight:400;margin-top:1px}
/* Group headings between tables — bold, with breathing room above. */
.card .sr-grouphead{font-family:var(--font-display);font-size:14px;font-weight:600;letter-spacing:-.01em;text-transform:none;color:var(--text);padding:0;margin:26px 0 10px}
.card .sr-grouphead:first-child{margin-top:6px}
.sr-ptable{table-layout:fixed;width:100%}
.sr-ptable col:nth-child(1){width:52px}    /* icon / thumbnail   */
.sr-ptable col:nth-child(3){width:136px}   /* SKU / order #      */
.sr-ptable col:nth-child(4){width:112px}   /* price / total      */
.sr-ptable col:nth-child(5){width:64px}    /* qty / items        */
.sr-ptable col:nth-child(6){width:126px}   /* modified / date    */
.sr-ptable col:nth-child(7){width:132px}   /* status             */
/* page dim behind the search overlay (matches the copilot backdrop), below the
   fixed topbar so the search box + overlay stay bright */
.search-scrim{position:fixed;top:60px;left:0;right:0;bottom:0;background:rgba(16,42,94,.18);opacity:0;pointer-events:none;transition:opacity .18s ease;z-index:45}
.search-scrim.open{opacity:1;pointer-events:auto}
.tb-actions{display:flex;align-items:center;gap:6px;margin-left:auto}
.iconbtn{
  width:38px;height:38px;border-radius:10px;border:1px solid transparent;background:transparent;
  color:color-mix(in srgb,var(--topbar-fg) 85%,transparent);display:grid;place-items:center;cursor:pointer;transition:.15s;position:relative;
}
.iconbtn:hover,.iconbtn.is-active{background:rgba(255,255,255,.15);color:var(--topbar-fg)}
.iconbtn svg,.iconbtn i[data-lucide]{width:18px;height:18px}
.iconbtn .dot{position:absolute;top:8px;right:9px;width:7px;height:7px;border-radius:50%;background:var(--sidebar-indicator);border:2px solid var(--topbar-bg)}
/* "Who's online" — the count rides the icon as a corner badge, in the same spot
   as the CoPilot bell's .header-bell-count but a size down (14px/9px vs 17px/10px):
   a headcount is ambient, the bell's count is a to-do.
   GREEN, not the bell's red: this is presence,
   not an alert. The colour pair is --presence-bg/--presence-text (theme.css),
   picked against --topbar-bg rather than a page surface — that is the trap here:
   --success is dark green, which on the light theme's blue topbar comes out at
   1.16:1 and simply vanishes.
   Defined here rather than reused from copilot-widget.css because that sheet is
   lazily loaded and absent on 14 pages, while the topbar renders on all of them.
   Sits on .iconbtn, which is already position:relative. */
.presence-count{
  position:absolute;top:-1px;right:-1px;min-width:14px;height:14px;padding:0 3px;box-sizing:border-box;
  border-radius:999px;background:var(--presence-bg);color:var(--presence-text);
  font-size:9px;font-weight:700;font-variant-numeric:tabular-nums;line-height:14px;text-align:center;
}
.presence-count.hidden{display:none}
/* A to-do count on a topbar icon — the Support headset's unread Sellevry replies.
   Same corner, size and red as the bell's .header-bell-count beside it, but
   defined here because copilot-widget.css (where the bell's lives) is lazily
   loaded and absent on some pages. Server-rendered, `hidden` at zero, and
   /js/nav-badges.js only flips that attribute — so `[hidden]` must win over the
   display below. */
.iconbtn-count{
  position:absolute;top:-3px;right:-3px;min-width:17px;height:17px;padding:0 4px;box-sizing:border-box;
  border-radius:999px;background:var(--danger);color:var(--on-brand);box-shadow:0 0 0 2px var(--topbar-bg);
  font-size:10px;font-weight:700;font-variant-numeric:tabular-nums;line-height:17px;text-align:center;
}
.iconbtn-count[hidden]{display:none}
/* …and the names behind it. Same chrome as the avatar's .user-menu-dropdown a
   few pixels to the right (surface, 1.25px border, 8px radius) so the two panels
   hanging off this bar read as one family — just smaller, because this is a
   hover tooltip and not a menu. Sits BESIDE the icon, off its left edge, with
   its TOP aligned to the button's (top:0, not top:50%): centring it on a 38px
   button makes a multi-name panel grow upwards out of the 60px topbar, so it
   reads as floating above the icon. Opening inward from the right-hand action
   cluster also means it can never run past the viewport edge. Revealed on hover AND
   focus-within, so it is reachable from the keyboard; visibility (not display)
   keeps it fade-able and out of the a11y tree while hidden. */
.presence{position:relative;display:inline-flex}
.presence-pop{
  position:absolute;right:calc(100% + 8px);top:0;min-width:150px;max-width:250px;z-index:1000;
  background:var(--surface);border:1.25px solid var(--border);border-radius:8px;box-shadow:var(--shadow-lg);
  padding:8px 10px;color:var(--text);font-size:12.5px;line-height:1.5;text-align:left;
  opacity:0;visibility:hidden;transform:translateX(4px);pointer-events:none;
  transition:opacity .12s ease,transform .12s ease,visibility .12s;
}
/* Once open the panel TAKES the pointer: a long list scrolls (.presence-pop-list
   max-height), and with pointer-events:none the wheel/scrollbar was dead and the
   hover dropped the moment the pointer left the icon. The ::after bridges the 8px
   gap to the icon, so moving across keeps .presence:hover alive; it spans the
   panel's full height, which is always at least the button's. Hidden, the panel
   stays pointer-events:none so it never catches a hover it didn't earn. */
.presence-pop::after{content:"";position:absolute;top:-1.25px;bottom:-1.25px;left:100%;width:10px}
.presence:hover .presence-pop,.presence:focus-within .presence-pop{opacity:1;visibility:visible;transform:none;pointer-events:auto}
.presence-pop-head{font-size:10.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--text-muted);margin-bottom:3px}
.presence-pop-list{list-style:none;margin:0;padding:0;max-height:220px;overflow-y:auto}
.presence-pop-list li{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:1px 0}
.avatar{
  width:34px;height:34px;border-radius:50%;display:grid;place-items:center;font-weight:600;font-size:12.5px;
  font-family:"Space Grotesk";color:#fff;background:linear-gradient(140deg,#6366f1,#8b5cf6);cursor:pointer;margin-left:6px;
}

/* ============================ SIDEBAR ============================ */
.sidebar{
  position:fixed;top:60px;bottom:0;left:0;width:68px;z-index:40;
  background:var(--sidebar-bg);border-right:1px solid var(--sidebar-border);
  padding:12px 0;overflow:hidden;transition:width .18s cubic-bezier(.4,0,.2,1);
  display:flex;flex-direction:column;
}
.sidebar:hover{width:248px;box-shadow:var(--shadow-lg)}
.nav{display:flex;flex-direction:column;gap:2px;padding:0 12px;overflow-y:auto;overflow-x:hidden;flex:1}
.nav::-webkit-scrollbar{width:0}
.nav-group{font-size:10.5px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:color-mix(in srgb,var(--sidebar-fg) 75%,transparent);
  padding:14px 12px 6px;white-space:nowrap;opacity:0;transition:opacity .18s}
.sidebar:hover .nav-group{opacity:1}
.navlink{
  display:flex;align-items:center;gap:13px;height:42px;padding:0 11px;border-radius:10px;
  color:var(--sidebar-fg);cursor:pointer;white-space:nowrap;position:relative;transition:.13s;
}
.navlink svg,.navlink i[data-lucide]{width:20px;height:20px;flex-shrink:0}
.navlink span{opacity:0;transition:opacity .18s;font-weight:500;font-size:13.5px}
.sidebar:hover .navlink span{opacity:1}
.navlink:hover{background:var(--sidebar-hover);color:var(--sidebar-strong)}
.navlink.active{background:var(--sidebar-active-bg);color:var(--sidebar-active-fg)}
.navlink.active::before{content:"";position:absolute;left:-12px;top:4px;bottom:4px;width:3px;border-radius:0 3px 3px 0;background:var(--sidebar-indicator)}
.navlink .badge{margin-left:auto;font-family:"JetBrains Mono";font-size:11px;background:var(--sidebar-badge-bg);color:var(--sidebar-badge-fg);
  border-radius:20px;padding:1px 7px;opacity:0;transition:opacity .18s;font-weight:600}
.sidebar:hover .navlink .badge{opacity:1}
/* Nav/tab count badges are ALWAYS rendered and hidden at zero, so a refresh is a
   text change rather than an insertion that shifts the strip (see
   /js/nav-badges.js). `.pill` sets display:inline-flex, which would otherwise beat
   the UA `[hidden]{display:none}` rule — assert it here for both badge families. */
.pill[hidden], .badge[hidden], .info-btn[hidden]{display:none !important}
/* Same trap one level up: `.empty` sets display:grid, which beats the UA
   `[hidden]{display:none}` rule — an empty state toggled with the `hidden`
   attribute (the timeline/feed pattern) would stay on screen next to the
   content it says is missing. */
.empty[hidden]{display:none !important}

/* ============================ LAYOUT ============================ */
.main{margin-left:68px;margin-top:60px;min-height:calc(100vh - 60px)}
/* NO global width cap — an operator page fills whatever screen it is on. `.view`
   used to be max-width:1480px + centered, which is invisible on a 13" laptop
   (1512pt viewport − 68px rail = 1444px, already under the cap) but left ~500px
   of dead gutter on EACH side of a 27" display, on all 185 pages. The inventory
   pages had already opted out for exactly that reason. Do NOT reintroduce a
   global cap: a page that genuinely needs a reading width says so itself with
   `.view.narrow` below. `margin:0 auto` stays because `.narrow` needs it to
   center. (2026-09-20) */
.view{padding:15px 34px 60px;margin:0 auto;animation:fade .25s ease}
/* PROSE pages only (release notes, a settings sheet that is mostly text): cap
   the reading width so a paragraph doesn't run the full width of a 27" display.
   NOT for intake forms — trade-in / consignment / purchase all carried this and
   read as a narrow column in the middle of a wide screen; they are full-width
   `.view` like every other operator page since 2026-09-20. Before adding it to
   a page, ask "is this text to READ, or fields to FILL?" */
.view.narrow{max-width:1020px}
@keyframes fade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.page-head{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;margin-bottom:10px;flex-wrap:wrap}
.page-head h1{font-size:25px;font-weight:600;line-height:1.2}
.page-head .sub{color:var(--muted);font-size:13.5px;margin-top:4px}
/* The page-level action cluster (Back / Export / the one primary action) is
   pinned TOP-RIGHT, on the title's own line. `.page-head` used to be
   align-items:flex-end, which bottom-aligned this cluster to whatever the LEFT
   column grew into — so a page with a subtitle paragraph under its <h1> put the
   very same Back/Export buttons 20-40px lower than a page without one. That is
   the drift this pair of rules removes: flex-start puts the cluster at the top,
   and -4px centres the 38px control band on the ~30px h1 line box so it reads as
   sitting ON the title, not above it. */
.page-head > .actions{align-self:flex-start;margin-top:-4px;min-height:38px}
/* min-height reserves the button band even when every action is hidden: a tabbed
   page (Inventory) shows Export on some tabs only, and an empty cluster let the
   head shrink 34px → 30px, so the tab strip below jumped on every tab switch. */
/* A search box in the head (the dashboard quick find) stays a compact 240px beside the primary action; mobile.css widens it to the row. */
.page-head > .actions > .tb-input{max-width:240px}
/* …and the title block must be allowed to SHRINK. `.page-head` wraps, so a left
   block holding a long subtitle grew to its max-content width and bumped the
   action cluster onto its own line below the paragraph — the same Back/Export
   ending up a full row lower than on a page with no subtitle. flex:1 1 auto +
   min-width:0 makes the paragraph wrap inside the space left over instead.
   Scoped to a `div` first child so page-heads that lead with an inline element
   (a status pill, crm-lead-detail) are untouched. */
.page-head > div:first-child:not(.actions){flex:1 1 0;min-width:0}
.crumbs{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--muted);margin-bottom:9px;flex-wrap:wrap}
.crumbs a:hover{color:var(--accent)}
.crumbs svg,.crumbs i[data-lucide]{width:13px;height:13px}

/* ============================ COMPONENTS ============================ */
.btn{
  display:inline-flex;align-items:center;gap:8px;height:38px;padding:0 15px;border-radius:10px;
  font-family:inherit;font-size:13.5px;font-weight:600;cursor:pointer;border:1px solid var(--border);
  background:var(--surface);color:var(--text);transition:.14s;white-space:nowrap;
}
.btn svg,.btn i[data-lucide]{width:16px;height:16px}
.btn:hover{background:var(--surface-2);border-color:var(--border-strong)}
.btn.primary{background:linear-gradient(135deg, var(--accent), var(--accent-end, var(--accent-strong, var(--accent))));border-color:var(--accent);color:var(--on-brand);box-shadow:0 2px 6px color-mix(in srgb, var(--accent) 26%, transparent)}
.btn.primary:hover{filter:brightness(1.06)}
.btn.danger{background:var(--red-bg);border-color:transparent;color:var(--red)}
.btn.danger:hover{background:color-mix(in srgb,var(--red) 18%,transparent)}
/* Positive/affirmative soft-green (e.g. re-enable a disabled account) — mirrors .btn.danger */
.btn.success{background:var(--green-bg);border-color:transparent;color:var(--green)}
.btn.success:hover{background:color-mix(in srgb,var(--green) 18%,transparent)}
.btn.ghost{background:transparent;border-color:transparent;color:var(--text-2)}
.btn.ghost:hover{background:var(--surface-2);color:var(--text)}
/* Accent-TEXT button: the label (and its icon) carry the theme's primary color
   — Trimble Blue in light, amber in dark. TEXT only, never a fill, so `--accent`
   is the right token here (a colored FILL still needs `--on-brand`). Pairs with
   `ghost` for a low-emphasis action that should still read as the accent. */
.btn.accent,.btn.ghost.accent,.btn.ghost.accent:hover{color:var(--accent)}
.btn.sm{height:32px;padding:0 12px;font-size:12.5px;border-radius:9px}
/* A button the height of a line of field text (22px, like a .pill), for an action
   that sits among field values — the CRM Contact card's Circle field — where an
   .sm box would stand taller than the values in the fields beside it. */
.btn.xs{height:22px;padding:0 8px;gap:5px;font-size:12.5px;border-radius:7px}
.btn.xs svg,.btn.xs i[data-lucide]{width:13px;height:13px}
/* Icon-only button: the label lives in `title`/`aria-label` and shows on hover.
   For a DENSE action row where the words do not fit — the tray item header runs
   eight of these, and spelled out they wrapped onto a second line and pushed the
   item name out of view. Square, so a run of them reads as one strip of icons
   rather than a ragged row of pills. Never use it where there IS room: a lone
   button with space for its word should carry the word. The standing exceptions
   are the actions that repeat across pages and whose glyph IS the name: Export
   (file-spreadsheet), Pin (bookmark), Email-on-a-schedule (clock), Refresh,
   Packing slip (printer), Print label (tag), Labels (printer) and Activity log
   (history) are always icon-only, with `data-tip` + `aria-label`, wherever they
   appear. Back keeps its word. */
.btn.icon{position:relative;padding:0;width:38px;justify-content:center;gap:0}
.btn.icon.sm{width:32px}
/* The Circle's mark (public/circle/logo-mark.svg) as a button's icon, on every
   button that invites a customer to The Circle — a brand mark, not a Lucide
   glyph. Drawn through a mask so it takes the button's text colour like the
   icons beside it (--on-brand on a filled .btn.primary included). The ring sits
   inside more padding than a Lucide glyph, so it is drawn 2px larger and the
   negative margin gives the 2px back: it takes up exactly an icon's space. */
.circle-mark{display:inline-block;flex:none;width:18px;height:18px;margin:-1px;background:currentColor;
  -webkit-mask:url(/circle/logo-mark.svg) center/contain no-repeat;mask:url(/circle/logo-mark.svg) center/contain no-repeat}
.btn.xs .circle-mark{width:15px;height:15px}
/* A count riding the top-right corner of an icon button — the same idea as the
   topbar's .presence-count, in tokens that read on a page surface instead of the
   coloured topbar. --on-brand is the ONLY correct text token on an accent fill
   (white on blue in light, dark on amber in dark). No ring: the surface behind
   it changes on hover (--surface-2 → --surface-3 on a tray item header), and a
   ring pinned to one of them reads as a halo on the other.
   ALWAYS rendered and hidden at zero, so a first note is a text change rather
   than an insertion that resizes the button and shifts the row. */
.btn-count{
  position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;box-sizing:border-box;
  border-radius:999px;background:var(--accent);color:var(--on-brand);
  font-family:var(--font-mono);font-size:9.5px;font-weight:700;font-variant-numeric:tabular-nums;
  line-height:16px;text-align:center;pointer-events:none;
}
.btn-count[hidden]{display:none}
/* ===== The app-wide fast tooltip (/js/tooltip.js, `data-tip`) ==============
   The native `title` tooltip waits ~1s. On an icon-only control the label IS
   the button's name, so a second of hovering per icon is slower to read than
   the text the icon replaced. `data-tip` shows this bubble after 120ms.
   Use `data-tip` INSTEAD of `title` (both = two tooltips, one fast one slow)
   and pair it with `aria-label` for assistive tech. tooltip.js also upgrades a
   `title` it finds on any button/link — including ones rendered or re-labelled
   after load — so every control in the app resolves to THIS bubble. A `title`
   on a non-control (the full text of a truncated cell) is left native, and a
   DISABLED control is handed back to `title` because it fires no hover.
   ONE element, appended to <body> and positioned with fixed coords by the
   script — a `::after` on the control would be clipped by any overflow:hidden
   ancestor, and `.tray-item` (the densest icon row in the app) is one.
   z-index sits above modals (9000-9500) but below the toast region (99996). */
.tip{
  position:fixed;z-index:9800;display:none;max-width:280px;
  padding:5px 9px;border-radius:7px;
  background:var(--surface);color:var(--text);border:1.25px solid var(--border);
  font-family:var(--font-body);font-size:12px;font-weight:500;line-height:1.35;
  box-shadow:var(--shadow-md);pointer-events:none;white-space:pre-line;
}
.tip.open{display:block}
/* On touch the bubble opens on PRESS-AND-HOLD (tooltip.js): the browser's own long-press
   callout (copy / open link) would race it, so it is off on every labelled control. */
[data-tip]{-webkit-touch-callout:none}
/* A disabled button's badge must fade with it, or a greyed-out control keeps a
   full-strength accent dot that reads as live. */
.btn:disabled .btn-count,.btn[disabled] .btn-count{opacity:.45}
.btn:disabled,.btn[disabled],.btn:disabled:hover,.btn[disabled]:hover{background:var(--surface-2);border-color:var(--border);color:var(--text-faint);cursor:not-allowed;box-shadow:none;filter:none;opacity:1}

/* ---- Legacy button vocab → canonical theme-driven look. SINGLE source of
   truth that retires admin-buttons.css + the scattered per-page skins. Primary
   group (save/add) = accent gradient like .btn.primary; neutral group
   (secondary/run) = base surface; danger group (reset/err-del) = .btn.danger.
   Base layout sits in :where() (ZERO specificity) so a real base class (.btn,
   .small-btn) ALWAYS wins sizing — this fallback only shapes TRULY-standalone
   legacy buttons (no height, so it never leaks into .small-btn sizing). Colors
   use normal specificity and layer on whatever base. All theme-accent driven —
   NO hard-coded color — so buttons match in BOTH themes and everywhere. ---- */
:where(.primary-btn,.secondary-btn,.save-btn,.add-btn,.reset-btn,.run-btn,.err-del,.danger-btn){
  display:inline-flex;align-items:center;gap:8px;padding:9px 15px;border-radius:10px;
  font-family:inherit;font-size:13.5px;font-weight:600;line-height:1.1;cursor:pointer;
  border:1px solid var(--border);background:var(--surface);color:var(--text);
  transition:.14s;white-space:nowrap;
}
.primary-btn svg,.secondary-btn svg,.save-btn svg,.add-btn svg,.reset-btn svg,.run-btn svg,.err-del svg,.danger-btn svg,.primary-btn i[data-lucide],.secondary-btn i[data-lucide],.save-btn i[data-lucide],.add-btn i[data-lucide],.reset-btn i[data-lucide],.run-btn i[data-lucide],.err-del i[data-lucide],.danger-btn i[data-lucide]{width:16px;height:16px}
/* neutral hover — only the neutral group (primary/danger keep their own bg below) */
.secondary-btn:hover,.run-btn:hover{background:var(--surface-2);border-color:var(--border-strong)}
/* primary group = accent gradient (identical to .btn.primary; compound .btn.primary-btn included) */
.btn.primary-btn,.primary-btn,.save-btn,.add-btn{background:linear-gradient(135deg, var(--accent), var(--accent-end, var(--accent-strong, var(--accent))));border-color:var(--accent);color:var(--on-brand);box-shadow:0 2px 6px color-mix(in srgb, var(--accent) 26%, transparent)}
.btn.primary-btn:hover,.primary-btn:hover,.save-btn:hover,.add-btn:hover{filter:brightness(1.06)}
/* danger group = soft-red (identical to .btn.danger) */
.btn.danger-btn,.danger-btn,.reset-btn,.err-del{background:var(--red-bg);border-color:transparent;color:var(--red)}
.btn.danger-btn:hover,.danger-btn:hover,.reset-btn:hover,.err-del:hover{background:color-mix(in srgb,var(--red) 18%,transparent)}
/* disabled parity */
.primary-btn:disabled,.secondary-btn:disabled,.save-btn:disabled,.add-btn:disabled,.reset-btn:disabled,.run-btn:disabled,.err-del:disabled,.danger-btn:disabled,.primary-btn[disabled],.save-btn[disabled],.add-btn[disabled]{background:var(--surface-2);border-color:var(--border);color:var(--text-faint);cursor:not-allowed;box-shadow:none;filter:none;opacity:1}

.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);padding:0}
/* Side-by-side cards that stack on a phone: each keeps a preferred width but never
   exceeds the screen (an inline min-width:400px did, on Marketing — 2026-09-26).
   `.wide` / `.narrow` set the share and preferred width of a card in the row. */
.card-row{display:flex;gap:16px;flex-wrap:wrap;align-items:stretch;margin-bottom:16px}
.card-row>.card{flex:1 1 400px;min-width:min(400px,100%);display:flex;flex-direction:column}
.card-row>.card.wide{flex:2 1 380px;min-width:min(380px,100%)}
.card-row>.card.narrow{flex:1 1 300px;min-width:min(300px,100%)}
.pad{padding:20px}
.card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 20px;border-bottom:1px solid var(--border)}
/* Inside an already-padded card (.card.pad) the head must not double-pad: sit
   flush with the card's own padding and put breathing room under the divider,
   so the rule line never touches the content that follows. */
.pad > .card-head{padding:0 0 10px;margin:0 0 14px}
.card-head h3{font-size:15px;font-weight:600}
.card-head .cap{font-size:12px;color:var(--muted)}

.pill{display:inline-flex;align-items:center;gap:5px;height:22px;padding:0 9px;border-radius:20px;font-size:11.5px;font-weight:600;line-height:1;white-space:nowrap;vertical-align:middle}
.pill .pd{width:6px;height:6px;border-radius:50%;background:currentColor}
.pill svg,.pill i[data-lucide]{width:13px;height:13px;flex-shrink:0}
/* A pill that fits a small caption line — a status beside a field label (the CRM
   Contact card's "The Circle ?") without making that line taller than its neighbours'. */
.pill.xs{height:18px;padding:0 7px;gap:4px;font-size:10.5px}
.pill.xs .pd{width:5px;height:5px}
.pill.green{color:var(--green);background:var(--green-bg)}
.pill.blue{color:var(--blue);background:var(--blue-bg)}
.pill.amber{color:var(--amber);background:var(--amber-bg)}
.pill.red{color:var(--red);background:var(--red-bg)}
.pill.gray{color:var(--gray);background:var(--gray-bg)}
.pill.violet{color:var(--violet);background:var(--violet-bg)}
.pill.magenta{color:var(--magenta);background:var(--magenta-bg)}
/* Two pills that qualify ONE thing (a status + "3/5 on tray") stack instead of
   sitting side by side: on one line the cell is as wide as both pills together,
   which is what made the purchases Progress column the widest on the page. */
.pill-stack{display:inline-flex;flex-direction:column;align-items:flex-start;gap:4px}

.grid{display:grid;gap:18px}
/* Auto-fitting grid of wide content cards (settings/config panels that hold a
   form, not a number). Wraps to one column below ~360px of available width —
   use instead of .field-grid, whose 190px track is sized for single inputs. */
.grid.cards-wide{grid-template-columns:repeat(auto-fit,minmax(360px,1fr))}
.kpis{grid-template-columns:repeat(4,1fr)}
.kpi{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px 18px 12px;box-shadow:var(--shadow);position:relative;overflow:hidden}
.kpi .lbl{font-size:12.5px;color:var(--muted);display:flex;align-items:center;gap:7px;font-weight:500}
.kpi .lbl svg,.kpi .lbl i[data-lucide]{width:15px;height:15px}
/* line-height 1.1 — a display number carries its own leading; inheriting the
   body's 1.5 put 15px of dead space around every KPI value and made the tile
   12px taller than the same tile on Inventory (.inv-kpi-value, which has always
   been 1.1). Keep the two in step. */
.kpi .val{font-family:"Space Grotesk";font-size:30px;font-weight:600;margin-top:9px;letter-spacing:-.02em;line-height:1.1}
/* The sub-line under a KPI number carries more than one fact on several tiles
   ("+57% · 6d" PLUS "oldest 77d"; "3 critical" PLUS "1 warning"), and as one
   nowrap line it ran off the tile / under the sparkline. It is a WRAPPING box:
   the headline stays on row 1, `.delta-note` (the trailing detail) always takes
   row 2, indented to the icon's width so it lines up under the headline text.
   align-content:flex-start keeps row 1 at the same y whether or not row 2 is
   used, so tiles read level. */
.kpi .delta{display:flex;flex-wrap:wrap;align-items:center;align-content:flex-start;column-gap:3px;row-gap:0;font-size:12px;font-weight:600;margin-top:5px;line-height:18px}
.kpi .delta svg,.kpi .delta i[data-lucide]{width:13px;height:13px}
.kpi .delta .delta-note{flex:0 0 100%;padding-left:16px}
.up{color:var(--green)} .down{color:var(--red)}
/* The decorative trend line behind a KPI tile's number. A PERCENTAGE width (not
   a fixed 120px, which hugged the right edge and left the tile's whole left half
   empty) so it grows with the tile / grid track.
   THE TRAP: do NOT size it with left+right offsets. An <svg> is a REPLACED
   element, so with width:auto it takes its INTRINSIC width from the viewBox
   ratio (120/46 x height:46px = 120px) and the over-constrained `right` is then
   dropped — the box stayed 120px, slid inward to the `left` offset and sat on
   top of the number with dead space to its right. Only an explicit width works.
   ONE rule for both tile flavours: the dashboard .kpi and Inventory's .inv-kpi
   (inventory.css used to carry an identical copy) — mirrors, keep them in step. */
.kpi .spark,.inv-kpi .spark{position:absolute;right:0;bottom:0;width:60%;height:46px;opacity:.9;pointer-events:none;z-index:0}
/* Text above the line where they overlap — a wider spark now runs under the
   value, and an absolutely-positioned sibling otherwise paints over inline text
   whatever its z-index. (.inv-kpi does the same for its own three lines.) */
.kpi .lbl,.kpi .val,.kpi .delta{position:relative;z-index:1}

/* Clickable KPI tile that FILTERS the list below it (drift errors, and any
   future summary strip whose tiles narrow the table under them). The tile is a
   real <button>, so it keeps keyboard focus + Enter/Space for free; these rules
   only strip the button chrome back to the .kpi box and add the affordance.
   `.is-active` marks the tile whose filter is currently in force — it must
   always agree with the filter bar, never be a second source of truth. */
.kpi.kpi-filter{
  cursor:pointer;transition:.16s;text-align:left;width:100%;font:inherit;color:inherit;
  appearance:none;-webkit-appearance:none;display:block;
}
.kpi.kpi-filter:hover{border-color:var(--accent);transform:translateY(-1px);box-shadow:var(--shadow-md)}
.kpi.kpi-filter:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.kpi.kpi-filter.is-active{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
.kpi.kpi-filter.is-active .lbl{color:var(--text)}

.tiles{grid-template-columns:repeat(4,1fr)}
.tile{
  background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px;
  box-shadow:var(--shadow);cursor:pointer;transition:.16s;display:flex;flex-direction:column;gap:12px;min-height:128px;
}
.tile:hover{transform:translateY(-3px);box-shadow:var(--shadow-lg);border-color:var(--border-strong)}
.tile .ic{width:42px;height:42px;border-radius:11px;display:grid;place-items:center;background:var(--accent-soft);color:var(--accent)}
.tile .ic svg,.tile .ic i[data-lucide]{width:21px;height:21px}
.tile h3{font-size:15px;font-weight:600}
.tile p{font-size:12.5px;color:var(--muted);margin:2px 0 0;line-height:1.45}
.tile .tcount{margin-left:auto;font-family:"JetBrains Mono";font-size:11.5px;color:var(--text-2);background:var(--surface-2);border:1px solid var(--border);border-radius:7px;padding:2px 8px}
.tile .trow{display:flex;align-items:center;gap:10px}

/* table */
.tablewrap{overflow:auto;border-radius:var(--radius);background:var(--surface);border:1px solid var(--border);box-shadow:var(--shadow)}
/* Bounded list: keeps a card a predictable height while every row stays
   reachable by scrolling (paired columns that must not out-grow each other). */
.tablewrap.scroll-y{max-height:340px;overflow-y:auto}
.tablewrap.scroll-y thead th{position:sticky;top:0;z-index:1;background:var(--surface-2)}
.card .tablewrap{background:transparent;border:0;box-shadow:none;border-radius:0}
/* Full-bleed tablewrap flush to a card edge must round to match the card, so its
   overflow:auto clips the sticky header / last row instead of poking square corners
   past the card's rounded corners. Interior tablewraps (mid-card) stay square.
   mobile-tables.js puts its phone sort bar (.mtbl-sort, hidden on a desktop) in
   FRONT of every 4+ column table, so a leading one doesn't count as content. */
.card>.tablewrap:first-child,.card>.mtbl-sort:first-child+.tablewrap{border-top-left-radius:var(--radius);border-top-right-radius:var(--radius)}
.card>.tablewrap:last-child{border-bottom-left-radius:var(--radius);border-bottom-right-radius:var(--radius)}
table{width:100%;border-collapse:collapse;font-size:13px}
thead th{
  text-align:left;font-weight:600;color:var(--th-fg);font-size:11.5px;letter-spacing:.03em;text-transform:uppercase;
  padding:11px 16px;background:var(--th-bg);
  border-bottom:1px solid color-mix(in srgb,var(--th-bg) 75%,#000);position:sticky;top:0;white-space:nowrap;
}
tbody td{padding:12px 16px;border-bottom:1px solid var(--border);white-space:nowrap}
tbody tr{cursor:pointer;transition:background .12s}
tbody tr:hover{background:var(--surface-2)}
tbody tr:last-child td{border-bottom:none}
.t-strong{font-weight:600}
.t-mut{color:var(--muted)}
/* An icon leading a muted note (`<p class="t-mut"><i data-lucide="info"></i> …`) sits in running text —
   Lucide draws every icon at 24px, so size it to the line instead of letting it tower over the words. */
p.t-mut > svg.lucide,p.t-mut > i[data-lucide]{width:1em;height:1em;vertical-align:-.15em}
.cellflex{display:flex;align-items:center;gap:10px}
/* The row-selection column of a table with bulk actions: a narrow cell holding one checkbox. */
.table .pick{width:36px;padding-right:0}
.table .pick input[type=checkbox]{accent-color:var(--accent);margin:0;cursor:pointer;vertical-align:middle}
/* A cell that belongs WITH the column beside it — a status pill and the button that
   acts on it, a value and its unit. The two 16px gutters between the columns add up
   to a 32px hole that reads as two unrelated groups, so the trailing one is dropped
   and the pair reads as one cluster (same idea as .pick above). Put it on the <th>
   too, or the header and its column no longer line up. */
.table td.joins-next,.table th.joins-next{padding-right:0}
.thumb{width:34px;height:34px;border-radius:8px;display:grid;place-items:center;background:var(--surface-2);border:1px solid var(--border);color:var(--text-2);flex-shrink:0}
.thumb svg,.thumb i[data-lucide]{width:17px;height:17px}

.toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:16px}
/* A toolbar sitting directly under a card header needs space from the divider. */
.card-head + .toolbar{margin-top:16px}
/* An unpadded card's toolbar lines up with its .card-head (20px sides) instead
   of pressing its first control against the card border. A .card.pad already
   supplies that inset, so it is left alone. */
.card:not(.pad) > .toolbar{padding-inline:20px}
/* Expandable / drill-down table rows: click a row to reveal a detail sub-row. */
.table tr.drill-row{cursor:pointer}
.table tr.drill-row .drill-caret{width:14px;height:14px;vertical-align:middle;color:var(--muted);transition:transform .18s ease}
.table tr.drill-row:hover .drill-caret{color:var(--text)}
.table tr.drill-row.open .drill-caret{transform:rotate(90deg);color:var(--accent)}
.table tr.drill-detail>td{padding:0}
.table tr.drill-detail .drill-body{padding:2px 4px 12px;animation:drillIn .18s ease}
@keyframes drillIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
/* Small thumbnail that shows a large floating preview on hover (JS positions it). */
.thumb-zoom{width:36px;height:36px;object-fit:cover;border-radius:6px;border:1px solid var(--border);background:var(--surface-2);cursor:zoom-in;display:block}
.thumb-zoom-empty{display:inline-flex;align-items:center;justify-content:center;color:var(--muted)}
.thumb-zoom-preview{position:fixed;display:none;max-width:320px;max-height:320px;border-radius:10px;border:2px solid var(--surface);box-shadow:0 10px 34px rgba(0,0,0,.4);z-index:9999;pointer-events:none;background:var(--surface)}
/* Whole-row link: the entire <tr> navigates to its data-href on click
   (the row still carries a real <a> in its first cell for keyboard/middle-click). */
tr.row-link{cursor:pointer}
/* Sub-row: a row that belongs to the row above it (the items a payment bought,
   listed under it). /js/ui-table.js keeps it with its parent through sorting and
   paging (data-child-row); this is what makes it READ as belonging there — a
   tinted band, a left rule, and the first cell stepped in under its parent's. */
.table tr.subrow>td{background:var(--surface-2);border-bottom-color:var(--border-light);font-size:12.5px}
.table tr.subrow:hover>td{background:var(--surface-3)}
.table tr.subrow>td:first-child{padding-left:38px;box-shadow:inset 3px 0 0 var(--border)}
/* The LAST sub-row of a group closes it off with the normal row border, so the
   group reads as one block rather than fading into the payment below it. */
.table tr.subrow:not(:has(+ tr.subrow))>td{border-bottom-color:var(--border)}
/* The photo column: a fixed, narrow cell so a row with no photo holds the same
   box as one with three, and the columns beside it never shift. */
.table td.thumbs,.table th.thumbs{width:1%;white-space:nowrap}
/* --- col-min: keep a column OUT of the spare-width sharing ---
   `table{width:100%}` hands a wide screen's slack to every auto column alike, so
   a column of short values (a date, a method, a check number, a row's buttons)
   grows just as much as the column of text that could actually use the room.
   `width:1%` asks for the least the content allows, so that column stays put and
   the slack goes to the columns that have something to show. Put it on the <th>
   AND the <td> — a header-only rule is overruled by the widest cell.
   It cannot help a table that already OVERFLOWS: with no slack to redirect,
   the levers there are `.table.compact` and capping the runaway columns. */
.table th.col-min,.table td.col-min{width:1%;white-space:nowrap}
/* --- col-tight: a column whose values are SHORT (a method, a check number) ---
   Such a column is sized by its HEADER plus the cell chrome, never by the data:
   a letter-spaced uppercase label, a 14px sort arrow and 16px of padding either
   side dwarf a value like `9001`. `col-min` cannot help once a table has no
   slack left to redirect. This gives back half the side padding, which
   is the only width left to reclaim. Use it only where the values really are
   short; a column of real text needs its air. */
.table th.col-tight,.table td.col-tight{padding-left:8px;padding-right:8px}
/* --- col-cap: a CEILING for a column of free text ---
   The other half of the overflow toolkit: `col-min` keeps a SHORT column out of
   the spare-width sharing, this one stops a LONG one (a name, a store, a
   description) from sizing the whole table off the longest value in it. Put it
   on the <td> with `<span class="cell-ellip fill">` around the value and the
   full text in the cell's title: the column stops at the cap, the text fills
   whatever width the column actually gets, and the … appears only when the
   value really doesn't fit — so a wide monitor opens the column back out
   instead of showing a truncated value next to empty space.
   Not on the <th>: the cap can be narrower than the header (`Entered by` on a
   laptop), and a header has no ellipsis to fall back on. The widest CELL is
   what sizes a column anyway, so the <td> alone does the job.
   The caps follow the viewport, like `.cell-ellip.fluid`; `sm` is for a short
   label column (a store, a person), the base one for the table's text column. */
.table td.col-cap{max-width:clamp(7rem,11vw,18rem)}
.table td.col-cap-sm{max-width:clamp(4.5rem,7vw,11rem)}
/* --- .table.compact: a denser table, for one with many columns ---
   The 16px of side padding on every cell is right for a table of five or six
   columns and is what pushes a nine-column one into a horizontal scroll: it
   spends ~100px on air alone. This takes back 6px a side, everywhere, so the
   row stays evenly spaced — unlike tightening single columns, which leaves
   gaps of different widths down the row. Reach for it before capping columns
   so hard that their text starts disappearing. */
.table.compact thead th,.table.compact tbody td{padding-left:10px;padding-right:10px}
/* …but the padding EXCEPTIONS still apply: a cell that joins the one beside it
   keeps no right gutter, and the checkbox column stays flush. Same specificity
   as the rule above, so they have to come after it — left before it, `compact`
   silently handed .joins-next its 10px back and the pair drifted apart again. */
.table.compact td.joins-next,.table.compact th.joins-next{padding-right:0}
.table.compact .pick{padding-right:0}
.table td.thumbs .cellflex{gap:4px}
.table td.thumbs .thumb-zoom{width:32px;height:32px}
.table td.thumbs .thumb{width:32px;height:32px}
/* A drill-detail item row that links somewhere (e.g. the product page) on click. */
.table tr.dq-item{cursor:pointer}
.table tr.dq-item:hover{background:var(--surface-2)}
/* Attention rows: tint a whole table row to flag it (e.g. an overdue shipment).
   Left accent bar + tinted cells; the hover tint is intentionally kept so the
   flag survives mouse-over instead of reverting to the neutral row hover. */
.table tr.row-danger>td{background:var(--red-bg)}
.table tr.row-danger:hover>td{background:var(--red-bg)}
.table tr.row-danger>td:first-child{box-shadow:inset 3px 0 0 var(--red)}
.table tr.row-warn>td{background:var(--amber-bg)}
.table tr.row-warn:hover>td{background:var(--amber-bg)}
.table tr.row-warn>td:first-child{box-shadow:inset 3px 0 0 var(--amber)}
.tb-input{position:relative;flex:1;min-width:220px;max-width:340px}
.tb-input input{width:100%;height:36px;border-radius:9px;border:1px solid var(--border);background:var(--surface);padding:0 12px 0 34px;color:var(--text);outline:none;font-family:inherit;font-size:13px}
.tb-input input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.tb-input svg,.tb-input i[data-lucide]{position:absolute;left:11px;top:50%;transform:translateY(-50%);width:15px;height:15px;color:var(--muted)}
/* Wide search box — for a bar whose search is the main way in (names, SKUs and
   titles in one box). It takes most of the row's spare room, up to 560px, instead
   of splitting it 1:1 with .spacer. The 220px basis equals the base min-width, so
   the bar wraps at exactly the same width as with a plain .tb-input; only spare
   room moves. Use this, not an inline max-width. */
.tb-input.wide{flex:6 1 220px;max-width:560px}
.chip{display:inline-flex;align-items:center;gap:6px;height:36px;padding:0 12px;border-radius:9px;border:1px solid var(--border);background:var(--surface);color:var(--text-2);font-size:13px;font-weight:500;cursor:pointer}
.chip svg,.chip i[data-lucide]{width:14px;height:14px}
.chip:hover{border-color:var(--border-strong);color:var(--text)}
/* Selected state for chips used as view filters (e.g. the 360 timeline kinds).
   Accent-soft fill + accent text so "which filter is on" reads at a glance in
   both themes — never hard-code a colour on a chip per page. */
.chip.is-on{background:var(--accent-soft);border-color:var(--accent-border);color:var(--accent);font-weight:600}
.chip.sm{height:28px;padding:0 10px;font-size:12px;border-radius:8px}
/* ── Social post preview (marketing → social composer) ──────────────────────
   A neutral mock of how a post renders on the picked platform: account header,
   media, caption, link card. Token-based so it reads correctly in both themes;
   deliberately platform-neutral chrome (we preview CONTENT, not Facebook's UI). */
.post-preview{border:1px solid var(--border);border-radius:14px;background:var(--surface);overflow:hidden;max-width:420px}
.post-preview .pp-head{display:flex;align-items:center;gap:9px;padding:10px 13px;border-bottom:1px solid var(--border)}
.post-preview .pp-avatar{width:30px;height:30px;border-radius:50%;background:var(--accent-soft);color:var(--accent);display:grid;place-items:center;flex-shrink:0}
.post-preview .pp-avatar svg,.post-preview .pp-avatar i[data-lucide]{width:15px;height:15px}
.post-preview .pp-who{min-width:0}
.post-preview .pp-name{font-size:13px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.post-preview .pp-sub{font-size:11px;color:var(--muted)}
.post-preview .pp-img{display:block;width:100%;aspect-ratio:1/1;object-fit:cover;background:var(--surface-2)}
.post-preview .pp-img.tall{aspect-ratio:3/4}
.post-preview .pp-noimg{display:grid;place-items:center;aspect-ratio:1/1;background:var(--surface-2);color:var(--muted);font-size:12.5px}
.post-preview .pp-text{padding:11px 13px;font-size:13.5px;line-height:1.5;color:var(--text);white-space:pre-line;overflow-wrap:anywhere}
.post-preview .pp-text .pp-link{color:var(--accent);word-break:break-all}
.post-preview .pp-linkcard{margin:0 13px 12px;border:1px solid var(--border);border-radius:10px;overflow:hidden}
.post-preview .pp-linkcard img{display:block;width:100%;aspect-ratio:2/1;object-fit:cover;background:var(--surface-2)}
.post-preview .pp-linkcard .pp-lc-meta{padding:7px 11px;background:var(--surface-2)}
.post-preview .pp-linkcard .pp-lc-domain{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)}
.post-preview .pp-linkcard .pp-lc-title{font-size:12.5px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.post-preview .pp-warn{margin:0 13px 12px;padding:7px 11px;border-radius:9px;background:var(--amber-bg);color:var(--amber);font-size:12px}
.post-preview .pp-count{padding:0 13px 11px;font-size:11.5px;color:var(--muted);text-align:right}
.post-preview .pp-count.over{color:var(--red);font-weight:600}

/* ── Multi-photo previews — each platform shows a photo set its OWN way ──────
   Instagram/Pinterest swipe through one photo at a time with dots and a counter;
   Facebook lays a photo set out as a collage. Showing the real shape (rather
   than a "+2 more" line) is the point of the preview. */
.post-preview .pp-carousel{position:relative}
.post-preview .pp-counter{position:absolute;top:9px;right:9px;padding:2px 8px;border-radius:20px;
  background:rgba(0,0,0,.62);color:#fff;font-size:11px;font-weight:600;font-family:"JetBrains Mono";letter-spacing:.02em}
.post-preview .pp-nav{position:absolute;top:50%;transform:translateY(-50%);width:26px;height:26px;
  display:grid;place-items:center;border:none;border-radius:50%;cursor:pointer;padding:0;
  background:rgba(255,255,255,.9);color:#1b1b1b;box-shadow:0 1px 4px rgba(0,0,0,.3)}
.post-preview .pp-nav:hover{background:#fff}
.post-preview .pp-nav.prev{left:8px}
.post-preview .pp-nav.next{right:8px}
.post-preview .pp-nav svg,.post-preview .pp-nav i[data-lucide]{width:15px;height:15px}
.post-preview .pp-dots{display:flex;justify-content:center;gap:5px;padding:9px 0 3px}
.post-preview .pp-dot{width:6px;height:6px;border-radius:50%;background:var(--border-strong);
  border:none;padding:0;cursor:pointer;transition:background .12s ease}
.post-preview .pp-dot.is-on{background:var(--accent)}
/* Facebook: a photo set is ONE story with a collage, not N posts. */
.post-preview .pp-collage{display:grid;gap:2px;background:var(--border)}
.post-preview .pp-collage img{display:block;width:100%;height:100%;object-fit:cover;background:var(--surface-2)}
.post-preview .pp-collage.n2{grid-template-columns:1fr 1fr;aspect-ratio:2/1}
.post-preview .pp-collage.n3{grid-template-columns:1fr 1fr;aspect-ratio:1/1}
.post-preview .pp-collage.n3 > :first-child{grid-column:1 / -1}
.post-preview .pp-collage.n4{grid-template-columns:1fr 1fr;aspect-ratio:1/1}
.post-preview .pp-more{position:relative}
.post-preview .pp-more::after{content:attr(data-more);position:absolute;inset:0;display:grid;place-items:center;
  background:rgba(0,0,0,.55);color:#fff;font-size:19px;font-weight:600;font-family:var(--font-display)}

/* A .chip wrapping a CHECKBOX (multi-select chip row, e.g. the social platform
   picker): the box takes the accent, a checked chip lights up like .is-on, a
   disabled one greys out — no per-page styling needed. */
.chip input[type=checkbox]{accent-color:var(--accent);margin:0;cursor:inherit}

/* --- a .chip carrying its own caption + control, so a filter row stays ONE 36px
   band instead of stacking a .field-label above every input. The control sits
   borderless inside the pill; the caption is muted and the VALUE is the solid
   text, so the row still says what each control is. Replaces the per-page
   `style="border:none;background:transparent…"` copies. --- */
.chip:has(>select),.chip:has(>input:not([type=checkbox])){cursor:default;color:var(--muted);gap:7px}
.chip>select,.chip>input:not([type=checkbox]):not([type=radio]){
  border:0;background:transparent;color:var(--text);font:inherit;font-size:13px;font-weight:500;
  padding:0;margin:0;outline:none;height:auto;width:auto;min-width:0;max-width:100%}
.chip>select{cursor:pointer;--select-pad-end:18px;--select-chevron-x:0px}
.chip:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}

/* --- a from–to pair reading as ONE control. They stay two inputs because they
   are two query params; only the chrome is shared. --- */
.chip.range>svg{color:var(--muted);flex:0 0 auto}
.chip.range>input{width:116px}
.chip.range>input[type=number]{width:62px}
.range-sep{color:var(--muted);font-size:13px}

/* --- ONE calendar glyph per date FIELD, and it is the browser's own picker
   button (app-wide). A native date input already paints a working calendar on
   each field, so a leading Lucide calendar on the chip made three glyphs for one
   control — and the themed one, the only one that looked like the app, opened
   nothing. The chip's icon is gone from every date range; each field keeps its
   own. NO width override either: a date input's automatic width is exactly its
   text + that button, so `auto` (from the .chip>input rule above) is the
   narrowest the control can be — a fixed px width only ever padded it out.
   /js/date-input.js (app-wide, from partials/icons) adds the rest: a first click
   anywhere in the field opens the picker, a second click types. --- */
.chip>input[type=date],.chip>input[type=datetime-local],.chip>input[type=month]{cursor:pointer}
.chip>input:focus{cursor:text}


/* --- a bare .input sitting directly on a toolbar row: auto width, rather than
   the 100% it gets inside a .field --- */
.tb-field{width:auto;min-width:130px;max-width:220px}
select.tb-field{min-width:0}

/* --- a toolbar's actions travel as ONE unit, so a narrow viewport can never
   strand the primary button on its own line --- */
.tb-actions{display:inline-flex;align-items:center;gap:8px;flex-wrap:nowrap}
/* --- actions on the rows ticked in the table below: shown only while something is
   selected, and sat right after the filters (no margin-left:auto, unlike .tb-actions).
   [hidden] is re-asserted because the display rule would otherwise beat it --- */
.tb-bulk{display:inline-flex;align-items:center;gap:8px;flex-wrap:nowrap}
.tb-bulk[hidden]{display:none}
.chip:has(input[type=checkbox]:checked){background:var(--accent-soft);border-color:var(--accent-border);color:var(--accent);font-weight:600}
.chip:has(input[type=checkbox]:disabled){opacity:.55;cursor:not-allowed}
/* --- a WRAPPING row of chips (multi-select pickers: rep pools, platform
   pickers, filter sets). The chips already carry their own chrome; this is
   only the flow container, so a picker never needs a per-page grid. --- */
.chip-row{display:flex;flex-wrap:wrap;gap:8px}
/* …and a picker whose options are a long list of LIKE items (a rep pool, a
   user list) reads faster as aligned columns than as a ragged wrap: add a
   .cols-N modifier from the shared grid-column family. Chips then stretch to
   the column, so a long name ellipsises instead of blowing the 36px row. */
.chip-row.cols-2,.chip-row.cols-3,.chip-row.cols-4{display:grid}
.chip-row>.chip{min-width:0}
.chip-row>.chip>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.spacer{flex:1}

.avatar-sm{width:26px;height:26px;border-radius:50%;display:grid;place-items:center;font-size:10.5px;font-weight:600;color:#fff;font-family:"Space Grotesk";flex-shrink:0}

/* segmented / subnav */
.subnav{display:flex;flex-direction:column;gap:2px}
.subnav a{display:flex;align-items:center;gap:11px;height:38px;padding:0 12px;border-radius:9px;color:var(--text-2);font-size:13.5px;font-weight:500;cursor:pointer}
.subnav a svg,.subnav a i[data-lucide]{width:17px;height:17px}
.subnav a:hover{background:var(--surface-2);color:var(--text)}
.subnav a.active{background:var(--accent-soft);color:var(--accent);font-weight:600}

/* The admin sub-rail was removed: it was a second navigation surface one level
   below x-sidebar, which is the very thing the tabs rule exists to prevent. The
   /admin hub is the index, every admin page carries a Back, and ⌘K searches every
   page and setting from anywhere. Its zone-header and search-trigger styles went
   with it; the zone bands below are the HUB's, and the ⌘K dialog's styles live
   further down (the trigger moved, the dialog did not). */
/* Hub counterpart: the zone band above each zone's chapters on /admin. */
.admin-zone-head{
  display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
  margin:2.4rem 0 1.1rem;padding-bottom:.5rem;border-bottom:2px solid var(--accent);
  font-family:var(--font-display,inherit);font-size:1.05rem;font-weight:600;letter-spacing:-.02em;color:var(--text);
}
.admin-zone-head:first-child{margin-top:0}
.admin-zone-head i,.admin-zone-head svg{width:18px;height:18px;color:var(--accent);align-self:center}
.admin-zone-head .admin-zone-desc{font-family:var(--font-body,inherit);font-size:.82rem;font-weight:400;letter-spacing:0;color:var(--text-muted)}

/* ── Setup progress on the Admin hub ─────────────────────────────────────────
   A state, not an alarm: "Not set up" is the normal condition of a chapter you
   have not reached yet, so it is neutral, and only the DETAIL line (what to do
   next) carries any colour. A check that could not run renders neither, because
   a wrong "not done" would send someone to redo finished work. */
.setup-chip{
  display:inline-flex;align-items:center;gap:5px;margin-left:10px;
  font-family:var(--font-body,inherit);font-size:.7rem;font-weight:600;letter-spacing:.02em;
  text-transform:none;padding:2px 8px;border-radius:999px;
}
.setup-chip i,.setup-chip svg{width:12px;height:12px}
.setup-chip.done{background:var(--pill-green-bg);color:var(--pill-green-text)}
.setup-chip.todo{background:var(--surface-3);color:var(--text-muted)}
.setup-detail{
  display:flex;align-items:flex-start;gap:7px;margin:-6px 0 12px;
  font-size:.85rem;color:var(--pill-amber-text);background:var(--pill-amber-bg);
  border-radius:8px;padding:8px 11px;max-width:70ch;
}
/* On a chapter that IS done, the line is drift worth knowing about, not a task. */
.setup-detail.is-note{color:var(--text-2);background:var(--surface-2)}
.setup-detail i,.setup-detail svg{width:14px;height:14px;flex:none;margin-top:2px}

/* ── Admin search (⌘K) ───────────────────────────────────────────────────────
   The rail trigger reads as a search FIELD, not a nav link, so it isn't mistaken
   for a destination — and it carries the shortcut, because a shortcut nobody can
   see is a shortcut nobody uses. */
/* Results live in the shared .x-modal chrome; only the list is bespoke. */
.x-modal.adm-search{width:min(620px,94vw)}
.adm-search .adm-search-status{margin:.6rem 0 0;font-size:.85rem}
.adm-search-list{list-style:none;margin:.6rem 0 0;padding:0;max-height:min(52vh,420px);overflow-y:auto;display:flex;flex-direction:column;gap:2px}
.adm-search-list li a{display:flex;align-items:flex-start;gap:11px;padding:9px 10px;border-radius:9px;color:var(--text)}
.adm-search-list li.active a,.adm-search-list li a:hover{background:var(--accent-soft);color:var(--accent)}
.adm-search-ic{display:flex;align-items:center;justify-content:center;width:26px;height:26px;flex:none;border-radius:7px;background:var(--surface-2);color:var(--text-muted)}
.adm-search-list li.active .adm-search-ic{background:var(--surface);color:var(--accent)}
.adm-search-ic svg,.adm-search-ic i[data-lucide]{width:15px;height:15px}
.adm-search-txt{display:flex;flex-direction:column;gap:2px;min-width:0}
.adm-search-label{display:flex;align-items:center;gap:7px;font-weight:600;font-size:13.5px}
.adm-search-kind{text-transform:none;letter-spacing:0;font-size:10px}
.adm-search-ctx{font-size:11.5px;color:var(--text-muted);overflow-wrap:anywhere}
.adm-search-list li.active .adm-search-ctx{color:var(--accent)}
/* Sub-rail layout. The content track is minmax(0,1fr) — NOT 1fr — and its items
   get min-width:0, so a wide table/chart stays inside its own .tablewrap scroll
   instead of stretching the grid track (and the whole page) out to the table's
   min-content width. With plain 1fr the track's automatic minimum is the content's
   min-content size, and a nowrap table blows the page several screens wide. */
.split{display:grid;grid-template-columns:186px minmax(0,1fr);gap:22px;align-items:start}
.split>*{min-width:0}
/* The admin sub-rail is gone: x-sidebar is the one navigation surface, the /admin
   hub is the index, every page carries a Back, and ⌘K jumps anywhere. So a
   `.split` normally holds ONE child now — span it across both tracks rather than
   leaving the content squeezed into the 186px rail column. Self-adjusting: a
   second child restores the two-column layout with no edit here. */
.split>:only-child{grid-column:1/-1}

/* Tabs — also the SECTION NAV for a module (CRM, Inventory). A module's sections
   are a horizontal strip under the page title, never a second vertical rail: the
   left rail is the one navigation surface, and a sub-rail duplicated it one level
   down while eating 208px of every content area. Tabs carry a Lucide icon and an
   optional .pill count, so a section rail converts across with no bespoke CSS. */
/* The strip scrolls sideways rather than overflowing the page — inventory's 9
   sections don't fit a laptop width. The 1px rule is an INSET SHADOW, not a
   border-bottom, and the active tab's 2px underline sits inside the box (no
   `margin-bottom:-1px`): a negative margin would be clipped by the scroll
   container. Renders identically to the old border+overhang. */
.tabs{display:flex;gap:3px;box-shadow:inset 0 -1px 0 var(--border);margin-bottom:20px;overflow-x:auto;overflow-y:hidden;overscroll-behavior-x:contain;scrollbar-width:none}
.tabs::-webkit-scrollbar{height:0}
/* No tab strip shows a scrollbar (.tabs here, the tray/review panel .ptabs in
   edit-tray.css), so the edge that has more tabs behind it FADES instead, at
   every width. /js/mobile-chrome.js (app-wide) sets .tabs-more-right /
   .tabs-more-left from the strip's scroll position. */
.tabs.tabs-more-right,.ptabs.tabs-more-right{-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 44px),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 44px),transparent)}
.tabs.tabs-more-left:not(.tabs-more-right),.ptabs.tabs-more-left:not(.tabs-more-right){-webkit-mask-image:linear-gradient(to right,transparent,#000 36px);mask-image:linear-gradient(to right,transparent,#000 36px)}
.tabs.tabs-more-left.tabs-more-right,.ptabs.tabs-more-left.tabs-more-right{-webkit-mask-image:linear-gradient(to right,transparent,#000 36px,#000 calc(100% - 44px),transparent);mask-image:linear-gradient(to right,transparent,#000 36px,#000 calc(100% - 44px),transparent)}
.tabs a{display:inline-flex;align-items:center;gap:8px;white-space:nowrap;flex-shrink:0;padding:10px 14px;font-size:13.5px;font-weight:500;color:var(--text-2);cursor:pointer;border-bottom:2px solid transparent}
.tabs a svg,.tabs a i[data-lucide]{width:16px;height:16px;flex-shrink:0}
.tabs a:hover{color:var(--text)}
.tabs a.active{color:var(--accent);border-bottom-color:var(--accent);font-weight:600}
/* Count badge on a section tab (unread, overdue, failed…) — the shared .pill
   vocab, sized down so it sits inside the tab row without stretching it. */
.tabs a .pill{height:18px;padding:0 7px;font-size:11px}
/* A section that is switched off (My Work → Fraud while the fraud checks are off):
   the tab keeps its place in the strip, so nothing moves and the section can say
   why, but reads as inactive. Pair it with a gray "Off" .pill. */
.tabs a.is-off{color:var(--muted)}
.tabs a.is-off.active{color:var(--text-2);border-bottom-color:var(--muted)}

/* The content column under a module's section tabs. Replaces the .split grid on
   CRM pages: full width, and a flex column so a viewport-locked page (the inbox)
   can hand its remaining height to the last child. */
.crm-body{display:flex;flex-direction:column;min-width:0}

/* kanban */
.board{display:flex;gap:16px;overflow-x:auto;padding-bottom:14px}
/* A column never gets NARROWER than its basis (min-width keeps the board
   scrolling once the lanes no longer fit) but DOES grow into the slack, so a
   board with few lanes fills the width instead of leaving dead space on the
   right. Pages that size their own lanes (kanban, leads) override the flex. */
.col{flex:1 1 290px;min-width:290px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);display:flex;flex-direction:column;max-height:calc(var(--vh) - 230px)}
.col-h{display:flex;align-items:center;gap:9px;padding:13px 15px;background:var(--th-bg);color:var(--th-fg);border-bottom:1px solid color-mix(in srgb,var(--th-bg) 75%,#000);border-radius:var(--radius) var(--radius) 0 0;font-weight:600;font-size:13.5px}
.col-h .ct{margin-left:auto;font-family:"JetBrains Mono";font-size:11.5px;color:var(--th-fg);background:rgba(255,255,255,.18);border:1px solid rgba(255,255,255,.28);border-radius:20px;padding:1px 8px}
.col-h .dotc{width:9px;height:9px;border-radius:50%}
.col-body{padding:11px;display:flex;flex-direction:column;gap:10px;overflow-y:auto}
.kcard{background:var(--surface);border:1px solid var(--border);border-radius:11px;padding:12px;cursor:grab;box-shadow:var(--shadow);transition:.14s}
.kcard:hover{border-color:var(--border-strong);transform:translateY(-2px);box-shadow:var(--shadow-lg)}
.kcard .kt{display:flex;gap:11px;align-items:flex-start}
.kcard h4{font-size:13.5px;font-weight:600;line-height:1.3}
.kcard .kref{font-size:11.5px;margin-top:2px;color:var(--muted)}
.kcard .kf{display:flex;align-items:center;justify-content:space-between;margin-top:11px;gap:8px}
.kcard .kprice{font-family:"JetBrains Mono";font-weight:600;font-size:13px}

/* misc */
.activity{display:flex;flex-direction:column}
.act{display:flex;gap:13px;padding:13px 0;border-bottom:1px solid var(--border)}
.act:last-child{border-bottom:none}
.act .aic{width:34px;height:34px;border-radius:9px;display:grid;place-items:center;flex-shrink:0}
.act .aic svg,.act .aic i[data-lucide]{width:16px;height:16px}
.act .atxt{font-size:13px;flex:1}
.act .atime{font-size:11.5px;color:var(--muted);white-space:nowrap}
.act b{font-weight:600}

.bars{display:flex;align-items:flex-end;gap:12px;height:170px;padding-top:10px}
.bar{flex:1;display:flex;flex-direction:column;align-items:center;gap:8px;height:100%;justify-content:flex-end}
.bar .bb{width:100%;max-width:38px;border-radius:7px 7px 3px 3px;background:linear-gradient(180deg,var(--accent),color-mix(in srgb,var(--accent) 55%,transparent));transition:.3s}
.bar .bl{font-size:11px;color:var(--muted);font-weight:500}
.bar .bv{font-size:11px;font-family:"JetBrains Mono";color:var(--text-2)}

/* Horizontal proportion meter — a labelled bar showing one value's share of a
   whole (e.g. a cost as a share of revenue). The fill width is set inline
   (data-driven). Accent by default; add .neg (cost/loss) or .pos (gain). */
.meter{position:relative;height:8px;border-radius:999px;background:var(--surface-3);overflow:hidden}
.meter>span{position:absolute;inset:0 auto 0 0;height:100%;border-radius:999px;background:var(--accent)}
.meter.neg>span{background:var(--danger)}
.meter.pos>span{background:var(--success)}

.list-row{display:flex;align-items:center;gap:14px;padding:14px 0;border-bottom:1px solid var(--border)}
.list-row:last-child{border-bottom:none}

/* A removable pill — a person on a recipient list. The × is a real button. */
.pill .pill-x{display:inline-grid;place-items:center;width:16px;height:16px;margin:0 -4px 0 1px;padding:0;border:0;border-radius:50%;background:transparent;color:inherit;cursor:pointer}
.pill .pill-x:hover{background:color-mix(in srgb, currentColor 16%, transparent)}
.pill .pill-x svg,.pill .pill-x i[data-lucide]{width:11px;height:11px}

/* EMAIL THIS REPORT — the window behind the clock button on every report page
   (public/js/report-schedule-window.js): When · Who · What sections, the report's existing
   schedules as .list-rows. Everything else in it is canonical vocab (.x-modal,
   .field-grid/.field/.input, .chip.sm checkboxes, .btn.icon, .pill). */
.rs-sec+.rs-sec{margin-top:18px;padding-top:16px;border-top:1px solid var(--border-light)}
.rs-sec>.form-sec{margin:0 0 10px}
.rs-sec>.field,.rs-sec>.field.check{margin-top:12px}
.rs-days{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
.rs-days[hidden]{display:none}
.rs-preview{margin:10px 0 0;font-size:13px}
.rs-recipients{display:flex;flex-wrap:wrap;gap:6px;min-height:24px;margin-bottom:12px}
.rs-intro,.rs-what{margin:0 0 6px}
.rs-row-main{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}
.rs-row-acts{display:flex;gap:2px;flex:0 0 auto}
@media (max-width:560px){.list-row.rs-row{flex-wrap:wrap}.rs-row-acts{width:100%;justify-content:flex-end}}

/* SCHEDULE ROWS — one job or one email per row on Admin → Job Schedules, Email
   Schedules and Mail → Recipients (partials/job-schedule-row.ejs,
   partials/email-event-block.ejs). The switch is .cfg-switch, the run dot
   .status-dot, a recipient a .pill with its .pill-x. */
.sched-intro{margin:0 0 12px}
.sched-intro:last-child{margin-bottom:0}
.sched-row{border:1px solid var(--border);border-radius:10px;padding:14px 16px;background:var(--surface)}
.sched-row+.sched-row{margin-top:12px}
.sched-row.is-off{background:var(--surface-2)}
.sched-row.is-forced{border-left:4px solid var(--danger)}
.sched-row.is-per-user{border-left:4px solid var(--warning)}
.sched-head{display:flex;justify-content:space-between;align-items:flex-start;gap:16px}
.sched-head-main{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}
.sched-title{margin:0 0 2px;font-size:15px;font-weight:700;color:var(--text-strong)}
.sched-title>.pill{margin-left:6px;vertical-align:middle}
.sched-desc{margin:4px 0 0;font-size:13.5px;color:var(--text-subtle)}
.sched-note{margin:6px 0 0;font-size:12.5px;font-weight:600;color:var(--danger)}
.sched-note.warn{color:var(--warning)}
.sched-toggle{display:inline-flex;align-items:center;gap:8px;flex:0 0 auto;font-size:13px;color:var(--text-subtle);cursor:pointer}
.sched-builder{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:10px;padding-top:10px;border-top:1px dashed var(--border)}
.sched-builder>label,.sched-fields{display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--text-subtle)}
.sched-builder .input{width:auto}
.sched-builder input[type=number].input{width:72px}
.sched-builder .f-cron{min-width:200px;font-family:var(--font-mono)}
.sched-fields{display:none;flex-wrap:wrap}
.sched-fields.active{display:inline-flex}
.sched-weekdays{display:inline-flex;flex-wrap:wrap;gap:4px}
.sched-status{display:flex;flex-wrap:wrap;gap:6px 18px;align-items:center;margin-top:10px;font-size:13px;color:var(--text-subtle)}
.sched-preview{color:var(--accent);font-weight:600}
.sched-last{display:inline-flex;align-items:center;gap:6px}
.sched-event{margin-top:12px;padding-top:12px;border-top:1px dashed var(--border)}
.sched-event .sched-title{font-size:14px}
.sched-row .form-sec,.sched-event .form-sec{margin:12px 0 6px}
.recip-list{display:flex;flex-wrap:wrap;gap:6px;min-height:24px}
.recip-add{margin-top:12px}
.recip-add>.field-act{gap:6px}
@media (max-width:560px){.sched-head{flex-direction:column;gap:8px}}

/* A chart bar / list row that opens its own detail is an <a>: it keeps the
   chart's look (no link colour or underline) and answers hover + keyboard focus. */
a.bar,a.list-row{color:inherit;text-decoration:none}
a.bar:hover .bb{background:var(--accent)}
a.bar:hover .bl,a.list-row:hover .t-strong{color:var(--accent)}
a.bar:focus-visible,a.list-row:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:6px}

/* A list row whose entity is archived/disabled — dimmed but fully readable.
   Shared by the admin field list (fields.disabled) and any future archive UI. */
.row-archived{opacity:.55}

.empty{display:grid;place-items:center;text-align:center;padding:60px 20px;color:var(--muted)}
.empty .eic{width:56px;height:56px;border-radius:14px;background:var(--surface-2);display:grid;place-items:center;margin-bottom:14px;border:1px solid var(--border)}
.empty .eic svg,.empty .eic i[data-lucide]{width:26px;height:26px}

/* =====================================================================
   TOAST REGION — THE one corner for every transient notice: TOP-RIGHT.
   ---------------------------------------------------------------------
   Every toast/alert stack in the app lives inside this ONE fixed region so
   they queue vertically instead of overlapping. .toast-region is emitted once
   per page by partials/x-topbar.ejs and holds the sub-stacks in a fixed
   `order`: transient App.toast first (closest to the corner, read-then-gone),
   then the CoPilot widget's stack, then the persistent channel-auth alerts.
   Anchored on --toast-top/--toast-right (theme.css) — 72px clears the fixed
   60px .topbar. Do NOT anchor a new notice to its own corner: append it to
   #toast-region (or just call App.toast).
   z-index 99996 makes this the TOP UI layer — a toast must stay visible over
   whatever fired it. It clears every overlay in the app: .x-modal-backdrop
   (9000), .app-modal-backdrop (9500), and the whole CoPilot widget stack
   (fab 99990 / panel 99991 / panel-open topbar 99992 / sw-modal 99993-99995).
   Only #maint-lock-toast (the maintenance countdown) sits higher, deliberately.
   ===================================================================== */
.toast-region{
  /* + --scroll-lock-gap: holds still while a modal locks the page (see .topbar) */
  position:fixed;top:var(--toast-top);right:calc(var(--toast-right) + var(--scroll-lock-gap, 0px));z-index:99996;
  display:flex;flex-direction:column;gap:var(--toast-gap);align-items:flex-end;
  width:var(--toast-width);
  /* The region spans a fixed width but is only as tall as its contents; keep it
     click-through so an empty/short stack never eats clicks on the page under it. */
  pointer-events:none;
}
/* Sub-stacks span the region; the notices INSIDE them shrink-wrap to their text
   and hang off the right edge (see .toasts/.chan-toasts/.sw-toast-stack below),
   so a one-word "Saved." doesn't render as a 420px slab. */
.toast-region>*{width:100%}
/* Only the parts of a notice you can actually CLICK take the pointer — the BOX
   stays click-through, like the region around it (pointer-events inherits, so
   these need no rule of their own; the exceptions below re-take it).
   A notice floats OVER the page, and the persistent ones (a sticky App.toast,
   the channel-auth alerts) park in this corner until they are dismissed — with
   pointer-events on the whole box they silently ATE every click on whatever sat
   under them, and the corner they own is exactly where a table puts its ACTION
   column. (Inventory -> Listing errors: retry a row that fails, and the sticky
   error toast it opens covers the Retry button of the rows near the top of the
   table, so the next click lands on the toast and nothing happens — reported
   as "sometimes Retry does nothing"; fixed 2026-09-11.)
   .sw-toast is the deliberate exception: the WHOLE toast is one click target
   (it opens the thread) and it clears itself after 10s. .inv-sync-banner is
   pure status with nothing to click. */
.toast-region .sw-toast{pointer-events:auto}
/* Every stack right-aligns its notices so all three read as one column. */
.toast-region .chan-toasts,.toast-region .sw-toast-stack{align-items:flex-end}
.toast-region .chan-alert,.toast-region .sw-toast{max-width:100%}
/* Sub-stack order inside the region (lowest number = closest to the corner). */
.toast-region .toasts{order:1}
.toast-region .sw-toast-stack{order:2}
.toast-region .chan-toasts{order:3}
.toast-region .inv-sync-banner{order:4}
.toast-region .scan-banner{order:5}
/* Inside the region the sub-stacks are plain flow children — the region owns the
   anchoring. Each keeps its own fixed position as a FALLBACK for the handful of
   standalone pages (login/2fa/easter-egg) that render no x-topbar. */
.toast-region .toasts,.toast-region .sw-toast-stack,.toast-region .chan-toasts,
.toast-region .inv-sync-banner,.toast-region .scan-banner{
  position:static;top:auto;right:auto;bottom:auto;left:auto;
  width:100%;max-width:none;z-index:auto;
}

/* toast */
/* Standalone fallback anchor for pages with no x-topbar (login/2fa/easter-egg);
   inside .toast-region the region owns the position and this is neutralised. */
.toasts{position:fixed;top:var(--toast-top);right:var(--toast-right);z-index:99996;display:flex;flex-direction:column;gap:var(--toast-gap);align-items:flex-end;width:var(--toast-width)}
.toast{display:flex;align-items:flex-start;gap:11px;background:var(--elev);border:1px solid var(--border);border-radius:11px;padding:13px 16px;box-shadow:var(--shadow-lg);min-width:260px;max-width:100%;font-size:13.5px;font-weight:500;color:var(--text);animation:slidein .25s ease;pointer-events:none}
/* ...and the bits you click take it back (see .toast-region above). App.toast
   hands .toast-msg its pointer back only when the message is long enough to
   need its own scrollbar. */
.toast .toast-close,.toast .toast-action{pointer-events:auto}
/* Message body. `pre-line` honors the \n in grouped multi-line summaries (the
   channel-retry / bulk-action results that used to need their own toast family),
   and the max-height keeps a long one scrollable instead of filling the screen. */
.toast .toast-msg{flex:1 1 auto;min-width:0;white-space:pre-line;overflow-wrap:anywhere;word-break:break-word;max-height:40vh;overflow-y:auto}
.toast svg,.toast i[data-lucide]{width:18px;height:18px;color:var(--green);flex:0 0 auto;margin-top:1px}
/* Error variant — same chrome, red icon + hairline so a failure reads differently. */
.toast.error svg{color:var(--red)}
.toast.error{border-color:color-mix(in srgb,var(--red) 45%,var(--border))}
/* Info variant — neutral notice (blue icon), between success-green and error-red. */
.toast.info svg{color:var(--blue)}
/* Toast action (App.toast {action:{label,href|onClick}}) — e.g. "View product",
   "Undo". App.toast renders an <a> for `href` but a <button> for `onClick`, so
   this MUST carry a full button reset: without it the button variant keeps the
   UA chrome (grey ButtonFace fill, outset border, 13.33px Arial) and reads as an
   unstyled box inside a themed toast. Both element types render identically. */
.toast .toast-action{
  margin-left:auto;flex:0 0 auto;
  -webkit-appearance:none;appearance:none;border:0;background:none;padding:0;
  font-family:inherit;font-size:inherit;line-height:inherit;font-weight:600;
  color:var(--accent);text-decoration:none;white-space:nowrap;cursor:pointer;
  border-radius:6px;
}
.toast .toast-action:hover{text-decoration:underline;color:var(--accent)}
.toast .toast-action:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* Sticky-toast close (App.toast {sticky:true}) — quiet ×, same idiom as chip closes.
   Same reset reason as .toast-action: a bare <button> otherwise draws its glyph
   in the UA font instead of the app's. */
.toast .toast-close{
  margin-left:auto;flex:0 0 auto;
  -webkit-appearance:none;appearance:none;border:0;background:none;
  font-family:inherit;font-size:16px;font-weight:inherit;line-height:1;
  color:var(--text-muted);cursor:pointer;padding:2px 5px;border-radius:6px;
}
.toast .toast-close:hover{color:var(--text);background:var(--surface-3)}
.toast .toast-close:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
@keyframes slidein{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:none}}

/* watchgpt / copilot */
.chat{max-width:860px;margin:0 auto;display:flex;flex-direction:column;gap:18px}
.msg{display:flex;gap:13px}
.msg .mav{width:34px;height:34px;border-radius:9px;display:grid;place-items:center;flex-shrink:0}
.msg .mav svg,.msg .mav i[data-lucide]{width:17px;height:17px}
/* User initials avatar — matches the topbar .user-avatar colors in both themes:
   light = white fill + navy initials; dark = raised surface + amber initials. */
.msg .mav.mav-initials{font-family:var(--font-display);font-size:12.5px;font-weight:700;letter-spacing:.02em;background:#fff;color:var(--accent-strong);box-shadow:0 0 0 1px rgba(0,0,0,.12)}
html[data-theme="dark"] .msg .mav.mav-initials{background:var(--surface-3);color:var(--accent);box-shadow:0 0 0 1px rgba(255,255,255,.16)}
.bubble{background:var(--surface);border:1px solid var(--border);border-radius:13px;padding:14px 16px;box-shadow:var(--shadow);font-size:13.5px}
.msg.me{flex-direction:row-reverse}
.msg.me .bubble{background:var(--accent-soft);border-color:transparent}
/* Rendered-markdown chat replies (mdToHtml): tidy spacing for blocks, lists, links. */
.bubble>div{margin:2px 0}
.bubble .sw-md-list{margin:4px 0;padding-left:18px}
.bubble .sw-md-list li{margin:2px 0}
.bubble a{color:var(--accent);text-decoration:underline}
.bubble code{font-family:"JetBrains Mono";font-size:12px;background:var(--surface-2);border-radius:5px;padding:1px 5px}
.cite{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-family:"JetBrains Mono";color:var(--accent);background:var(--accent-soft);border-radius:7px;padding:3px 8px;margin:4px 6px 0 0}
.chatbar{position:sticky;bottom:0;max-width:860px;margin:18px auto 0;display:flex;gap:10px;background:var(--surface);border:1px solid var(--border);border-radius:13px;padding:9px;box-shadow:var(--shadow-lg)}
.chatbar input{flex:1;border:none;background:transparent;outline:none;color:var(--text);font-family:inherit;font-size:14px;padding:0 10px}

/* scanner phone */
.phone{width:380px;margin:0 auto;background:var(--surface);border:1px solid var(--border);border-radius:28px;box-shadow:var(--shadow-lg);overflow:hidden}
.phone .ph-top{background:var(--accent);color:var(--on-brand);padding:18px 20px;display:flex;align-items:center;justify-content:space-between}
.phone .ph-top h3{font-size:16px}
.phone .ph-body{padding:18px}
.scan-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px}
.scan-btn{aspect-ratio:1.3;border-radius:14px;border:1px solid var(--border);background:var(--surface);box-shadow:var(--shadow);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;cursor:pointer;transition:.15s;font-weight:600;font-size:13px}
.scan-btn:hover{border-color:var(--accent);color:var(--accent);box-shadow:var(--shadow-lg);transform:translateY(-2px)}
.scan-btn svg,.scan-btn i[data-lucide]{width:26px;height:26px;color:var(--accent)}
.scanwindow{border:2px dashed var(--border-strong);border-radius:14px;height:120px;display:grid;place-items:center;color:var(--muted);margin-bottom:16px;position:relative;overflow:hidden;background:var(--surface-2)}
.scanline{position:absolute;left:8%;right:8%;height:2px;background:var(--accent);box-shadow:0 0 12px var(--accent);animation:scanmove 2.2s ease-in-out infinite}
@keyframes scanmove{0%,100%{top:20%}50%{top:80%}}

/* responsive */
@media(max-width:1180px){.kpis,.tiles{grid-template-columns:repeat(2,1fr)}}
@media(max-width:920px){.split{grid-template-columns:1fr}}
/* The topbar search is hidden on PHONES ONLY (≤768px, the same line as the hamburger) and is
   replaced there by the .search-toggle button + a full-width search strip — mobile.css section A.
   It used to vanish at 920px with nothing in its place, so a phone had no search at all. */
/* KPI tiles stay TWO-UP on a phone (a tile is a label and a number — half a
   screen is plenty), so a 14-tile report is seven rows, not fourteen. Workspace
   tiles carry a description and go single-column. (2026-09-26) */
@media(max-width:680px){.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.tiles{grid-template-columns:1fr}.view{padding:20px 16px 50px}}
.cols-2{grid-template-columns:1.6fr 1fr}
.cols-3{grid-template-columns:repeat(3,1fr)}
.cols-4{grid-template-columns:repeat(4,1fr)}
/* Five controls that read as ONE row (e.g. the consignment intake header: date,
   number, agent, store, status). A plain .field-grid is auto-fit, so it wraps at
   whatever point the viewport happens to force — which splits a group that is
   meant to be scanned as a line. minmax(0,1fr) lets the columns shrink below
   their content rather than overflowing the card. */
.cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}
@media(max-width:1100px){.cols-2,.cols-3{grid-template-columns:1fr}.cols-4{grid-template-columns:repeat(2,1fr)}.cols-5{grid-template-columns:repeat(3,minmax(0,1fr))}}
@media(max-width:680px){.cols-4{grid-template-columns:1fr}.cols-5{grid-template-columns:1fr}}

/* ============================================================
   Form vocab (canonical) — .field / .field-grid / .input + affordances.
   Replaces the legacy / .t-* form classes as pages migrate. (.modal is
   intentionally NOT here yet — it collides with legacy style.css .modal;
   it lands when the modal-using pages migrate off style.css.)
   ============================================================ */
.field{display:flex;flex-direction:column;gap:6px}
.field[hidden]{display:none} /* the display above defeats the UA's [hidden] — a field shown only when it applies */
.field>label,.field .field-label{font-size:12px;font-weight:600;color:var(--text-2)}
.field .field-help{font-size:11.5px;font-weight:400;color:var(--muted)}
/* A field label that carries a trailing action ON THE LABEL'S OWN LINE (e.g.
   "change this locked value"). The action is a bare text link, NOT a button:
   a button — even a small one — is taller than the 18px label line, which makes
   that one label taller than its neighbours in the same .field-grid row and
   pushes its input out of line with theirs. Nothing here may add height. */
.field .field-label.row{display:flex;align-items:center;gap:8px;min-height:18px}
.lbl-action{margin-left:auto;flex:0 0 auto;display:inline-flex;align-items:center;gap:3px;
  background:none;border:0;padding:0;height:auto;line-height:inherit;
  font-family:inherit;font-size:11px;font-weight:600;color:var(--accent);
  cursor:pointer;white-space:nowrap;text-decoration:none}
.lbl-action:hover{text-decoration:underline}
.lbl-action i,.lbl-action svg{width:11px;height:11px}
/* The `hidden` attribute must win over that inline-flex — an author display rule
   otherwise beats the UA `[hidden]{display:none}`, and a label action is exactly
   the kind of thing shown conditionally (the price-floor "Change", the linked
   product link). */
.lbl-action[hidden]{display:none}
/* ONE field made of several controls on one line (a key date's picker + its
   "Year unknown" box), under one .field-label. The controls share the field's
   width and the first takes the most; min-width:0 lets them shrink on a phone
   instead of pushing the card sideways. */
.field-split{display:flex;gap:6px;min-width:0}
.field-split>*{flex:1 1 0;min-width:0}
.field-split>:first-child{flex:2 1 0}
/* a checkbox in the row keeps its words on one line; the controls take the rest */
.field-split>.field.check{flex:0 0 auto;white-space:nowrap}
/* A field that takes a whole .field-grid row (a multi-control date, a notes line). */
.field-grid>.span-all{grid-column:1/-1}
.field.check{flex-direction:row;align-items:center;gap:8px}
.field.check>label{font-weight:500;color:var(--text)}
/* A checkbox row sharing a .field-grid row with labeled fields: pin it to the
   36px control band at the cell bottom so it lines up with the inputs beside
   it (not floating at the label height). */
.field-grid .field.check{align-self:end;min-height:36px}
/* A .field-grid cell holding an ACTION instead of an input (e.g. an "Open
   product" link beside the linked-SKU field): it carries no label of its own,
   so pin it to the same 36px control band the inputs beside it sit in. */
.field-grid>.field-act{display:flex;align-items:flex-end;align-self:end;min-height:36px}
.field-grid>.field-act>.btn{height:36px}
.input,
.field input:not([type=checkbox]):not([type=radio]):not([type=file]),
.field select,.field textarea{
  height:36px;width:100%;padding:0 12px;border:1px solid var(--border);border-radius:9px;
  font-family:inherit;font-size:13px;font-weight:400;background:var(--surface);color:var(--text);
  outline:none;transition:border-color .15s,box-shadow .15s}
textarea.input,.field textarea{height:auto;min-height:62px;padding:9px 12px;line-height:1.5;resize:vertical}
.input:focus,
.field input:focus,.field select:focus,.field textarea:focus{
  border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.input:disabled,.field input:disabled,.field select:disabled,.field textarea:disabled{opacity:.6;cursor:not-allowed}
select.input,.field select{cursor:pointer}
/* ── ONE DROPDOWN ARROW (app-wide) ─────────────────────────────────────────
   Every single-value <select> in the app, canonical (.input / .field) or not,
   paints the SAME chevron (--select-chevron, theme.css) 10px in from its right
   edge, and keeps 30px free on the right so a long value never runs under it.
   A select left on the browser's NATIVE arrow draws it flush against its right
   border (Chrome/macOS), which is what the tray panels, admin cards, modals and
   every legacy page-sheet select looked like beside the canonical ones.
   Why !important: page and legacy sheets set a `background:` or `padding:`
   SHORTHAND on their selects (for the colour / spacing), which silently resets
   background-image to none and padding-right to a few px, and several of them
   out-specify anything app.css could write. Only the arrow's OWN longhands are
   forced; a page keeps its background COLOUR, border, height and font.
   Tune it per context with the two variables, at normal specificity, and
   never re-declare the image: --select-pad-end (the room kept free on the
   right, default 30px) and --select-chevron-x (the arrow's inset, default 10px).
   A listbox ([multiple], or [size] > 1) has no arrow and keeps its own look.
   Tested by src/ui/select-chevron.browser.test.js. */
select:not([multiple],[size]:not([size="0"],[size="1"])){
  -webkit-appearance:none !important;appearance:none !important;
  background-image:var(--select-chevron) !important;
  background-repeat:no-repeat !important;
  background-size:14px 14px !important;
  background-position:right var(--select-chevron-x,10px) center !important;
  padding-right:var(--select-pad-end,30px) !important;
}
:where(select:not(:disabled)){cursor:pointer}
.input.sm{height:32px;width:auto;padding:0 10px;font-size:12.5px;border-radius:8px}
select.input.sm{--select-pad-end:28px;--select-chevron-x:9px}
/* A .tb-input search box nested INSIDE a .field: the `.field input` rule above
   outguns `.tb-input input` on specificity and resets the left padding — the
   magnifying-glass icon then sits ON the text. Re-assert the icon gutter at
   higher specificity so the pairing just works. */
.field .tb-input input:not([type=checkbox]):not([type=radio]):not([type=file]){padding-left:34px}
/* Canonical FILE picker: <input type="file" class="input">. The native
   "Choose file" control gets button-like, theme-aware chrome instead of the
   unstyled UA widget. */
.input[type=file]{padding:0 12px 0 0;height:36px;line-height:34px;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-2)}
.input[type=file]::file-selector-button{height:34px;margin:0 10px 0 0;padding:0 12px;border:0;border-right:1px solid var(--border);border-radius:8px 0 0 8px;background:var(--surface-2);color:var(--text);font-family:inherit;font-size:12.5px;font-weight:600;cursor:pointer;transition:background .15s}
.input[type=file]::file-selector-button:hover{background:var(--surface-3)}
/* Long-text checkbox rows (attestations, consent notices): top-align the box
   against multi-line copy — the base .field.check centers, which is right only
   for one-liners. */
.field.check.top{align-items:flex-start}
.field.check.top input[type=checkbox]{margin-top:2px;flex-shrink:0}
.field.check input[type=checkbox]{accent-color:var(--accent);cursor:pointer}
.input.prefix{border-top-left-radius:0;border-bottom-left-radius:0}
.input.suffix{border-top-right-radius:0;border-bottom-right-radius:0}
.field-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:16px}
/* Mini section label inside a long form card ("IDENTIFICATION", "PRICING") —
   breaks a wall of equal-height inputs into scannable groups. */
.form-sec{display:block;font-size:11.5px;font-weight:700;letter-spacing:.07em;
  text-transform:uppercase;color:var(--muted);margin:20px 0 10px}
.form-sec:first-child,.card-head+.form-sec{margin-top:2px}
/* Required-field affordances: the label's asterisk, and the miss-marker a
   form's save handler paints on unfilled required fields (cleared on the next
   attempt / on input). */
.field .req,.form-sec .req{color:var(--red);font-weight:700}
.field.req-missing input,.field.req-missing textarea,.field.req-missing select,
.field.req-missing .input-affix{border-color:var(--red)}
.field.req-missing>.field-label,.form-sec.req-missing{color:var(--red)}
/* Affixed input ($ / % chip fused to the field) — promoted from the
   consignments report page the moment a second page (the intake form) needed
   it. The !important on the inner border is deliberate: `.field input` would
   otherwise re-draw a border inside the wrapper's own. */
.input-affix{display:flex;align-items:stretch;border:1px solid var(--border);border-radius:9px;background:var(--surface);overflow:hidden}
.input-affix:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.input-affix .affix{background:var(--surface-2);color:var(--muted);font-weight:600;font-size:0.85rem;padding:0 10px;display:flex;align-items:center;border-right:1px solid var(--border)}
.input-affix.suffix .affix{border-right:none;border-left:1px solid var(--border)}
.input-affix input,.field .input-affix input:not([type=checkbox]):not([type=radio]):not([type=file]){
  border:none !important;outline:none;box-shadow:none;height:34px;padding:0 10px;flex:1;font-size:13px;background:transparent;min-width:0;border-radius:0}
.input-affix input:focus{outline:none;box-shadow:none}
.num{text-align:right;font-variant-numeric:tabular-nums}
.card.accent{position:relative;overflow:hidden}
.card.accent::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--accent)}

/* Field info-help "?" badge — opens the info modal wherever tray field
   definitions surface (tray/review/fixing forms, product page). accent-soft +
   accent are both theme-aware (blue in light, amber in dark), so the "?" stays
   readable in both themes — no per-theme override needed. */
.info-btn{display:inline-block;width:15px;height:15px;line-height:15px;text-align:center;
  background:var(--accent-soft);color:var(--accent);border-radius:50%;font-weight:bold;
  cursor:help;margin-left:.4rem;font-size:.7rem;vertical-align:middle;position:relative;top:-1px}
/* Media attached to a field's info help (rendered by /js/info-content.js) —
   keep images/videos inside the modal, whatever size was uploaded. Put this
   class on every info modal's content container. */
.info-modal-content img,.info-modal-content video{display:block;max-width:100%;height:auto;
  margin:.75rem auto 0;border-radius:8px;border:1px solid var(--border)}

/* Field info-help editor (/js/field-info-editor.js on the admin field forms):
   plain-text box + attached-media chips + upload action. */
.info-editor{display:flex;flex-direction:column;gap:8px}
.info-editor-bar{display:flex;align-items:center;gap:10px}
.info-editor-status{font-size:12px;color:var(--muted)}
.info-editor-status.error{color:var(--red)}
.info-editor-media{display:flex;flex-wrap:wrap;gap:8px}
.info-media-chip{display:flex;align-items:center;gap:8px;max-width:260px;padding:6px 8px;
  border:1px solid var(--border);border-radius:9px;background:var(--surface-2)}
.info-media-chip img,.info-media-chip video{width:44px;height:44px;object-fit:cover;border-radius:7px;
  border:1px solid var(--border);background:var(--surface);flex-shrink:0}
.info-media-name{font-size:12px;color:var(--text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.info-media-remove{flex-shrink:0;width:22px;height:22px;border:none;border-radius:7px;background:transparent;
  color:var(--text-muted,var(--muted));font-size:15px;line-height:1;cursor:pointer}
.info-media-remove:hover{background:var(--surface-3,var(--surface));color:var(--text)}

/* ============================================================
   Compat helpers — legacy t-* form/utility classes ported from
   ds.css onto the example token set, so pages that load app.css
   (without ds.css) render their filters & forms correctly.
   Pages that self-define these (e.g. #search-request-form .t-field)
   keep their own look via higher specificity / source order.
   ============================================================ */
.t-field{display:flex;flex-direction:column;gap:6px;font-size:11.5px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)}
.t-field input:not([type=checkbox]):not([type=radio]):not([type=file]),
.t-field select,.t-field textarea,
.t-input,input.t-prefix,input.t-suffix{
  height:36px;padding:0 12px;border:1px solid var(--border);border-radius:9px;font-family:inherit;font-size:13px;
  font-weight:400;text-transform:none;letter-spacing:normal;background:var(--surface);color:var(--text);outline:none;transition:border-color .15s,box-shadow .15s}
.t-field textarea{height:auto;min-height:62px;padding:9px 12px;line-height:1.5;resize:vertical}
.t-field input:not([type=checkbox]):not([type=radio]):not([type=file]):focus,
.t-field select:focus,.t-field textarea:focus,
.t-input:focus,input.t-prefix:focus,input.t-suffix:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);background:var(--surface)}
.t-field input:disabled,.t-field select:disabled,.t-input:disabled{opacity:.6;cursor:not-allowed}
.t-field-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:16px}
.t-muted{color:var(--muted)}
.t-mono{font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace}
.t-green{color:var(--green)} .t-blue,.t-cyan{color:var(--blue)} .t-amber,.t-yellow,.t-orange{color:var(--amber)}
.t-red,.t-pink{color:var(--red)} .t-gray{color:var(--gray)} .t-violet,.t-purple,.t-indigo{color:var(--violet)}


/* ============================================================
   Shell widgets (avatar menu, account-settings modal, footer)
   moved from style.css 2026-06-20 so the x-shell no longer needs
   the legacy sheet. Mirrored into examples/app.css to keep the
   drift gate green.
   ============================================================ */
.user-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    /* The header is a dark-blue gradient in BOTH light and dark mode, so a blue
       (--accent) circle blended in. Use a light fill + navy initials so it reads
       clearly against the header regardless of theme. */
    background-color: #ffffff;
    color: var(--accent-strong);
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.12);
    font-size: 12px;
    font-weight: 700;
    line-height: 1;
    letter-spacing: 0.02em;
    flex-shrink: 0;
    user-select: none;
    /* button reset (the avatar is now a menu trigger) */
    border: none;
    padding: 0;
    cursor: pointer;
    font-family: inherit;
}
.user-avatar:hover { background-color: var(--surface-3); }
.user-avatar:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Dark mode: a white circle washed out the light-amber initials
   (--accent-strong is light amber in dark). Use a raised surface fill + amber
   initials so the text reads clearly against the dark header. */
html[data-theme="dark"] .user-avatar {
    background-color: var(--surface-3);
    color: var(--accent);
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16);
}
html[data-theme="dark"] .user-avatar:hover { background-color: var(--surface-2); }
/* ── User-avatar account menu ─────────────────────────────────────────── */
.user-menu { position: relative; display: inline-flex; align-items: center; }
/* In the example-template topbar the avatar is a 34px disc sitting flush with
   the 38px .iconbtn row (gap 6px). Match that rhythm so the trigger doesn't
   read smaller/shifted next to the action icons. Scoped to .topbar so the
   scanner-login avatar keeps its own sizing. */
.topbar .user-menu { margin-left: 6px; }
.topbar .user-avatar { width: 34px; height: 34px; font-size: 13px; }
.user-menu-dropdown {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    min-width: 248px;
    background: var(--surface);
    border: 1.25px solid var(--border);
    border-radius: 8px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
    padding: 6px;
    z-index: 1000;
}
.user-menu-id { padding: 8px 10px 10px; border-bottom: 1px solid var(--border-light); margin-bottom: 4px; }
.user-menu-name { font-weight: 700; color: var(--text); font-size: 13px; }
.user-menu-email { color: var(--text-muted); font-size: 12px; margin-top: 2px; word-break: break-all; }
.user-menu-item {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    text-align: left;
    background: none;
    border: none;
    padding: 8px 10px;
    font-size: 13px;
    color: var(--text);
    border-radius: 6px;
    cursor: pointer;
    font-family: inherit;
    text-decoration: none;
    box-sizing: border-box;
}
.user-menu-item:hover { background: var(--surface-2); }/*
then the label.
   Toggle rows add a switch on the far right (below). Leading icons are muted so
   the labels read first; the logout icon goes red via its own rule. */
.user-menu-item svg, .user-menu-toggle > svg,.user-menu-item i[data-lucide],.user-menu-toggle > i[data-lucide] {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    color: var(--text-muted);
}
.user-menu .user-menu-dropdown a.user-menu-logout svg,
.user-menu .user-menu-dropdown button.user-menu-logout svg { color: var(--danger); }
.user-menu-row-label { flex: 1 1 auto; min-width: 0; white-space: nowrap; }
/* Hairline separator that sets the destructive logout apart from the rest. */
.user-menu-sep { height: 1px; margin: 4px 6px; background: var(--border-light); }
/* The logout row is wrapped in a POST form; keep it laying out like a plain row. */
.user-menu-logout-form { margin: 0; display: block; width: 100%; }/*
so they're untouched. */
/* Logout is a <button> inside a POST form (a GET /logout was CSRF-able), so these
   match both element types — the row must look identical either way. */
.user-menu .user-menu-dropdown a.user-menu-item.user-menu-logout,
.user-menu .user-menu-dropdown button.user-menu-item.user-menu-logout {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    box-sizing: border-box;
    padding: 8px 10px;
    border-radius: 6px;
    text-align: left;
    background: none;
    color: var(--danger);
    font-weight: 700;
}
.user-menu .user-menu-dropdown a.user-menu-item.user-menu-logout:hover,
.user-menu .user-menu-dropdown button.user-menu-item.user-menu-logout:hover {
    background: var(--surface-2);
    color: var(--danger);
}/*
but for non-destructive <a> rows (e.g.
   Admin): force the normal text color so they match the <button> items
   instead of inheriting the grey/near-white topbar link color. */
.user-menu .user-menu-dropdown a.user-menu-item:not(.user-menu-logout), .user-menu .user-menu-dropdown a.user-menu-item:not(.user-menu-logout):hover {
    color: var(--text);
}
.user-menu-toggle {
    position: relative;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 10px;
    border-radius: 6px;
    font-size: 13px;
    color: var(--text);
    cursor: pointer;
    box-sizing: border-box;
}
.user-menu-toggle:hover { background: var(--surface-2); }
/* The native checkbox stays in the DOM (the JS toggles still listen to it) but
   is visually hidden; the switch below shows the on/off state instead. */
.user-menu-toggle-input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    margin: 0;
    pointer-events: none;
}/*
accent = on. */
.user-menu-switch {
    position: relative;
    flex-shrink: 0;
    width: 36px;
    height: 20px;
    border-radius: 999px;
    background: var(--border-subtle);
    transition: background 0.15s ease;
}
.user-menu-switch::after {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--surface);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
    transition: transform 0.15s ease;
}
.user-menu-toggle-input:checked ~ .user-menu-switch { background: var(--accent); }
.user-menu-toggle-input:checked ~ .user-menu-switch::after { transform: translateX(16px); }
.user-menu-toggle:hover .user-menu-switch { background: var(--text-faint); }
.user-menu-toggle:hover .user-menu-toggle-input:checked ~ .user-menu-switch { background: var(--accent-strong); }
/* ── Account modals ───────────────────────────────────────────────────── */
.account-modal-overlay {
    position: fixed;
    inset: 0;
    background: var(--overlay);
    display: none;
    align-items: center;
    justify-content: center;
    z-index: 2000;
}
.account-modal-overlay:not([hidden]) { display: flex; }
.account-modal {
    background: var(--surface);
    border-radius: 10px;
    padding: 22px 22px 18px;
    width: 360px;
    max-width: calc(100vw - 32px);
    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.25);
    display: none;
}
.account-modal:not([hidden]) { display: block; }
.account-modal h3 { margin: 0 0 14px; font-size: 16px; color: var(--text); }
.account-modal label {
    display: block;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-subtle);
    margin-bottom: 10px;
}
.account-modal label input {
    display: block;
    width: 100%;
    margin-top: 4px;
    padding: 8px 10px;
    border: 1px solid var(--border-subtle);
    border-radius: 6px;
    font-size: 14px;
    box-sizing: border-box;
}
.account-modal-hint { font-size: 11px; color: var(--text-muted); margin: -4px 0 12px; }
/* Live password-strength checklist — mirrors #password-requirements on the
   admin edit-user page. */
.account-pw-req { font-size: 12px; color: var(--text-faint); margin: -2px 0 12px; padding-left: 4px; }
.account-pw-req div { transition: color 0.3s; }
.account-pw-req div.valid { color: var(--success); text-decoration: line-through; }
.account-pw-req div.valid::before { content: '✓ '; }
.account-modal-msg { font-size: 12px; min-height: 16px; margin-bottom: 8px; }
.account-modal-msg.is-error { color: var(--danger); }
.account-modal-msg.is-ok { color: var(--success); }
.account-modal-actions { display: flex; justify-content: flex-end; gap: 8px; }
.account-modal-actions .btn-primary,
.account-modal-actions .btn-secondary {
    padding: 8px 16px;
    border-radius: 6px;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    border: 1px solid transparent;
    font-family: inherit;
}
.account-modal-actions .btn-primary { background: linear-gradient(135deg, var(--accent), var(--accent-end, var(--accent-strong, var(--accent)))); color: var(--on-brand); }
.account-modal-actions .btn-primary:hover { filter: brightness(1.06); }
.account-modal-actions .btn-primary:disabled { opacity: 0.6; cursor: default; }
.account-modal-actions .btn-secondary { background: var(--surface-3); color: var(--text); border-color: var(--border); }
.account-modal-actions .btn-secondary:hover { background: var(--border); }
/* ── User-settings modal (tabbed: password / PIN / phone / MCP) ────────── */
.account-modal.account-settings { position: relative; width: 718px; }
.account-settings > h3 {
    font-family: var(--font-display); font-weight: 600; letter-spacing: -0.02em;
    padding-right: 30px; /* clear the top-right close */
}
/* Top-right ghost icon-close, matching the canonical modal close. */
.account-modal-close {
    position: absolute; top: 12px; right: 12px;
    display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: 30px; padding: 0;
    background: none; border: none; border-radius: 8px;
    color: var(--text-muted); cursor: pointer;
}
.account-modal-close:hover { background: var(--surface-3); color: var(--text); }
.account-modal-close svg,.account-modal-close i[data-lucide] { width: 18px; height: 18px; }
.account-tabs {
    display: flex; flex-wrap: wrap; gap: 4px;
    margin: 0 0 16px; padding-bottom: 12px;
    border-bottom: 1px solid var(--border-light);
}
.account-tab {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 6px 10px; border-radius: 6px;
    background: none; border: 1px solid transparent;
    font-family: inherit; font-size: 12px; font-weight: 600;
    color: var(--text-muted); cursor: pointer;
}
.account-tab svg,.account-tab i[data-lucide] { width: 14px; height: 14px; color: currentColor; }
.account-tab:hover { background: var(--surface-2); color: var(--text); }
.account-tab.is-active { background: var(--surface-2); color: var(--text); border-color: var(--border); }
/* Stack every panel in one grid cell so the modal is always as tall as the
   TALLEST panel (password) — the height never jumps when you switch tabs. The
   inactive panels keep their box (so the cell keeps its size) but are hidden. */
.account-panels { display: grid; }
.account-panel { grid-area: 1 / 1; min-width: 0; }
.account-panel[hidden] { display: block; visibility: hidden; } /* keep sized, override UA display:none */
.account-panel:not([hidden]) { visibility: visible; }
/* Connect-via-MCP panel */
.account-mcp-lead { font-size: 13px; color: var(--text-subtle); margin: 0 0 12px; line-height: 1.5; }
.account-mcp-steps { margin: 0 0 14px; padding-left: 18px; font-size: 13px; color: var(--text); line-height: 1.5; }
.account-mcp-steps li { margin-bottom: 6px; }
.account-mcp-field { display: block; font-size: 12px; font-weight: 600; color: var(--text-subtle); margin-bottom: 8px; }
.account-mcp-url { display: flex; gap: 6px; margin-top: 4px; }
.account-mcp-url input {
    flex: 1 1 auto; min-width: 0;
    padding: 8px 10px; border: 1px solid var(--border-subtle); border-radius: 6px;
    font-family: var(--font-mono); font-size: 12px;
    background: var(--surface-2); color: var(--text); box-sizing: border-box;
}
.account-mcp-url .btn { flex-shrink: 0; }
/* The generic hint tucks tight under form inputs (-4px); give it real breathing
   room under the MCP address row instead. */
.account-mcp .account-modal-hint { margin-top: 12px; }
/* A lead paragraph at the top of a settings panel. */
.account-lead { font-size: 13px; color: var(--text-subtle); margin: 0 0 12px; line-height: 1.5; }
/* Passkeys panel — the account's passkeys, above the "add" form. */
.passkey-list { display: flex; flex-direction: column; gap: 6px; margin: 0 0 14px; }
.passkey-row {
    display: flex; align-items: center; gap: 10px; padding: 8px 10px;
    border: 1px solid var(--border); border-radius: 8px; background: var(--surface-2);
}
.passkey-row > svg,.passkey-row > i[data-lucide] { width: 16px; height: 16px; flex: 0 0 auto; color: var(--text-muted); }
.passkey-meta { flex: 1 1 auto; min-width: 0; }
.passkey-name { font-size: 13px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.passkey-sub { font-size: 11.5px; color: var(--text-muted); }
.passkey-empty { font-size: 12.5px; color: var(--text-muted); margin: 0; }
.passkey-row > .btn { flex: 0 0 auto; }
/* Renaming in place: the name becomes a text box with Save / Cancel. */
.passkey-rename { display: flex; align-items: center; gap: 6px; flex: 1 1 auto; min-width: 0; margin: 0; }
.passkey-rename .input { flex: 1 1 auto; min-width: 0; }

/* ── Standalone account pages (the account-setup link) ───────────────────
   A card on a plain page, for a flow that runs before anyone is signed in.
   Controls inside are the canonical .field / .input / .btn / .flash. */
/* minmax(0,1fr): an `auto` track sizes itself to the card's 440px and overflows a
   phone, and the card's max-width:100% then resolves against that track — the
   track must be the screen, so the card can shrink to it (2026-09-26). */
body.auth-page { display: grid; grid-template-columns: minmax(0, 1fr); place-items: center; min-height: 100vh; margin: 0; padding: 24px 16px; box-sizing: border-box; }
.auth-card {
    width: 440px; max-width: 100%; box-sizing: border-box; padding: 30px 30px 24px;
    background: var(--surface); border: 1px solid var(--border); border-radius: 18px; box-shadow: var(--shadow-lg);
}
.auth-brand { display: flex; justify-content: center; margin: 0 0 20px; }
/* SIGN-IN BACKGROUND — the se. logo tiles drifting behind the sign-in card on the
   staff sign-in and the scanner kiosk sign-in (markup: partials/login-bg). The
   tile file carries its own gradient, so only its opacity follows the theme. */
.login-bg { position: fixed; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.login-bg-tile { position: absolute; height: auto; max-width: none; opacity: .09; }
[data-theme="dark"] .login-bg-tile { opacity: .08; }
.auth-kicker { margin: 0 0 6px; font-family: var(--font-mono); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); }
.auth-title { margin: 0 0 8px; font-family: var(--font-display); font-size: 21px; font-weight: 600; letter-spacing: -.02em; line-height: 1.25; color: var(--text); }
.auth-lead { margin: 0 0 16px; font-size: 13.5px; line-height: 1.55; color: var(--text-subtle); }
.auth-steps { margin: 0 0 18px; padding-left: 20px; font-size: 13px; line-height: 1.6; color: var(--text); }
.auth-form { display: flex; flex-direction: column; gap: 12px; margin: 0; }
.auth-form + .auth-form { margin-top: 8px; }
.auth-submit { width: 100%; height: 42px; justify-content: center; }
.auth-qr { display: block; width: 196px; height: 196px; margin: 0 auto 12px; border: 1px solid var(--border); border-radius: 10px; image-rendering: pixelated; }
.auth-help { margin: 0 0 6px; font-size: 12.5px; text-align: center; color: var(--text-muted); }
.auth-secret {
    margin: 0 0 18px; padding: 9px 12px; font-family: var(--font-mono); font-size: 13px; text-align: center;
    word-break: break-all; user-select: all; color: var(--text);
    background: var(--surface-2); border: 1px solid var(--border); border-radius: 9px;
}
.auth-done { display: grid; place-items: center; width: 48px; height: 48px; margin: 0 auto 14px; border-radius: 50%; background: var(--green-bg); color: var(--green); }
.auth-done svg,.auth-done i[data-lucide] { width: 24px; height: 24px; }
.auth-center { text-align: center; }
.auth-foot { margin: 16px 0 0; text-align: center; }
.auth-foot button { padding: 0; border: 0; background: none; font: inherit; font-size: 12.5px; color: var(--text-muted); cursor: pointer; }
.auth-foot button:hover { color: var(--text); text-decoration: underline; }
/* The "or" rule between the password form and the passkey button on /login. */
.auth-or { display: flex; align-items: center; gap: 10px; margin: 16px 0; font-size: 12px; color: var(--text-muted); }
.auth-or::before, .auth-or::after { content: ""; flex: 1; height: 1px; background: var(--border); }
/* ... all existing styles ... */

.app-footer {
    /* Use Flexbox for powerful centering */
    display: flex;
    justify-content: center;
    align-items: center;

    /* No width:100% here — the shell footer is a stretched flex item that also
       carries margin-left:68px to clear the fixed sidebar (see body > .app-footer
       below). width:100% would make it viewport-wide AND offset 68px, pushing it
       past the right edge and giving every page a horizontal scrollbar. Letting
       it auto-size (block / flex-stretch) fills the available space correctly. */

    /* Your other styles */
    padding: 2rem 1rem;
    margin-top: 3rem;
    color: var(--text-faint);
    font-size: 0.9rem;
    border-top: 1px solid var(--border);
}
/* Heart in the footer is the entry point to /easter-egg. We keep the heart
   the same colour as the surrounding footer text so the link is hidden in
   plain sight; on hover it blooms red and pulses gently. */
.app-footer .ee-heart,
.ee-heart {
    color: inherit;
    text-decoration: none;
    display: inline-block;
    transition: color 0.2s ease, transform 0.2s ease;
}
.app-footer .ee-heart:hover,
.ee-heart:hover {
    color: var(--danger);
    transform: scale(1.25);
    animation: ee-heart-beat 0.9s ease-in-out infinite;
}
body > .app-footer {
    flex: 0 0 auto;
    margin-left: 68px; /* clear the fixed 68px sidebar, matching .main */
}

@keyframes ee-heart-beat {
    0%, 100% { transform: scale(1.25); }
    50%      { transform: scale(1.45); }
}


/* ============================================================
   Generic utilities ported from style.css 2026-06-20 (.actions
   button-row, .chart-container, .empty-state-icon, .status-dot)
   so report/admin/crm pages can drop the legacy sheet. Mirrored
   into examples/app.css to keep the drift gate green.
   ============================================================ */
.actions {
    display: flex;
    align-items: center;
    gap: 1rem;
}
/* Compact table-cell primitives (reusable across report/admin tables):
   a vertical action stack, a right-aligned button row, a compact numeric
   input, a small muted sub-note line, and a tighter wrap-cell width. */
.act-stack { display: flex; flex-direction: column; gap: 6px; align-items: center; }
.act-row { display: flex; gap: 6px; justify-content: center; }
/* …against the previous column instead of centred — pair it with .joins-next on
   that column so the pill and the button that acts on it read as one cluster. */
.act-row.start { justify-content: flex-start; }
.input.num-input { width: 80px; text-align: right; font-variant-numeric: tabular-nums; }
.cell-note { font-size: 12px; color: var(--muted); line-height: 1.35; margin-top: 3px; white-space: normal; overflow-wrap: anywhere; }
/* …wrapping only BETWEEN words, for a note that carries a code (a tracking number):
   `anywhere` lets a squeezed table shrink the column to one character, which split
   the number across lines — here the column keeps the width of its longest word. */
.cell-note.keep-words { overflow-wrap: break-word; }
.wrap-cell.tight { max-width: 320px; }
.tac { text-align: center !important; }
/* Vertical rhythm for a column of blocks, such as a window body filled from JS: every
   block after the first keeps a gap from the one above it. */
.stack > * + * { margin-top: 14px; }
/* The same rhythm for a list of short lines inside one table cell. */
.stack.tight > * + * { margin-top: 4px; }
/* A control that must NOT stretch inside a column flex parent. `.field` is
   `flex-direction:column`, so a `.btn` placed in one is stretched to the full
   field width by the default `align-items:stretch` — invisible on a ghost
   button until it is hovered and the whole row paints. Add `self-start`. */
.self-start { align-self: flex-start; }
.tlink { cursor: pointer; }
.tlink:hover { text-decoration: underline; text-underline-offset: 2px; color: var(--accent); }
/* A link inside running text that must READ as a link at rest — every `a`
   inherits the text colour (`a{color:inherit}` above), so a link among words is
   otherwise invisible until hovered. The History's photo links are one. */
.text-link { color: var(--link); text-decoration: underline; text-underline-offset: 2px; }
.text-link:hover { color: var(--accent); }
.chart-container {
    position: relative;
    width: 100%;
    height: 200px;
    /* Adjust this value as needed */
    margin: 1rem 0;
}
.empty-state-icon svg.lucide,.empty-state-icon i[data-lucide] {
    width: 1em;
    height: 1em;
}
.status-dot {
    display: inline-block;
    width: 0.75em;
    height: 0.75em;
    border-radius: 50%;
    vertical-align: middle;
    margin-right: 0.25em;
}
.status-dot.red { background: var(--danger); }
.status-dot.orange { background: var(--signal); }
.status-dot.yellow { background: var(--warning); }
.status-dot.green { background: var(--success); }
.status-dot.blue { background: var(--accent); }


/* thumbnail image component ported from style.css 2026-06-20; mirrored to examples for drift gate */
.thumbnail {
    position: relative;
    width: 150px;
    height: 150px;
    border: 1px solid var(--border-subtle);
    border-radius: 8px;
    overflow: hidden;
    background-color: var(--surface-2);
    display: flex;
    align-items: center;
    justify-content: center;
}
.thumbnail img {
    max-width: 100%;
    max-height: 100%;
    cursor: pointer;
    transition: transform 0.2s;
}
@media (hover: hover) {
    .thumbnail img:hover {
        transform: scale(1.05);
    }
}
/* For saved images shown as text */
.thumbnail .text-preview {
    padding: 0.5rem;
    font-size: 0.9rem;
    color: var(--text-subtle);
    text-align: center;
    word-break: break-all;
}
.thumbnail .remove-image-btn {
    position: absolute;
    top: 5px;
    right: 5px;
    background-color: var(--overlay);
    color: var(--text-inverse);
    border: none;
    border-radius: 50%;
    box-sizing: border-box;
    padding: 0 0 2px;
    width: 24px;
    height: 24px;
    font-size: 14px;
    line-height: 1;
    cursor: pointer;
    transition: background-color 0.2s;

    /* Flexbox for perfect centering */
    display: flex;
    align-items: center;
    justify-content: center;
}
.thumbnail .remove-image-btn:hover {
    background-color: rgba(200, 0, 0, 0.8);
}
/* Styles for SortableJS Drag & Drop feedback */
.thumbnail.sortable-ghost {
    opacity: 0.4;
    background-color: var(--text-faint);
}
.thumbnail.sortable-chosen {
    cursor: grabbing;
}

/* ============================================================
   Layout customizer — the shared reorder/hide editor used by the
   sidebar (user-settings modal) and the dashboard tiles ("Customize").
   Markup: .layout-editor > (.layout-group-block >) .layout-list > .layout-row.
   ============================================================ */
.layout-editor { display: flex; flex-direction: column; gap: 4px; }
.layout-group-block + .layout-group-block { margin-top: 12px; }
.layout-group {
    font-family: var(--font-display, inherit);
    font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
    color: var(--text-muted); padding: 8px 2px 4px;
}
.layout-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.layout-row {
    display: flex; align-items: center; gap: 10px;
    padding: 8px 10px;
    background: var(--surface-2); border: 1px solid var(--border); border-radius: 10px;
}
.layout-row .drag-handle {
    display: inline-flex; align-items: center; color: var(--text-muted);
    cursor: grab; touch-action: none; flex: 0 0 auto;
}
.layout-row .drag-handle:active { cursor: grabbing; }
.layout-row .drag-handle i { width: 16px; height: 16px; }
.layout-row-icon { width: 18px; height: 18px; color: var(--text-muted); flex: 0 0 auto; }
.layout-row-label { flex: 1 1 auto; min-width: 0; font-size: 14px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.layout-row-toggle {
    display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
    font-size: 12px; color: var(--text-muted); cursor: pointer; user-select: none;
}
.layout-row-toggle input { accent-color: var(--accent); cursor: pointer; }
.layout-row.sortable-ghost { opacity: .45; }
.layout-row.sortable-chosen { cursor: grabbing; }
.layout-editor-actions {
    display: flex; align-items: center; gap: 10px; margin-top: 14px; padding-top: 12px;
    border-top: 1px solid var(--border);
}
.layout-editor-actions .layout-msg { flex: 1 1 auto; font-size: 13px; color: var(--text-muted); }
.layout-editor-actions .layout-msg.is-ok { color: var(--success); }
.layout-editor-actions .layout-msg.is-error { color: var(--danger); }

/* The sidebar editor lives in the tabbed settings modal, whose panels share one
   grid cell sized to the TALLEST panel and never scroll. A user with many nav
   items would otherwise push the modal past the viewport (Save/close unreachable).
   Bound this panel and scroll its item list, keeping the Save/Cancel bar pinned. */
.account-panel[data-panel="sidebar"] { display: flex; flex-direction: column; max-height: 58vh; }
.account-panel[data-panel="sidebar"] .layout-editor { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.account-panel[data-panel="sidebar"] .layout-editor-actions {
    position: sticky; bottom: 0; margin-top: 0;
    background: var(--surface);
}

/* ── Dashboard tiles — inline edit mode (gear → per-tile ×, drag, + add) ──── */
.tile { position: relative; }
.tile-remove {
    position: absolute; top: -8px; right: -8px;
    display: none; align-items: center; justify-content: center;
    width: 22px; height: 22px; padding: 0;
    border-radius: 50%; border: 2px solid var(--surface);
    background: var(--danger); color: #fff; cursor: pointer;
    box-shadow: var(--shadow); z-index: 3;
}
.tile-remove svg,.tile-remove i[data-lucide] { width: 12px; height: 12px; }
.tile-remove:hover { filter: brightness(1.08); }
.grid.tiles.editing .tile { cursor: move; }
.grid.tiles.editing .tile:hover { transform: none; box-shadow: var(--shadow); border-color: var(--border); }
.grid.tiles.editing .tile-remove { display: inline-flex; }
.tile.sortable-ghost { opacity: .45; }
[data-tiles-edit-toggle][aria-pressed="true"] { background: var(--surface-3); color: var(--text); border-color: var(--border); }
.tiles-status { font-size: 12px; color: var(--text-muted); }
.tiles-status.is-error { color: var(--danger); }

/* Add-a-workspace picker (inside the tiles "Add" modal) */
.tiles-add-list { display: flex; flex-direction: column; gap: 8px; }
.tiles-add-item {
    display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
    padding: 10px 12px; border: 1px solid var(--border); border-radius: 10px;
    background: var(--surface-2); color: var(--text); cursor: pointer; font: inherit;
}
.tiles-add-item:hover { background: var(--surface-3); border-color: var(--border-strong); }
.tiles-add-item .ic { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; flex: 0 0 auto; }
.tiles-add-item .ic svg,.tiles-add-item .ic i[data-lucide] { width: 17px; height: 17px; }
.tiles-add-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1 1 auto; }
.tiles-add-text strong { font-size: 14px; font-weight: 600; }
.tiles-add-text span { font-size: 12px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tiles-add-plus { color: var(--text-muted); display: inline-flex; flex: 0 0 auto; }
.tiles-add-plus svg,.tiles-add-plus i[data-lucide] { width: 18px; height: 18px; }

/* The `hidden` attribute must win over the flex/inline-flex `display` these set —
   author display rules otherwise beat the UA `[hidden]{display:none}`. Used by the
   dashboard editors to hide widgets and to show the "Add" button only in edit mode.
   (Global `[hidden]!important` is avoided — it would break .account-panel[hidden].) */
.dash-widget[hidden] { display: none; }
.btn[hidden] { display: none; }
/* Same trap, one level up: `.field` is display:flex, so a `hidden` field label
   (e.g. the social composer's schedule time, shown only for "Schedule…") stayed
   fully visible — label, input and all. */
.field[hidden] { display: none; }

/* ── Overview widget grid — a standardized "bento" grid on a 4-column base with a
   --dash-row row height for CHART cards, so every widget snaps to the layout and
   heights always line up. Widgets span whole columns AND rows. Four sizes:
     stat  1×1 — a KPI number card / tiny chart
     tile  2×1 — a small chart (sparkline or donut) with a headline number
     panel 2×2 — a full chart with axes
     wide  4×2 — a full-width chart (dense time series)
   A chart card fills its grid cell and the chart flexes to fill the card, so a chart
   is exactly as tall as its tile. A KPI card is the ONE exception: it sizes to its
   own content, so it matches the .inv-kpi tile on Inventory instead of being padded
   out to a chart's height. ── */
/* minmax(0,1fr) — NOT plain 1fr (=minmax(auto,1fr)) — so a tile with a wide, non-wrapping
   value/label cannot stretch its column past the others; every column stays exactly equal. */
/* Rows are content-sized; --dash-row is the height a CHART card claims (what the
   grid used to hand every row). A KPI card is shorter than a chart — it holds a
   label, a number and a delta, not a plot — so a fixed row height stretched it
   past the identical .inv-kpi tile on Inventory. Rows now size to what they hold:
   a row of KPI tiles is exactly one KPI tall, a row with a chart is --dash-row. */
.grid.widgets { --dash-row: 158px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); grid-auto-rows: auto; gap: 16px; }
.dash-widget { position: relative; display: flex; min-width: 0; }
.dash-widget > .kpi, .dash-widget > .dash-widget-card { flex: 1 1 auto; width: 100%; height: 100%; min-width: 0; }
/* A KPI widget is exactly as tall as its content — the SAME height as the .inv-kpi
   tiles on Inventory (identical padding / label / value / delta metrics). Both the
   grid item and the card have to opt out of the stretch: the item out of the row's
   `align-self:stretch`, the card out of `height:100%` above. */
.dash-widget:has(> .kpi) { align-self: start; }
.dash-widget > .kpi { height: auto; }
/* …and because it sizes to its own content, a tile whose sub-line uses the
   second row would stand 18px taller than its neighbours in the same auto-sized
   row (they are top-aligned, so the row would read ragged). Every Overview KPI
   RESERVES both rows — the "reserve the box" rule — so the strip stays level
   whether a tile carries a `.delta-note` or not. */
.dash-widget > .kpi .delta { min-height: 36px; }
/* Chart cards keep the heights the fixed-row grid gave them: a stat/tile chart is
   one row tall, a panel/wide chart two rows plus the gap between them. */
.dash-widget.w-stat > .dash-widget-card,
.dash-widget.w-tile > .dash-widget-card { min-height: var(--dash-row); }
.dash-widget.w-panel > .dash-widget-card,
.dash-widget.w-wide > .dash-widget-card { min-height: calc(var(--dash-row) * 2 + 16px); }
.dash-widget.w-stat  { grid-column: span 1; grid-row: span 1; }
.dash-widget.w-tile  { grid-column: span 2; grid-row: span 1; }
.dash-widget.w-panel { grid-column: span 2; grid-row: span 2; }
.dash-widget.w-wide  { grid-column: span 4; grid-row: span 2; }
.dash-widget[data-widget-href] { cursor: pointer; }
.grid.widgets.editing .dash-widget { cursor: move; }
.grid.widgets .tile-remove { display: none; }
.grid.widgets.editing .tile-remove { display: inline-flex; }
.grid.widgets.editing .dash-widget .kpi:hover,
.grid.widgets.editing .dash-widget .dash-widget-card:hover { transform: none; box-shadow: var(--shadow); }
/* Chart cards mirror the KPI card exactly — same surface / border / shadow /
   padding, a `.lbl` label at the top (no divider line, no `.card-head`), a `.val`
   headline number, and the chart filling the rest. So a stat-size chart tile is
   visually identical in structure to a KPI tile. */
.dash-widget-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 18px 18px 12px;
  display: flex; flex-direction: column; height: 100%; overflow: hidden;
}
.dash-widget-card .lbl { font-size: 12.5px; color: var(--muted); display: flex; align-items: center; gap: 7px; font-weight: 500; min-width: 0; }
.dash-widget-card .lbl svg,.dash-widget-card .lbl i[data-lucide] { width: 15px; height: 15px; flex: 0 0 auto; }
/* "Opens a report" arrow on a chart widget: always laid out, shown only once the
   widget's fetch set data-widget-href (a viewer who can't open the report gets none). */
.dash-widget-card .lbl .widget-link-arrow { margin-left: auto; color: var(--muted); visibility: hidden; }
.dash-widget[data-widget-href] .dash-widget-card .lbl .widget-link-arrow { visibility: visible; }
/* A card a link can jump to (…#section) clears the fixed 60px topbar. */
.card[id] { scroll-margin-top: 76px; }
.dash-widget-card .lbl span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-widget-card .val { font-family: "Space Grotesk"; font-size: 28px; font-weight: 600; margin-top: 8px; letter-spacing: -.02em; line-height: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-widget-chart { position: relative; flex: 1 1 auto; min-height: 0; margin-top: 8px; }
.dash-widget-msg { padding: 4px 0; font-size: 13px; }
/* Responsive: collapse spans so nothing overflows the narrower grids. Uses
   .grid.widgets specificity so it wins over the base rule above. */
@media(max-width:1180px){
  .grid.widgets { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dash-widget.w-wide { grid-column: span 2; }
}
/* Phone: stat tiles go TWO-UP (a label and a number need half a screen, not a
   whole one — seven stacked tiles were a screen and a half of scrolling); the
   tile, panel and wide widgets keep the full width. (2026-09-26) */
@media(max-width:680px){
  .grid.widgets { grid-template-columns: repeat(2, minmax(0, 1fr)); --dash-row: 140px; }
  .dash-widget.w-stat { grid-column: span 1; }
  .dash-widget.w-tile, .dash-widget.w-panel, .dash-widget.w-wide { grid-column: span 2; }
}


/* ============================================================
   Shared components consolidated from style.css 2026-06-20:
   modal (.modal-content/.close-button/.styled-modal/.modal-footer),
   kanban (.kanban-column/.kanban-card/.image-status-label),
   findings (.finding-details/.finding-status). Semantic names kept;
   mirrored to examples/app.css for the drift gate. Candidates for
   later vocab refinement (.modal/.board fragments).
   ============================================================ */
.modal-content {
    background-color: var(--surface);
    border-radius: 14px;
    border: 1.25px solid var(--border);
    box-shadow: 0 20px 60px rgba(16, 42, 94, 0.35);
    position: relative;
    padding: 20px;
    width: 80%;
    max-width: 1100px;

    /* This ensures tall modals don't go off-screen */
    max-height: calc(var(--vh) * .9);
    display: flex;
    flex-direction: column;
}/*
so let its content scroll inside the modal instead of overflowing the page. */
#priceModal .modal-content {
    overflow-y: auto;
    padding: 24px 28px 28px;
    gap: 4px;
}
.close-button {
    color: var(--text-muted);
    /* A <button> does NOT inherit the page font (the UA sets its own), so a text
       × close would render in the UA font while an identical <span> one renders in
       ours. Inherit it so both spell the same glyph. */
    font-family: inherit;
    position: absolute;
    top: 12px;
    right: 22px;
    /* Square ghost icon-button — same affordance as the canonical .x-modal__close. */
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 28px;
    line-height: 1;
    font-weight: bold;
    border-radius: 8px;
    cursor: pointer;
    transition: background .12s, color .12s;
}/*
same top/height. */
.modal-back-button {
    position: absolute;
    top: 12px;
    right: 56px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    background: none;
    border: none;
    color: var(--text-faint);
    cursor: pointer;
}
.modal-back-button:hover {
    color: var(--text-strong);
}/*
so it needs more size to read as the same visual weight. */
.modal-back-button i, .modal-back-button svg.lucide {
    width: 28px;
    height: 28px;
    stroke-width: 2.5;
}
.close-button:hover,
.close-button:focus {
    background: var(--surface-3);
    color: var(--text);
    text-decoration: none;
    cursor: pointer;
}
/* Image Upload Cell Styles */
.image-cell {
    vertical-align: middle;
}
/* UPDATED Modal Styles */
.modal-content.large {
    max-width: 950px;
}
.styled-modal h2 {
    margin: 0 0 4px;
    font-family: var(--font-display);
    font-size: 16px;
    font-weight: 600;
    letter-spacing: -.02em;
    color: var(--text-strong);
}
.styled-modal .modal-subtitle {
    margin: 0 0 1.25rem;
}
.styled-modal .modal-footer {
    display: flex;
    justify-content: flex-end;
    gap: 0.75rem;
    margin-top: 1.5rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--border-light);
}
.styled-modal .primary-btn,
.styled-modal .secondary-btn,
.styled-modal .danger-btn {
    background-color: var(--surface);
    color: var(--text-subtle);
    border: 1px solid var(--border-subtle);
    font-weight: 600;
    transition: background-color 0.2s, color 0.2s, border-color 0.2s;
}
.styled-modal .primary-btn:hover,
.styled-modal .secondary-btn:hover {
    background-color: var(--surface-2);
    border-color: var(--border);
}
/* Filled primary (matches the canonical .btn.primary) so the modal's main action
   reads as a prominent button, not a faint outline. Accent fill + on-brand text =
   readable in both themes. */
.styled-modal .primary-btn {
    background: linear-gradient(135deg, var(--accent), var(--accent-end, var(--accent-strong, var(--accent))));
    color: var(--on-brand);
    border-color: var(--accent);
    box-shadow: 0 2px 6px color-mix(in srgb, var(--accent) 26%, transparent);
}
.styled-modal .primary-btn:hover { filter: brightness(1.06); background-color: transparent; }
.styled-modal .danger-btn {
    color: var(--danger);
    border-color: var(--pill-red-border);
}
.styled-modal .danger-btn:hover {
    background-color: var(--danger);
    color: var(--on-brand);
    border-color: var(--danger);
}/*
modern spacing. */
#consignmentModal .modal-content {
    /* The Add Item step is a 6-tile chooser: give it room so the tiles lay out
       3-across with their title + description on one line each (see
       .add-item-tiles in edit-panels.css). min() keeps the floor from
       overflowing a phone, where mobile.css wants 94vw. */
    width: 88%;
    min-width: min(640px, 94vw);
    max-width: 1140px;
    height: auto;
    padding: 28px 36px 32px;
}
#consignmentModal .modal-subtitle {
    margin: 0 0 1.25rem;
}
/* Footer rows inside this modal: tighter and centered.
   Scoped to #consignmentModal so it outranks the later .modal-footer rule. */
#consignmentModal .consignment-actions {
    justify-content: center;
    border-top: none;
    margin-top: 0.9rem;
    padding-top: 0;
    gap: 0.75rem;
}
.close-button.lightbox {
    position: absolute;
    top: 20px;
    right: 35px;
    color: var(--text-inverse);
    font-size: 40px;
}
.modal-subtitle {
    margin: 0 0 1rem 0;
    color: var(--text-muted);
    font-size: 0.9rem;
}
.modal-footer {
    padding-top: 1rem;
    margin-top: 1rem;
    border-top: 1px solid var(--border);
    display: flex;
    justify-content: flex-end;
    /* Aligns buttons to the right */
    gap: 1rem;
}
.finding-history-item {
    gap: 1rem;
}
.finding-details {
    flex-grow: 1;
    display: flex;
    align-items: baseline;
    gap: 1rem;
}
.finding-details a {
    font-size: 0.9rem;
}
.finding-status select {
    padding: 0.5rem;
    border-radius: 4px;
    border: 1px solid var(--border-subtle);
}
.finding-status {
    display: flex;
    align-items: center;
    gap: 1rem;
}
.status-feedback {
    font-size: 0.9rem;
    font-style: italic;
    transition: color 0.3s;
}
.lock-status {
    font-size: 0.8rem;
    color: var(--danger);
    /* red */
    font-weight: 500;
    margin-top: 0.25rem;
}
.kanban-column {
    flex: 0 0 4vw;
    background-color: var(--bg);
    /* radius inherited from .col (var(--radius)) so the column corners match
       the .col-h header's top corners — don't hardcode a different value */
    display: flex;
    flex-direction: column;
}
.kanban-column-title {
    /* Used only with .col-h on kanban; defer box/colour to .col-h so the
       header keeps its blue --th-bg. Keep just the title-text truncation. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-weight: 600;
    margin: 0;
}
.kanban-column-cards {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    padding: 1rem;
}
/* --- The rest of the styles for cards are the same --- */
.kanban-card {
    background: var(--surface);
    border-radius: 6px;
    padding: 1rem;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.07);
    text-decoration: none;
    color: var(--text);
    border-left: 4px solid var(--info);
}
.kanban-card:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    transform: translateY(-2px);
}
.kanban-card .card-title {
    font-weight: 500;
    margin-bottom: 0.75rem;
}
.kanban-card .card-meta {
    font-size: 0.8rem;
    color: var(--text-muted);
}
.kanban-card.submitted {
    border-left-color: var(--success);
    opacity: 0.8;
}
/* A tray that's locked (open by another user) is not navigable — the whole card link
   (kanban), the tray-name link and row (trays list), or the Review button and row
   (review-requests report) are click-guarded in JS; these rules give the matching
   visual affordance. Kept in app.css so all three views share it. Each guarded row
   also carries `cursor:not-allowed` so the click-anywhere-in-the-row shortcut stops
   advertising itself as clickable. */
.kanban-card.locked {
    opacity: 0.6;
    cursor: not-allowed;
}
.kanban-card.locked:hover {
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.07);
    transform: none;
}
.tray-list-item.locked a.t-strong,
.review-card tbody tr.locked a.review-btn {
    opacity: 0.6;
    cursor: not-allowed;
}
.tray-list-item.locked,
.review-card tbody tr.locked {
    cursor: not-allowed;
}
.modal-footer {
    display: flex;
    justify-content: flex-end;
    /* Aligns buttons to the right */
    gap: 1rem;
    padding-top: 1rem;
    margin-top: 1rem;
    border-top: 1px solid var(--border);
}
#conditionMatrixModal .modal-footer {
    flex-shrink: 0;
    border-top: 1px solid var(--border);
    padding-top: 12px;
    margin-top: 8px;
    background: var(--surface);
}
/* Camera Icon Styles */
.image-status-label {
    display: inline-flex;
    align-items: center;
    margin-right: 0.4rem;
    color: var(--text-subtle);
}
.image-status-label svg.lucide,.image-status-label i[data-lucide] {
    width: 24px;
    height: 24px;
    stroke-width: 2.4;
}
.image-status-label.camera-red { color: var(--danger); }
.image-status-label.camera-orange { color: var(--signal); }
.image-status-label.camera-yellow { color: var(--warning); }
.image-status-label.camera-green { color: var(--success); }
.image-status-label.camera-grey { color: var(--text-faint); }


/* ============================================================
   Page-specific stragglers consolidated from style.css 2026-06-20:
   shared .delete-*-btn buttons, .form-group, .meta-tag/.meta-info,
   .request-info, .matrix-group-admin, .truncated-title-part.
   Mirrored to examples for the drift gate.
   ============================================================ */
.meta-info {
    font-size: 0.8rem;
    color: var(--text-muted);
    margin-top: 0.5rem;
    display: flex;
    gap: 1rem;
}
.meta-info span {
    background-color: var(--surface-3);
    padding: 0.2rem 0.5rem;
    border-radius: 4px;
}
.request-info {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}
.request-info a {
    font-weight: 600;
    font-size: 1.1rem;
    color: var(--link);
    text-decoration: none;
}
.request-info a span {
    font-weight: 400;
    color: var(--text-subtle);
}
.delete-tray-btn,
.delete-request-btn,
.delete-row-btn,
.delete-panel-btn,
.delete-field-btn,
.delete-user-btn,
.delete-category-btn,
.delete-finding-btn,
.delete-role-btn,
.delete-option-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    background-color: var(--pill-red-bg);
    color: var(--danger);
    padding: 0.4rem 0.8rem;
    font-size: 0.8rem;
    font-weight: 600;
    border: 1px solid var(--danger);
    border-radius: 8px;
    line-height: 1.2;
    cursor: pointer;
    transition: background-color 0.2s, color 0.2s, border-color 0.2s;
}
/* Fill red only on hover so the destructive action reads clearly without shouting */
.delete-tray-btn:hover:not(:disabled),
.delete-request-btn:hover:not(:disabled),
.delete-row-btn:hover:not(:disabled),
.delete-panel-btn:hover:not(:disabled),
.delete-field-btn:hover:not(:disabled),
.delete-user-btn:hover:not(:disabled),
.delete-category-btn:hover:not(:disabled),
.delete-finding-btn:hover:not(:disabled),
.delete-role-btn:hover:not(:disabled),
.delete-option-btn:hover:not(:disabled) {
    background-color: var(--danger);
    color: var(--on-brand);
    border-color: var(--danger);
}
/* Lucide renders a 24px svg unless told otherwise — size it here so an ICON-ONLY
   delete button (the trash-can) is the same size everywhere, not just inside the
   admin tables that happened to scope their own rule. */
.delete-tray-btn svg,
.delete-request-btn svg,
.delete-row-btn svg,
.delete-panel-btn svg,
.delete-field-btn svg,
.delete-user-btn svg,
.delete-category-btn svg,
.delete-finding-btn svg,
.delete-role-btn svg,
.delete-option-btn svg,.delete-tray-btn i[data-lucide],.delete-request-btn i[data-lucide],.delete-row-btn i[data-lucide],.delete-panel-btn i[data-lucide],.delete-field-btn i[data-lucide],.delete-user-btn i[data-lucide],.delete-category-btn i[data-lucide],.delete-finding-btn i[data-lucide],.delete-role-btn i[data-lucide],.delete-option-btn i[data-lucide] {
    width: 16px;
    height: 16px;
}
/* A blocked delete must READ as blocked (and not flash red on hover) — the group
   had no disabled state outside the admin pages. */
.delete-tray-btn:disabled,
.delete-request-btn:disabled,
.delete-row-btn:disabled,
.delete-panel-btn:disabled,
.delete-field-btn:disabled,
.delete-user-btn:disabled,
.delete-category-btn:disabled,
.delete-finding-btn:disabled,
.delete-role-btn:disabled,
.delete-option-btn:disabled {
    opacity: 0.45;
    filter: grayscale(1);
    cursor: not-allowed;
}
.form-group {
    display: flex;
    align-items: center;
    /* Vertically centers the content within each group */
    flex-grow: 1;
    justify-content: center;
}
/* This adds the divider to the left of every group except the first one */
.form-group:not(:first-child) {
    border-left: 1px solid var(--border);
    /* The divider line */
    padding-left: 1rem;
    /* Space between the line and the element */
}
.truncated-title-part {
    display: inline-block;
    /* Allows the element to have a width */
    max-width: 500px;
    /* Set a max width before it truncates */
    vertical-align: bottom;
    /* Aligns it nicely with the preceding text */

    /* These rules create the left-side ellipsis */
    direction: rtl;
    text-align: left;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.meta-tag {
    font-size: 0.8rem;
    background-color: var(--surface-3);
    color: var(--text-subtle);
    padding: 0.2rem 0.6rem;
    border-radius: 4px;
}
.meta-tag.autofill {
    background-color: var(--pill-green-bg);
    /* Light blue */
    color: var(--pill-green-text);
}
.meta-tag.info {
    background-color: var(--pill-amber-bg);
    /* Light yellow */
    color: var(--pill-amber-text);
}
/* Styles for the Matrix Admin Page */
.matrix-group-admin {
    border: 1px solid var(--border-subtle);
    border-radius: 8px;
    padding: 1.5rem;
    margin-bottom: 1.5rem;
}
.matrix-group-admin legend {
    font-size: 1.2rem;
    padding: 0 0.5rem;
    margin-left: 1rem;
    font-weight: bold;
}
.matrix-group-admin legend button {
    font-size: 0.8rem;
    margin-left: 1rem;
    background-color: var(--danger);
    color: var(--on-brand);
}


/* Modal backdrop overlay (moved from style.css 2026-06-20 so de-linked modal
   pages keep their overlay; mirrored to examples). */
.modal {
    display: none;
    /* Hidden by default */
    position: fixed;
    z-index: 1000;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: var(--overlay);
    backdrop-filter: blur(2px);

    /* This is the key: Use flexbox to center the content */
    display: none;
    /* Will be changed to 'flex' by JavaScript */
    justify-content: center;
    align-items: center;
}

/* ============================================================
   Minimal native-<button> reset (2026-06-20): strip native chrome so
   pages migrated off style.css don't show grey UA buttons, but DON'T
   impose padding/size/colour — styled classes (.btn/.small-btn/etc.)
   own those, and standalone pages (login) keep their custom buttons.
   ============================================================ */
button { font-family: inherit; cursor: pointer; -webkit-appearance: none; appearance: none; }

/* ============================================================
   Shared thumbnail hover-zoom preview — a single floating <img> positioned
   with `fixed` coords in JS so it can't be clipped by table/row overflow.
   Mirrors the tray editor's #rail-thumb-zoom; used by list thumbnails that
   opt in (e.g. the trays manager .tray-thumb).
   ============================================================ */
/* On touch the preview opens on press-and-hold (thumb-hover-preview.js); the
   browser's own long-press image callout would race it. */
img[data-hover-preview] { -webkit-touch-callout: none; }
.thumb-hover-preview {
  position: fixed;
  z-index: 100000;
  width: 260px;
  height: 260px;
  object-fit: contain;
  background: var(--surface);
  border: 1.25px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 10px 30px rgba(16, 42, 94, .25);
  padding: 6px;
  pointer-events: none;
}

/* ============================================================================
   Shared fragment primitives (partials/ui-*.ejs)
   Token-only. These back the reusable EJS fragments so pages stop re-inventing
   tables, toolbars, kpi rows, pagination and form fields inline. Add new shared
   chrome HERE, never as a per-page inline <style>.
   ============================================================================ */

/* --- page-head actions cluster (right side of .page-head) --- */
.page-head .ph-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap}

/* --- wrap-cell: opt a table cell OUT of the global `tbody td{white-space:nowrap}` ---
   The default keeps numeric/SKU columns on one line, but long text/JSON/diff cells
   then force a horizontal scroll. Add class="wrap-cell" to such a <td> (or <th>)
   instead of re-declaring this rule inline on every page. The max-width is what
   actually forces the wrap (white-space:normal alone won't, since an auto-layout
   table just widens the column); override it inline per cell when you need a
   different cap, e.g. style="max-width:640px". !important defeats the global nowrap. */
.wrap-cell{white-space:normal!important;overflow-wrap:anywhere;word-break:break-word;max-width:460px}
/* A table's main text column beside many single-line ones (a report's case or title
   column): without a floor, the single-line columns take their full width and squeeze
   it to a word per line. Phone card rows drop the floor (mobile.css). */
.wrap-cell.wide{min-width:18rem}
/* A second wrapping column beside it (a label of a few words) keeps a smaller floor. */
.wrap-cell.mid{min-width:11rem}

/* --- cell-ellip: truncate a long table cell to ONE line with an ellipsis ---
   The wrap-cell alternative for cells that should stay single-line (names,
   titles). Put it on an inline element INSIDE the <td> (a <span> or <a>) with
   the full text in title="" for hover. Override max-width inline if a column
   needs a different cap. */
.cell-ellip{display:inline-block;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}
/* .fill — clip to whatever width the COLUMN already has, instead of the 220px
   default. This is the variant to use inside a table-layout:fixed table, where
   the column may be narrower than 220px and the default cap would let the text
   paint over its neighbour — and the one to pair with `.col-cap` on the <td>
   (the table helpers above) to cap a column in an ordinary auto-layout table:
   the column can never grow past the cap, and the text uses every pixel the
   column DOES get. That is what a .fluid cap cannot do — .fluid caps the TEXT,
   so once the table has slack the column keeps growing while the span stays at
   its cap and the value is cut with an … that sits beside a hole. */
.cell-ellip.fill{max-width:100%}
/* .fluid — the cap follows the VIEWPORT instead of a fixed 220px, so the column
   gives width back on a laptop and uses it on a wide monitor without anyone
   picking a pixel. The floor keeps it readable; the ceiling keeps it from
   dominating the table. Because the cap is a max, the column still shrinks to
   whatever the row actually holds — only a value longer than the cap is cut. */
.cell-ellip.fluid{max-width:clamp(9rem,15vw,26rem)}
/* .fluid-sm — the same viewport-following cap, one size down, for a SHORT label
   column (a store, a location, a status word). .fluid's floor (9rem) is wider than
   such a value ever renders, so its cap never bites and the column sits at its full
   natural width; this one is deliberately narrower than the text on a laptop, so the
   value IS cut with an … there, and opens out to the full name on a wide monitor. */
.cell-ellip.fluid-sm{max-width:clamp(4.5rem,7vw,11rem)}
/* A short suffix that must SURVIVE the truncation — "+ 3 more", a count, a unit.
   It has to sit OUTSIDE the .cell-ellip span: inside it, it is the first thing
   the ellipsis eats, so a row with a long name showed no "+ N more" at all. It
   repeats .cell-ellip's vertical-align so the clipped name and the suffix sit on
   one line without the inline-block baseline wobble. */
.cell-ellip + .cell-more{vertical-align:middle;white-space:nowrap}

/* --- small shared text utils (match the .t-strong/.t-mut convention) --- */
.t-red{color:var(--red)}
.t-green{color:var(--green)}
.t-cap{font-size:.78rem;color:var(--muted)}

/* --- section subheading INSIDE a card body (below .card-head, above a
   field-grid / table). Was `.inv-subhead` in inventory.css, which pages
   outside the inventory module referenced without loading that sheet. --- */
.subhead{margin:1.5rem 0 .5rem;font-size:.95rem;font-weight:600;color:var(--text)}
.subhead:first-child{margin-top:0}
.subhead i,.subhead svg{width:16px;height:16px;vertical-align:-2px;margin-right:6px}

/* --- compact "linked SKU" link under a cell's main value (consignments,
   and anywhere a row points at its inventory product). --- */
.sku-link{display:flex;align-items:center;gap:3px;margin-top:2px;font-size:.72rem;font-weight:600;color:var(--accent);text-decoration:none;min-width:0}
.sku-link:hover{text-decoration:underline}
.sku-link i,.sku-link svg{width:12px;height:12px;flex:0 0 auto}
.sku-link .sku-val{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}

/* --- card-head headings: keep the Lucide icon at heading size (its 24px default
   makes the heading taller than a sibling pill and throws the row out of line) --- */
.card-head h3{display:inline-flex;align-items:center;gap:8px}
.card-head h3 i,.card-head h3 svg{width:18px;height:18px}

/* --- chart canvas container (report pages with Chart.js canvases) --- */
.chart-container{position:relative;height:400px}
@media(max-width:768px){.chart-container{height:300px}}

/* --- vertical rhythm: stacked cards / kpi rows that sit directly in a .view ---
   (a dozen report pages re-declared this inline; an inline margin on the element
   still wins where a page wants a different gap). --- */
/* --- spinner util: for a button that swaps its leading icon while working (keeping
   the label, so the button — and any table column it sits in — never resizes) --- */
@keyframes spin{to{transform:rotate(360deg)}}
.spin{animation:spin 1s linear infinite}
/* One turn on a click (the CoPilot sparkle in the topbar) — the .spin rotation,
   played once. Re-added per click by the script that sets it. */
.spin-once{animation:spin .6s ease-in-out}
@media(prefers-reduced-motion:reduce){.spin-once{animation:none}}

/* --- upload overlay: THE one "please wait" modal while a file or image uploads
   (or converts). Scrim + a card with a spinner and a short sentence-case label
   ("Uploading image…", "Converting photo…"). Never write the markup — call
   App.uploadOverlay.show('Uploading image…') / .hide() (/js/upload-overlay.js,
   app-wide via partials/icons), which builds it once per page. --- */
.upload-loading-overlay{position:fixed;inset:0;background:var(--overlay);display:none;align-items:center;justify-content:center;z-index:9999}
.upload-loading-overlay.open{display:flex}
.upload-loading-card{background:var(--surface);padding:1.6rem 2.2rem;border-radius:12px;box-shadow:var(--shadow-lg);display:flex;align-items:center;gap:1rem;color:var(--text);font-weight:600}
.upload-spinner{flex:0 0 auto;width:26px;height:26px;border:3px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}

/* --- composer: THE one "write a message" box (CoPilot chat, CRM inbox reply).
   The box IS the border: the textarea sits flush inside it at full width and
   its buttons are pinned to the bottom-right corner, so adding a button never
   narrows or lifts the box. Bottom padding reserves that corner so text never
   runs under the buttons.
     <div class="composer">
       <textarea class="composer-input" rows="4"></textarea>
       <div class="composer-actions">[btn ghost sm …] <button class="composer-send" aria-label="Send">…</button></div>
     </div> --- */
/* ── SUPPORT TICKET THREAD — the app's Support page and the Sellevry Console's.
   A ticket's messages, oldest first; the reader's own side (.mine) sits right.
   An internal note (console only) is dashed. No attachments. */
.ticket-thread{display:flex;flex-direction:column;gap:12px}
.ticket-msg{border:1px solid var(--border);background:var(--surface);border-radius:12px;padding:12px 14px;max-width:85%;align-self:flex-start}
.ticket-msg.mine{align-self:flex-end;background:var(--accent-soft);border-color:transparent}
.ticket-msg.internal{background:var(--surface-2);border:1px dashed var(--border)}
.ticket-msg-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:13px;margin-bottom:6px}
.ticket-msg-head > .t-mut:last-child{margin-left:auto}
.ticket-msg-body{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.55;font-size:14px;color:var(--text)}
.composer.ticket-reply{margin-top:16px}
.composer{position:relative;border:1px solid var(--border);background:var(--surface-2);border-radius:12px}
.composer:focus-within{border-color:var(--accent)}
.composer-input{display:block;width:100%;box-sizing:border-box;border:0;background:transparent;border-radius:12px;outline:none;color:var(--text);font-family:inherit;font-size:14px;line-height:1.4;padding:9px 11px 44px;resize:none;min-height:130px;max-height:232px;overflow-y:auto}
.composer-actions{position:absolute;right:6px;bottom:6px;display:flex;align-items:center;gap:6px}
.composer-send{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:none;border-radius:8px;cursor:pointer;background:var(--accent);color:var(--on-brand)}
.composer-send:disabled{opacity:.5;cursor:default}
/* flex-shrink:0 — mobile.css's `svg{max-width:100%}` would otherwise collapse
   the inline svg to 0 width inside the flex button and hide the arrow. */
.composer-send svg,.composer-send i[data-lucide]{width:17px;height:17px;flex-shrink:0;stroke:var(--on-brand);fill:none}

/* ======================= LOADING SKELETONS (canonical) =======================
   ONE loading look for the whole app: a flat --surface-3 placeholder with a
   --surface-2 highlight sweeping across it (1.2s). Use it EVERYWHERE content
   arrives after paint — never a "Loading…" string, never a bespoke spinner, and
   never a second copy of this recipe (there were four: the inventory dashboard,
   the drift report, the product gallery and the thumbnail loader — all now point
   here).

   The rule that makes it worth doing: a skeleton must occupy the SAME BOX the
   real content will, so nothing jumps when the fetch resolves. That's why the
   KPI/row/text variants below carry explicit heights.

   Primitive — one shimmering placeholder block:
     <span class="skel-bar"></span>                (fills its box)
     <span class="skel-bar sm w-60"></span>        (shorter, 60% wide)
   Composites:
     <div class="kpi kpi-skel"><div class="lbl"><span class="skel-bar"></span></div>
       <div class="val"><span class="skel-bar"></span></div></div>
     <tr class="skel-row"><td><span class="skel-bar"></span></td>…</tr>
     <div class="skel-block"></div>                (chart / panel area)
   Or from JS (public/js/format.js): App.skelKpis(n) / App.skelRows(cols, rows)
   / App.skelBlock(height) — they emit exactly this markup.                    */
@keyframes skel-shimmer{to{transform:translateX(100%)}}

.skel-bar{display:block;position:relative;overflow:hidden;width:100%;height:12px;
  border-radius:6px;background:var(--surface-3)}
.skel-bar::after{content:"";position:absolute;inset:0;transform:translateX(-100%);
  background:linear-gradient(90deg,transparent,var(--surface-2),transparent);
  animation:skel-shimmer 1.2s ease-in-out infinite}
/* height + width shapes (so a page never sets these inline) */
.skel-bar.sm{height:9px;border-radius:5px}
.skel-bar.lg{height:20px}
.skel-bar.w-25{width:25%}.skel-bar.w-40{width:40%}.skel-bar.w-50{width:50%}
.skel-bar.w-60{width:60%}.skel-bar.w-75{width:75%}

/* A larger filled area — a chart, a map, a panel that hasn't loaded. Give it a
   height inline (style="height:400px") or drop it inside .chart-container. */
.skel-block{position:relative;overflow:hidden;width:100%;height:100%;min-height:80px;
  border-radius:var(--radius);background:var(--surface-3)}
.skel-block::after{content:"";position:absolute;inset:0;transform:translateX(-100%);
  background:linear-gradient(90deg,transparent,var(--surface-2),transparent);
  animation:skel-shimmer 1.2s ease-in-out infinite}
.chart-container>.skel-block{height:100%}
/* stock heights, so a page never sets one inline */
.skel-block.sm{height:120px}.skel-block.md{height:220px}.skel-block.lg{height:340px}

/* KPI tile skeleton — the real .kpi box, with bars where the label and number
   go. The explicit .lbl/.val heights reproduce the real tile's line boxes
   (12.5px label + 30px value), so the row reserves its exact height and the
   numbers swap in without a jump. */
.kpi.kpi-skel{pointer-events:none}
.kpi-skel .lbl{height:18px}
.kpi-skel .val{height:33px;display:flex;align-items:center} /* = the real .val box: 30px * 1.1 */
.kpi-skel .lbl .skel-bar{width:60%;height:11px}
.kpi-skel .val .skel-bar{width:45%;height:22px}

/* Table skeleton rows — drop N of these in the <tbody> instead of a
   "Loading…" cell, so the table keeps its height and column rhythm. Cells
   alternate width so it reads as data, not as a progress bar. */
tr.skel-row{pointer-events:none}
/* keep the real cell padding (12/16 + border) and give the bar the text line
   box's height, so a skeleton row is the same height as the row that replaces it */
tr.skel-row>td>.skel-bar{height:12px;margin:4px 0;width:80%}
tr.skel-row>td:nth-child(2n)>.skel-bar{width:55%}
tr.skel-row>td:nth-child(3n)>.skel-bar{width:68%}
tr.skel-row>td:last-child>.skel-bar{width:40%}

/* Chart-shaped skeletons — a card whose body is a chart shows a placeholder in
   the SHAPE of that chart, so the card keeps its height and the loading state
   reads as "this chart is coming", not as a generic grey box.
     .skel-bars — a bar chart (.bars, 170px). Bar heights vary per column.
     .skel-list — a channel/entity list (.list-row rhythm: 34px thumb + 2 lines
                  of text + a pill on the right).
   Both are emitted by App.skelBars(n) / App.skelList(n) in /js/format.js. */
.skel-bars{display:flex;align-items:flex-end;gap:12px;height:170px;padding-top:10px}
.skel-bars>.skel-bar{flex:1;max-width:38px;margin:0 auto;border-radius:7px 7px 3px 3px}
.skel-bars>.skel-bar:nth-child(6n+1){height:55%}
.skel-bars>.skel-bar:nth-child(6n+2){height:80%}
.skel-bars>.skel-bar:nth-child(6n+3){height:38%}
.skel-bars>.skel-bar:nth-child(6n+4){height:92%}
.skel-bars>.skel-bar:nth-child(6n+5){height:64%}
.skel-bars>.skel-bar:nth-child(6n+6){height:47%}

.skel-list>.skel-list-row{display:flex;align-items:center;gap:14px;padding:14px 0;
  border-bottom:1px solid var(--border)}
.skel-list>.skel-list-row:last-child{border-bottom:none}
.skel-list .skel-lines{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px}
.skel-list .skel-lines>:where(.skel-bar:first-child){width:45%;height:11px}
.skel-list .skel-lines>:where(.skel-bar:last-child){width:28%;height:9px}
/* two more .skel-bar shapes, usable anywhere (a standalone chip row, an avatar
   cell) — not only inside .skel-list */
.skel-bar.skel-thumb{width:34px;height:34px;border-radius:8px;flex-shrink:0}
.skel-bar.skel-pill{width:64px;height:20px;border-radius:20px;flex-shrink:0}
.skel-bar.skel-pill.xs{width:56px;height:18px}

/* A doughnut-chart placeholder — a ring centred in its box. */
.skel-donut{position:relative;width:100%;height:100%;min-height:80px;display:grid;place-items:center}
/* The ring is sized off the box's HEIGHT (a chart area is usually wider than it
   is tall — sizing off width overflowed the card), and drawn with a radial
   gradient so the hole stays proportional at any size. */
.skel-donut::before{content:"";display:block;height:min(84%,110px);aspect-ratio:1;border-radius:50%;
  background:radial-gradient(circle,transparent 0 54%,var(--surface-3) 55% 100%)}
.skel-donut::after{content:"";position:absolute;inset:0;transform:translateX(-100%);
  background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--surface-2) 70%,transparent),transparent);
  animation:skel-shimmer 1.2s ease-in-out infinite}
/* fill the parent instead of the stock 170px (a chart box that sets its own height) */
.skel-bars.fill{height:100%}

/* Overlay form — for a chart box that already owns its size (a <canvas> the
   charting library measures). The skeleton floats ON TOP so the canvas keeps
   its own layout, and the page removes the node when the chart draws. Put it in
   any position:relative box: <div class="skel-block" data-skel-overlay></div>. */
[data-skel-overlay]{position:absolute;inset:0;z-index:1;pointer-events:none}

/* A line of placeholder text inside a card / detail pane. (:where() keeps these
   at zero specificity so a per-bar .sm/.lg/.w-* modifier still wins.) */
.skel-text{display:flex;flex-direction:column;gap:9px}
.skel-text>:where(.skel-bar){height:11px}
.skel-text>:where(.skel-bar:last-child){width:55%}

/* A card BODY skeleton — a short label bar over a larger value bar. For a page
   whose KPI markup isn't the canonical .kpi tile (a bespoke .kpi-card box, a
   report's summary card): drop this inside the page's own card element so the
   card keeps its chrome and only its contents shimmer. App.skelCards(n, cls). */
.skel-kpi{display:flex;flex-direction:column;gap:10px}
.skel-kpi>:where(.skel-bar:first-child){width:58%;height:10px}
.skel-kpi>:where(.skel-bar:last-child){width:42%;height:20px}

/* Respect the OS "reduce motion" setting: keep the placeholder box (it still
   says "not loaded yet" and still reserves the space), drop the sweep. */
@media(prefers-reduced-motion:reduce){
  .skel-bar::after,.skel-block::after{animation:none}
  img.is-thumb-loading{animation:none !important}
}

/* Thumbnail skeleton — the same look, painted as the <img>'s OWN background
   (an <img> can't carry a ::after, and wrapping every thumbnail in the app was
   not worth it), so the image simply paints over it as it decodes. The classes
   are toggled by /js/thumb-skeleton.js, which tracks every list thumbnail on
   the page. `!important` is required: several thumbnails already carry a flat
   `background` from a compound selector that would otherwise erase the sweep. */
img.is-thumb-loading{
  background-color:var(--surface-3) !important;
  background-image:linear-gradient(90deg,transparent 0%,var(--surface-2) 50%,transparent 100%) !important;
  background-size:200% 100% !important;background-repeat:no-repeat !important;
  animation:thumb-skel-shimmer 1.2s ease-in-out infinite !important;
}
@keyframes thumb-skel-shimmer{0%{background-position:-150% 0}100%{background-position:250% 0}}
/* Once resolved (or broken) stop shimmering; any original flat background
   returns for the image's transparent areas. */
img.is-thumb-loaded,img.is-thumb-error{animation:none !important;background-image:none !important}

.view>.card,.admin-content>.card{margin-bottom:18px}
.view .grid.kpis{margin-bottom:18px}
/* Cards stacked INSIDE a panel/tab body (a JS-filled container, so they are not
   direct children of .view and the rule above can't reach them). Wrap them in
   .card-stack instead of giving the container a bespoke per-page margin. */
.card-stack{display:flex;flex-direction:column;gap:18px}
.card-stack>.card{margin-bottom:0}

/* --- toolbar variant: bottom-align controls (when fields stack a label above
   the input, so Apply/Reset line up with the date pickers) --- */
.toolbar.tb-end{align-items:flex-end}

/* --- toolbar: labeled fields share the search box's control band ------------
   A .toolbar that mixes a labeled .field (a .field-label stacked ABOVE its
   control) with bare controls (the 36px .tb-input search box, chips, buttons)
   used to look broken: the label makes the field ~21px taller, so a CENTERED
   row floats the search box halfway up it, and .input.sm (32px) sat 4px shorter
   than the search input on top of that. Bottom-align such a row automatically
   (same as .tb-end) and put those fields' small inputs on the shared 36px band,
   so search + selects read as one control line. Never patch this per page. */
.toolbar:has(.field>.field-label){align-items:flex-end}
.toolbar:has(.field>.field-label) .field .input.sm{height:36px;border-radius:9px;font-size:13px;padding:0 12px}
.toolbar:has(.field>.field-label) .field select.input.sm{--select-pad-end:30px;--select-chevron-x:10px}
/* Bare inline text in such a row (a result count, a pill) would otherwise sit on
   the row's bottom edge, ~8px below the inputs' text — give it the same 36px band. */
.toolbar:has(.field>.field-label)>span:not(.spacer):not(.tb-input){min-height:36px;display:inline-flex;align-items:center}

/* --- info-grid: a read-only "label + value" detail grid (audit/detail pages).
   Replaces the per-page purchase-info / tray-info / item-info duplicates. --- */
.info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem}
.info-field{background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius-sm);padding:.7rem .9rem;display:flex;flex-direction:column;gap:4px}
.info-label{font-size:.7rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;font-weight:600}
.info-value{font-size:1rem;font-weight:600;color:var(--text-strong);overflow-wrap:anywhere} /* a long link/reference wraps inside its card */
.info-value a{color:var(--link);text-decoration:none}
.info-value a:hover{color:var(--text-strong);text-decoration:underline}
/* Full-width field (e.g. a long free-text reason/note) inside an .info-grid. */
.info-field.span-all{grid-column:1/-1}
/* Long free-text value: wrap + keep line breaks, body weight instead of bold. */
.info-value.pre{white-space:pre-wrap;overflow-wrap:anywhere;font-weight:400;font-size:.95rem}
/* Compact variant: smaller type and narrower tiles for dense detail grids (e.g. inside
   modals). 180px lets an .x-modal.is-xxl hold five tiles a row, scrollbar included. */
.info-grid.compact{grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}
.info-grid.compact .info-label{font-size:.62rem}
.info-grid.compact .info-value{font-size:.85rem}
/* A muted second line under a value (a warehouse's location), like a table cell's .t-cap. */
.info-value > .t-cap{display:block;margin-top:2px;font-weight:400}

/* --- audit timeline: a vertical dotted timeline of events (accent dot + line).
   Replaces the drifted per-page copies (purchase-audit was hard-coded purple). --- */
.timeline{position:relative;padding-left:26px}
.timeline-item{position:relative;padding-bottom:1.1rem}
.timeline-item:last-child{padding-bottom:0}
.timeline-item::before{content:'';position:absolute;left:-22px;top:6px;width:10px;height:10px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 3px var(--surface),0 0 0 4px var(--border)}
.timeline-item::after{content:'';position:absolute;left:-18px;top:18px;width:2px;height:calc(100% - 10px);background:var(--surface-3)}
.timeline-item:last-child::after{display:none}
.timeline-time{font-size:.78rem;color:var(--text-muted);margin-bottom:4px}
.timeline-content{background:var(--surface);padding:.75rem .9rem;border-radius:var(--radius-sm);border:1px solid var(--border-light);border-left:3px solid var(--accent)}
.timeline-user{font-weight:700;color:var(--text-strong);margin-right:8px}

/* --- field-change diff: old (struck red pill) then new (green pill) value --- */
.field-change{font-size:.9rem;margin-top:6px}
.field-change .old-val{display:inline-block;color:var(--pill-red-text);text-decoration:line-through;background:var(--pill-red-bg);border:1px solid var(--pill-red-border);border-radius:6px;padding:1px 8px}
.field-change .new-val{display:inline-block;color:var(--pill-green-text);font-weight:700;background:var(--pill-green-bg);border:1px solid var(--pill-green-border);border-radius:6px;padding:1px 8px}
.extra-info{display:inline-block;background:var(--pill-blue-bg);color:var(--link);border:1px solid var(--pill-blue-border);padding:2px 8px;border-radius:999px;font-size:.78rem;font-weight:600;margin-left:4px}

/* --- styled-card: the report/detail card with an accent top-bar + icon header.
   Canonical home (was duplicated in reports.css and inline on ~10 pages). The
   theme-* modifier classes are inert no-ops (single design-system accent). --- */
.styled-card{background:var(--surface-gradient);border:1.25px solid var(--border);border-radius:14px;box-shadow:var(--shadow-md);padding:1.1rem 1.5rem 1.25rem;margin-bottom:1.5rem;position:relative;overflow:hidden}
.styled-card::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;background:var(--accent-gradient,var(--accent-bar))}
.styled-card .card-header{display:flex;align-items:center;gap:12px;margin-bottom:.6rem;padding-bottom:.7rem;border-bottom:1px solid var(--border-light)}
.styled-card .card-header h2{margin:0;padding:0;border:none;font-size:1.2rem;color:var(--text-strong);line-height:1.2;font-weight:700}
.styled-card .header-icon,.card-head .header-icon{width:38px;height:38px;border-radius:10px;background:var(--accent-soft);color:var(--accent);display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}
.styled-card .header-icon i,.card-head .header-icon i{width:20px;height:20px}
.styled-card .count-pill{margin-left:auto;background:var(--accent-soft);color:var(--accent);font-weight:600;font-size:.8rem;padding:3px 10px;border-radius:999px;border:1px solid var(--accent-border);white-space:nowrap}
.styled-card .card-subtitle{margin:0 0 .9rem 0;color:var(--text-muted);font-size:.9rem}
.styled-card.theme-blue,.styled-card.theme-purple,.styled-card.theme-green,.styled-card.theme-amber,.styled-card.theme-teal,.styled-card.theme-red{/* single accent — no per-card overrides */}

/* --- type layer (folded from the retired typography.css). The base
   h1-h4/.display (display face) and .mono are set near the top; these just
   extend the display face to title/kpi-value classes and mono to raw code +
   sku/number classes, app-wide. --- */
.page-title,.section-title,.t-kpi-value,.kpi-value{font-family:var(--font-display);font-weight:600;letter-spacing:-.02em}
code,kbd,samp,pre,.t-mono,.sku,.sku-code,.mono-num{font-family:var(--font-mono)}

/* --- sortable column headers: THE one sort indicator, app-wide ---
   Markup: the header cell carries class="sortable" (plus data-sort="<key>" for
   public/js/ui-table.js, which wires click-to-sort and toggles .sort-asc /
   .sort-desc) and an inner <span class="sort-arrow">.

   ONE look, everywhere — the inventory products table's: EVERY sortable column
   shows a faint "\21C5" so the column reads as sortable BEFORE it is clicked,
   and the active one swaps it for "\2191" / "\2193" at full strength. Both are
   drawn in --th-accent: the primary colour as it reads on the solid --th-bg
   header bar (raw --accent is 1.4:1 on the light theme's blue bar — see the
   token's note in theme.css). The label itself stays --th-fg: the arrow is what
   tells the sorted column apart, not a colour shift on the text.

   NOT scoped to .table — a server-sorted table (CRM -> Customers is a
   table.mcards whose headers are links to the next sort URL) carries the same
   span, and scoping the glyph to one table class is exactly how four rival sort
   vocabularies grew (.cu-arrow, .sort-ind x3, .inv-sortable). The span is a
   fixed 1em box so swapping "\21C5" -> "\2191" can never shift the header
   beside it (the same "reserve the box" rule as the loading skeletons). */
thead th.sortable{cursor:pointer;user-select:none;white-space:nowrap}
thead th.sortable:hover{color:var(--th-fg)}
thead th .sort-arrow{display:inline-block;width:1em;margin-left:.3em;text-align:center;line-height:1;color:var(--th-accent);opacity:.7;font-size:.95em;font-weight:700;vertical-align:middle}
thead th .sort-arrow::after{content:"\21C5"}
thead th.sort-asc,
thead th.sort-desc{color:var(--th-fg)}
thead th.sort-asc .sort-arrow,
thead th.sort-desc .sort-arrow{opacity:1}
thead th.sort-asc  .sort-arrow::after{content:"\2191"}
thead th.sort-desc .sort-arrow::after{content:"\2193"}
/* A SERVER-sorted header wraps its label in a link to the next sort URL. It must
   not read as a link on the header bar — the arrow is the affordance. */
thead th.sortable > a{color:inherit;text-decoration:none;display:inline-flex;align-items:center;gap:.25em}
thead th.sortable > a:hover{color:var(--th-fg)}

/* ======================= COLUMN RESIZE (Excel-style) =======================
   /js/col-resize.js (app-wide, from partials/icons) puts a .col-resizer grip on
   the right edge of every header cell of every table with a <thead>. Drag it to
   size the column; double-click it to fit the longest value. The grip is
   absolutely positioned, so it takes no space and never moves the header. The
   global `thead th` is sticky, which already anchors it; .cr-pos is added by
   the script only to a header a page made static.
   A table someone has sized carries .col-sized: fixed layout, one <col> per
   column, width = the sum of the columns (all inline, set by the script). A value
   wider than its column ends in "…" — a full-width row (colspan: an empty state,
   a drill-down) is left alone. On a phone mobile.css drops all of it for a
   carded table. */
.col-resizer{position:absolute;top:0;right:0;bottom:0;width:8px;cursor:col-resize;touch-action:none;user-select:none;-webkit-user-select:none;z-index:2}
.col-resizer::after{content:"";position:absolute;top:25%;bottom:25%;right:3px;width:2px;border-radius:1px;background:transparent;transition:background .12s,top .12s,bottom .12s}
thead:hover .col-resizer::after{background:color-mix(in srgb,var(--th-fg) 30%,transparent)}
.col-resizer:hover::after,.col-resizer.active::after{top:0;bottom:0;background:var(--th-accent)}
th.cr-pos{position:relative}
/* While a column is being dragged the cursor stays the resize arrow over
   everything it crosses (rows and sortable headers set their own cursor), and
   nothing gets text-selected. The one place !important is right: it must beat
   every per-element cursor for the length of the drag only. */
body.col-resizing,body.col-resizing *{cursor:col-resize !important;user-select:none !important;-webkit-user-select:none !important}
table.col-sized>thead>tr>th,
table.col-sized>tbody>tr>td:not([colspan]),
table.col-sized>tfoot>tr>td:not([colspan]){overflow:hidden;text-overflow:ellipsis;min-width:0;max-width:none}
/* The "…" follows the column whatever the cell holds. The cell's own ellipsis
   only reaches text sitting directly in it: a value in a box of its own was
   sliced mid-letter — a <div>, the text beside a thumbnail in a .cellflex row —
   and a box with a width of its own (.cell-ellip's 220px cap) put its "…" out of
   sight under the next column. So every box in a sized cell shrinks to the
   column and ends its own text in "…". `overflow-x:clip`, not hidden: it clips
   without turning an inline-block's baseline into its bottom edge, so no label
   jumps up a few pixels. Pictures and controls keep their own size. */
table.col-sized>thead>tr>th :not(img,svg,video,canvas,input,select,textarea,button,.col-resizer),
table.col-sized>tbody>tr>td:not([colspan]) :not(img,svg,video,canvas,input,select,textarea,button),
table.col-sized>tfoot>tr>td:not([colspan]) :not(img,svg,video,canvas,input,select,textarea,button){min-width:0;max-width:100%;overflow-x:clip;text-overflow:ellipsis}
/* A pill lays its label out as a flex item the "…" can't reach, so in a sized
   cell it becomes an inline-block of the same size, dot and all. */
table.col-sized>tbody>tr>td:not([colspan]) .pill{display:inline-block;line-height:22px;overflow:hidden}
table.col-sized>tbody>tr>td:not([colspan]) .pill.xs{line-height:18px}
table.col-sized>tbody>tr>td:not([colspan]) .pill>:is(.pd,svg,i[data-lucide]){display:inline-block;vertical-align:middle;margin:-2px 5px 0 0}
table.col-sized>tbody>tr>td:not([colspan]) .pill.xs>:is(.pd,svg,i[data-lucide]){margin-right:4px}
/* Same for a server-sorted header's link (inline-flex): its label joins the
   header's own line, which ends in "…". */
table.col-sized>thead>tr>th>a{display:inline}
@media print{.col-resizer{display:none}}

/* --- pagination (.pager) ---
   THE pager — the one under Inventory → Products, on every paged list in the app
   (standing rule, CLAUDE.md → "Every list pages with ONE pager"; 2026-09-27):
     <div class="pager"><span class="pager-info">Page 2 · 51–100 of 187</span>
       <a|button class="pager-link">‹ Prev</…><a|button class="pager-link">Next ›</…></div>
   The cluster sits at the RIGHT of the table foot, the count reads as muted text
   directly beside the buttons, and Prev/Next are compact ghost buttons with their
   word. No page numbers, no First/Last, no rows-per-page picker; at an end a
   button greys out, it never disappears. JS builds it with App.pagerHtml
   (/js/format.js); src/ui/pager-contract.test.js holds every page to it. */
.pager{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap;margin-top:12px;font-size:13px;color:var(--text-2)}
/* display:flex would otherwise beat the UA's [hidden]{display:none}, so a pager
   hidden before its rows arrive still took its margin. The same trap as .empty. */
.pager[hidden]{display:none}
.pager .pager-info{color:var(--text-muted);font-size:.8rem}
.pager .pager-link{display:inline-flex;align-items:center;justify-content:center;gap:.4rem;padding:.28rem .6rem;border-radius:8px;border:1px solid var(--border);background:var(--surface);color:var(--text-subtle);font-size:.78rem;font-weight:500;line-height:1.2;cursor:pointer;text-decoration:none;transition:background .12s ease,border-color .12s ease,box-shadow .12s ease}
.pager .pager-link svg,.pager .pager-link i[data-lucide]{width:12px;height:12px}
.pager .pager-link:focus-visible{outline:none;box-shadow:0 0 0 3px var(--accent-focus-ring)}
/* The NEUTRAL hover the Products buttons actually show (inventory.css's late
   `.inv-btn-sm:hover` wins over the accent tint above it) — the accent version
   made every other pager light up blue/amber where Products turns grey. */
.pager .pager-link:hover{background:var(--surface-3);border-color:var(--border-subtle);color:var(--text-strong)}
/* A stepped-off Prev/Next must LOOK off, not just faded: opacity:.55 over a
   transparent fill dimmed the border and the label together, so the button read
   as a live one seen through a haze. This is the .inv-btn:disabled treatment the
   products pager always had — a --surface-2 fill, a solid border, --text-faint
   text and a dimmed arrow. pointer-events stay off because the server-rendered
   pagers step with <a>, which cannot carry a real `disabled`. */
.pager .pager-link.disabled,.pager .pager-link:disabled{background:var(--surface-2);border-color:var(--border);color:var(--text-faint);cursor:not-allowed;pointer-events:none;opacity:1}
.pager .pager-link.disabled svg,.pager .pager-link:disabled svg{opacity:.55}
/* An unpadded card's pager lines up with its .card-head (20px sides) and keeps a
   foot inset, instead of hanging its buttons off the card's bottom-right corner.
   Same idea as `.card:not(.pad) > .toolbar` above; a .card.pad (or a .pad wrapper
   inside the card, as on Reports -> Stage resets) already insets it. My Work ->
   Fraud is the one pager that sits directly in a bare .card. A script's plain
   mount div (no class — e.g. the purchase History's [data-purchase-history]) is
   no inset either, so the pager inside it is padded the same; it sat flush on
   the card's border until 2026-10-01 (src/ui/card-pager-inset.browser.test.js). */
.card:not(.pad) > .pager,
.card:not(.pad) > div:not([class]) > .pager{padding-inline:20px;padding-bottom:16px}

/* --- shared modal system (.x-modal*) — backed by /js/x-modal.js + window.App.modal ---
   This is the structural modal for CUSTOM content (forms, multi-step, lightboxes).
   For plain yes/no + alerts use window.App.confirm/alert (partials/ui-modal.ejs) instead.
   Markup contract:
     <div class="x-modal-backdrop" id="X" data-x-modal hidden>
       <div class="x-modal" role="dialog" aria-modal="true" aria-labelledby="X-title">
         <header class="x-modal__head"><h2 class="x-modal__title" id="X-title">…</h2>
           <button class="x-modal__close" data-x-close aria-label="Close"><i data-lucide="x"></i></button></header>
         <div class="x-modal__body">…page markup…</div>
         <footer class="x-modal__foot">…buttons (.btn vocab)…</footer></div></div>
   Full-bleed (lightbox / camera): put content DIRECTLY in .x-modal-backdrop.is-full (no .x-modal box),
   so a click on the dark surround = a click on the backdrop and dismisses. */
.x-modal-backdrop{position:fixed;inset:0;z-index:9000;display:none;align-items:center;justify-content:center;padding:1rem;background:var(--overlay);backdrop-filter:blur(2px)}
.x-modal-backdrop.open{display:flex}
.x-modal{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);width:min(560px,92vw);max-height:calc(var(--vh) * .9);display:flex;flex-direction:column;overflow:hidden;animation:xModalPop .14s ease-out}
@keyframes xModalPop{from{opacity:0;transform:translateY(8px) scale(.985)}to{opacity:1;transform:none}}
.x-modal.is-sm{width:min(380px,92vw)}
.x-modal.is-wide{width:min(720px,94vw)}
.x-modal.is-xl{width:min(980px,95vw)}
.x-modal.is-xxl{width:min(1100px,96vw)}
/* 75% of the window for a wide table (Market Price Details), never narrower than
   .is-xl and never past 95vw — on a phone that is the whole width, like .is-xl. */
.x-modal.is-75vw{width:min(max(75vw,980px),95vw)}
.x-modal.is-tall{height:calc(var(--vh) * .9)}
.x-modal__head{display:flex;align-items:center;gap:12px;flex:0 0 auto;padding:14px 18px;border-bottom:1px solid var(--border-light);background:var(--surface-2)}
.x-modal__title{margin:0;font-family:var(--font-display);font-weight:600;letter-spacing:-.02em;color:var(--text-strong);font-size:16px}
.x-modal__close{margin-left:auto;border:0;background:none;color:var(--text-muted);border-radius:8px;padding:5px;cursor:pointer;display:inline-flex;line-height:0}
.x-modal__close svg,.x-modal__close i[data-lucide]{width:18px;height:18px}
.x-modal__close:hover{background:var(--surface-3);color:var(--text)}
.x-modal__body{flex:1 1 auto;overflow:auto;padding:18px;color:var(--text)}
/* A <form> wrapping body + footer (a create/edit dialog) passes the column through, so
   the body stays the scroller and the footer's Save stays on screen — as a plain block
   it grew past the modal's max-height and overflow:hidden cut Save off
   (src/ui/x-modal-form.browser.test.js). */
.x-modal > form{display:flex;flex-direction:column;flex:1 1 auto;min-height:0}
.x-modal__body.pad-0{padding:0}
.x-modal__foot{display:flex;flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;flex:0 0 auto;padding:14px 18px;border-top:1px solid var(--border-light);background:var(--surface-2)}
/* A read-only modal drops its footer (the tray Notes feed once the tray is submitted);
   the display above would otherwise beat the UA [hidden]. */
.x-modal__foot[hidden]{display:none}
/* Legacy `.modal` backdrops may wrap a canonical `.x-modal` box to get the head/body/foot
   chrome while keeping their existing show/hide + close JS. The legacy `.close-button` span
   sits as a flex child in the head bar (override its absolute legacy positioning). */
.x-modal .x-modal__head .close-button{position:static;margin-left:auto;top:auto;right:auto}
/* `.close-button` carrying a Lucide <svg> (not the legacy &times; char) centers cleanly via
   the flex box — size it like the canonical .x-modal__close. (The full-bleed `.lightbox`
   variant keeps its larger × character and is unaffected.) */
.close-button:not(.lightbox):not(.close-button--icon) svg,.close-button:not(.lightbox):not(.close-button--icon) i[data-lucide]{width:18px;height:18px}
/* full-bleed: black lightbox/camera scrim; content sits directly in the backdrop */
.x-modal-backdrop.is-full{padding:0;flex-direction:column;gap:14px;background:rgba(0,0,0,.9)}
.x-modal-backdrop.is-full img,.x-modal-backdrop.is-full video{max-width:94vw;max-height:88vh;object-fit:contain;border-radius:6px}
/* A PDF in the lightbox (a receipt): an <iframe> has no intrinsic size, so it gets a fixed
   box. Turned a quarter-turn its axes swap — so `.is-rot-side` swaps the box too. */
.lightbox-pdf{width:95vw;height:92vh;border:0;border-radius:8px;background:var(--surface)}
.lightbox-pdf.is-rot-side{width:88vh;height:92vw}
.lightbox-pdf[hidden]{display:none}
/* The canonical `.close-button` closing a full-bleed lightbox. The scrim is always black —
   in BOTH themes — so the theme-aware `--text-muted`/`--surface-3` it normally uses would
   read as a dim grey on black; the colours are pinned to the on-dark scrim instead. It is a
   ROUND button here, the same shape and tone as the `.x-modal__nav` arrows it shares the
   scrim with (a square hover chip on top of a photo reads as a stray rectangle) — smaller
   than those, because closing is not the action the eye should land on first. */
.x-modal-backdrop.is-full > .close-button{z-index:1;top:14px;right:14px;width:34px;height:34px;
  padding:0;border:0;border-radius:50%;background:rgba(255,255,255,.14);color:#fff;
  display:grid;place-items:center}
.x-modal-backdrop.is-full > .close-button:hover,
.x-modal-backdrop.is-full > .close-button:focus{background:rgba(255,255,255,.26);color:#fff}

/* `.close-button--x` — the close for a page that loads NO icon bundle (the customer-facing
   ones: consignor portal, statement). A text `&times;` cannot be centred in a round button:
   its ink sits on the BASELINE, so the font's metrics decide where it lands — which is why it
   read as sitting off-centre inside its chip. So draw it: two crossing bars, same 2px stroke
   and proportions as the Lucide X the icon-bundle pages use. Deliberately NOT
   `:has(svg)`-based — a browser without `:has` drops the whole rule and would leave the
   button styled but the X uncentred. The `&times;` stays in the markup as fallback content.
   A button that carries a real Lucide <svg> just doesn't take this class. */
.close-button.close-button--x{font-size:0;line-height:0}
/* Positioned against the button box itself (`.close-button` is already `position:absolute`,
   so it IS the containing block) — NOT centred by grid/flex: the leftover text node counts as
   a box there and pushes the bars off-centre. top/left 50% minus half the bar is exact. */
.close-button--x::before,.close-button--x::after{content:"";position:absolute;top:50%;left:50%;
  width:13px;height:2px;margin:-1px 0 0 -6.5px;border-radius:1px;background:currentColor}
.close-button--x::before{transform:rotate(45deg)}
.close-button--x::after{transform:rotate(-45deg)}
/* Prev/next arrows for a full-bleed lightbox. Colours are pinned to the always-black
   scrim (not theme tokens), same as the `.is-full` close button above. Wired by
   App.bindLightboxKeys (/js/lightbox.js) so the arrows and the ← / → keys
   drive the same step. `hidden` collapses them when there is only one image. */
.x-modal__nav{position:absolute;top:50%;transform:translateY(-50%);z-index:1;width:44px;height:44px;border-radius:50%;border:0;background:rgba(255,255,255,.14);color:#fff;display:grid;place-items:center;cursor:pointer}
.x-modal__nav svg,.x-modal__nav i[data-lucide]{width:22px;height:22px}
.x-modal__nav:hover{background:rgba(255,255,255,.26)}
.x-modal__nav.prev{left:18px}
.x-modal__nav.next{right:18px}
.x-modal__nav[hidden]{display:none}
/* Blur-up loading, applied by App.lightboxLoader (/js/lightbox.js) to whatever <img>
   a viewer paints into — the class is added by the JS, so no page needs new markup.
   The already-cached ~400px thumb shows first and blurs while the full-res original
   decodes behind it, so a viewer is never BLANK on open or on ← / →. Lives here, not
   in a page sheet: every image viewer in the app must load the same way. */
.lightbox-img{transition:filter .18s ease-out,transform .18s ease-out}
.lightbox-img.is-loading-full{filter:blur(8px);transform:scale(1.02)}
/* Quarter-turn rotation, applied by App.setLightboxRotation (/js/lightbox.js). It uses the
   individual `rotate` property, NOT `transform`, so it composes with the blur-up scale above
   instead of replacing it. A quarter turn swaps the box's axes, so `.is-rot-side` swaps the
   size caps — in viewport units, because a % cap measures the parent's WIDTH, which is not
   what a sideways photo's width turns into. `!important` because every viewer sets its own
   caps (some inline) and this must win only while a photo is turned sideways. An element with
   a FIXED width/height (the receipt PDF <iframe>) swaps those itself. */
.lightbox-rot{rotate:var(--lightbox-rot,0deg)}
.lightbox-rot.is-rot-side{max-width:88vh!important;max-height:92vw!important}
/* Tool buttons on a full-screen image viewer (Rotate, Download), wired by
   App.lightboxRotator. Colours are pinned to the always-dark scrim, like `.x-modal__nav`.
   The group sits top-right; add `.beside-close` when the viewer has its own close button in
   that corner. */
.lightbox-tools{position:absolute;top:14px;right:14px;z-index:3;display:flex;gap:8px}
.lightbox-tools.beside-close{right:72px}
.lightbox-tool{display:inline-flex;align-items:center;gap:6px;height:34px;padding:0 14px;border:0;border-radius:999px;
  background:rgba(255,255,255,.14);color:#fff;font:600 13px/1 var(--font-body);cursor:pointer;text-decoration:none;white-space:nowrap}
.lightbox-tool:hover,.lightbox-tool:focus-visible{background:rgba(255,255,255,.26);color:#fff}
.lightbox-tool svg,.lightbox-tool i[data-lucide]{width:16px;height:16px}
.lightbox-tool[hidden]{display:none}
/* The legacy full-screen lightbox close (`.close-button.lightbox` on the tray / review / fixing
   pages) as the same round on-scrim button as `.x-modal-backdrop.is-full > .close-button`, at the
   same top and height as `.lightbox-tools` — so the × and Rotate sit on one line (the 40px text ×
   sat lower than the Rotate button). The X is CSS-drawn (`.close-button--x`), so a click still
   lands on the span the pages' close handlers test for. */
.close-button.lightbox.close-button--x{top:14px;right:14px;width:34px;height:34px;padding:0;border-radius:50%;
  background:rgba(255,255,255,.14);color:#fff}
.close-button.lightbox.close-button--x:hover{background:rgba(255,255,255,.26);color:#fff}
/* A manage-images tile whose turn is still saving keeps the tile's own caps — the sideways caps
   above are for full-screen. */
.thumbnail img.lightbox-rot.is-rot-side{max-width:100%!important;max-height:100%!important}


/* =====================================================================
   Global App.confirm/alert modal (partials/ui-modal.ejs) — token-based, shipped via partials/icons
   Moved here from the page's inline <style> so edits serve live (no
   view-cache restart). Verbatim move — no visual change.
   ===================================================================== */
  .app-modal-backdrop {
    position: fixed; inset: 0;
    background: var(--overlay, rgba(16, 24, 40, 0.55));
    display: none;
    align-items: center; justify-content: center;
    z-index: 9500;
    backdrop-filter: blur(2px);
    padding: 1rem;
  }
  .app-modal-backdrop.open { display: flex; }
  .app-modal {
    background: var(--surface, #fff);
    border-radius: 14px;
    width: min(460px, 92vw);
    max-height: calc(var(--vh) * .9);
    display: flex; flex-direction: column;
    box-shadow: 0 20px 60px rgba(16, 42, 94, 0.35);
    border: 1.25px solid var(--border, #e2e8f0);
    overflow: hidden;
    animation: appModalPop 0.14s ease-out;
  }
  @keyframes appModalPop {
    from { opacity: 0; transform: translateY(8px) scale(0.98); }
    to   { opacity: 1; transform: none; }
  }
  .app-modal > header {
    display: flex; align-items: center; gap: 0.55rem;
    padding: 1rem 1.15rem;
    border-bottom: 1px solid var(--border-light, var(--border, #edf0f4));
    background: var(--surface-2, var(--surface, #f8fafc));
  }
  .app-modal > header .app-modal-icon {
    display: inline-flex; align-items: center; justify-content: center;
    width: 32px; height: 32px; border-radius: 9px; flex: 0 0 auto;
    background: color-mix(in srgb, var(--info, #3b82f6) 14%, transparent);
    color: var(--info, #3b82f6);
  }
  .app-modal.v-warn  > header .app-modal-icon { background: color-mix(in srgb, var(--warning, #d97706) 16%, transparent); color: var(--warning, #d97706); }
  .app-modal.v-danger> header .app-modal-icon { background: color-mix(in srgb, var(--danger, #dc2626) 16%, transparent); color: var(--danger, #dc2626); }
  .app-modal.v-success>header .app-modal-icon { background: color-mix(in srgb, var(--success, #16a34a) 16%, transparent); color: var(--success, #16a34a); }
  .app-modal > header .app-modal-icon i { width: 18px; height: 18px; }
  .app-modal > header h2 {
    margin: 0; padding: 0; border: none;
    font-size: 16px; font-weight: 600; line-height: 1.2;
    color: var(--text-strong, var(--text, #0f172a));
    font-family: var(--font-display, inherit); letter-spacing: -0.02em;
  }
  .app-modal > header .app-modal-close {
    margin-left: auto;
    border: 0; background: none; cursor: pointer;
    color: var(--text-muted, var(--muted, #64748b));
    display: inline-flex; align-items: center;
    padding: 4px; border-radius: 6px;
  }
  .app-modal > header .app-modal-close:hover { background: var(--surface-3, #eef2f7); color: var(--text, #0f172a); }
  .app-modal > header .app-modal-close i { width: 18px; height: 18px; }
  .app-modal .app-modal-body {
    padding: 1.1rem 1.15rem;
    overflow-y: auto;
    color: var(--text, #334155);
    font-size: 0.92rem; line-height: 1.55;
    white-space: pre-wrap; word-break: break-word;
  }
  .app-modal footer {
    padding: 0.9rem 1.15rem;
    border-top: 1px solid var(--border-light, var(--border, #edf0f4));
    background: var(--surface-2, var(--surface, #f8fafc));
    display: flex; align-items: center; justify-content: flex-end;
    gap: 0.5rem;
  }
  .app-modal-btn {
    height: 38px; padding: 0 0.95rem;
    border-radius: 9px; border: 1px solid transparent;
    font-size: 0.88rem; font-weight: 600; cursor: pointer;
    display: inline-flex; align-items: center; gap: 0.4rem; line-height: 1;
    font-family: inherit;
  }
  .app-modal-btn.ghost {
    background: var(--surface, #fff);
    color: var(--text-strong, var(--text, #0f172a));
    border-color: var(--border-subtle, var(--border, #cbd5e1));
  }
  .app-modal-btn.ghost:hover { background: var(--surface-3, #eef2f7); }
  .app-modal-btn.primary {
    background: linear-gradient(135deg, var(--accent, #0350a6), var(--accent-end, var(--accent-strong, var(--accent, #0350a6))));
    border-color: var(--accent, #0063a3);
    color: var(--on-brand, #fff);
    box-shadow: 0 2px 6px color-mix(in srgb, var(--accent, #0063a3) 26%, transparent);
  }
  .app-modal-btn.primary:hover { filter: brightness(1.06); }
  .app-modal-btn.danger {
    background: var(--danger, #dc2626);
    border-color: var(--danger, #dc2626);
    color: var(--on-brand, #fff);
  }
  .app-modal-btn.danger:hover { filter: brightness(1.05); }
  .app-modal-btn:focus-visible { outline: 2px solid var(--accent, #0063a3); outline-offset: 2px; }


/* =====================================================================
   Topbar chrome (partials/x-topbar.ejs) — channel-auth banner + first-paint bell-count hide
   Moved here from the page's inline <style> so edits serve live (no
   view-cache restart). Verbatim move — no visual change.
   ===================================================================== */
  /* Critical: keep the CoPilot/notification count badges hidden at first paint.
     The full .header-bell-count styles (incl. this .hidden rule) live in the
     lazily-loaded copilot-widget.css, so without this inline copy the default
     "0" flashes for one frame before that stylesheet applies. */
  .header-bell-count.hidden { display: none; }

  /* Admin channel-auth alerts: PERSISTENT toasts shown while a sales channel's
     credential is dead or an eBay token is near expiry. Rendered INSIDE the
     shared .toast-region (top-right) so they queue under the transient toasts
     instead of owning their own corner — the fixed values below are only the
     fallback for a page with no x-topbar. Deliberately an overlay, not a strip:
     the old under-the-topbar banner sat in normal flow BEHIND the fixed topbar
     and was invisible. No dismiss button: they clear themselves once the
     channels are healthy again — see channel-auth-health.js. Theme-aware via
     pill tokens (no hard-coded colors). */
  .chan-toasts { position: fixed; top: var(--toast-top); right: var(--toast-right); z-index: 210;
    display: flex; flex-direction: column; gap: var(--toast-gap);
    width: var(--toast-width); }
  .chan-alert { display: flex; align-items: center; gap: .55rem; padding: .65rem .9rem;
    font-size: .85rem; line-height: 1.35; border-radius: 12px;
    border: 1px solid transparent; box-shadow: var(--shadow-lg);
    /* Opaque base, like .toast. The variants below tint this with a pill token,
       and in DARK those tokens are translucent (--pill-red-bg is rgba(...,.16))
       because a pill sits ON a card. This alert FLOATS over the page, so the
       tint alone let the table underneath read straight through it. */
    background: var(--elev); }
  .chan-alert i { width: 16px; height: 16px; flex: 0 0 auto; }
  .chan-alert .ca-msg { flex: 1 1 auto; min-width: 0; }
  /* The only clickable part of the alert — the box itself stays click-through
     so a persistent banner can't eat clicks on the page under it (.toast-region). */
  .chan-alert .ca-act { flex: 0 0 auto; font-weight: 600; text-decoration: underline; color: inherit; white-space: nowrap; pointer-events: auto; }
  /* Tint = the pill token painted as a layer OVER the opaque --elev base, so a
     translucent dark-mode token composites instead of showing the page through.
     Light-mode tokens are opaque, so they render exactly as before. */
  .chan-alert.ca-error { background: linear-gradient(var(--pill-red-bg), var(--pill-red-bg)), var(--elev); color: var(--pill-red-text); border-color: var(--pill-red-border); }
  .chan-alert.ca-warn  { background: linear-gradient(var(--pill-amber-bg), var(--pill-amber-bg)), var(--elev); color: var(--pill-amber-text); border-color: var(--pill-amber-border); }
  .chan-alert b { font-weight: 600; }


/* =====================================================================
   CoPilot widget first-paint initial state (partials/copilot-widget.ejs) — keep before the lazy copilot-widget.css
   Moved here from the page's inline <style> so edits serve live (no
   view-cache restart). Verbatim move — no visual change.
   ===================================================================== */
    .sw-panel { position: fixed; top: 64px; right: 0; height: calc(var(--vh) - 64px); transform: translateX(100%); }
    .sw-modal { display: none; }
    .sw-backdrop { position: fixed; inset: 0; opacity: 0; pointer-events: none; }

/* =====================================================================
   Notification banners — .flash (admin) + .message (reports) converged to ONE
   token-based style (was redefined ~13 different ways across pages). Inline
   status strip; .error = red, .success = green (pill tokens, theme-aware).
   .flash defaults to success-green (admin convention); .message stays neutral
   until given .success/.error (reports convention); .show toggles visibility.
   :not(.spot-card) keeps the base off the scanner's .spot-card.flash animation.
   ===================================================================== */
.flash:not(.spot-card), .message {
  padding: 0.7rem 1rem; border-radius: 10px; margin-bottom: 1rem;
  font-weight: 600; font-size: 0.9rem; border: 1px solid transparent;
}
.flash:not(.spot-card) { background: var(--pill-green-bg); border-color: var(--pill-green-border); color: var(--pill-green-text); }
.flash.success, .message.success { background: var(--pill-green-bg); border-color: var(--pill-green-border); color: var(--pill-green-text); }
.flash.error, .message.error { background: var(--pill-red-bg); border-color: var(--pill-red-border); color: var(--pill-red-text); }
.flash.warning, .message.warning { background: var(--pill-amber-bg); border-color: var(--pill-amber-border); color: var(--pill-amber-text); }
/* A link inside a banner ("switch it on under Admin → …") must read as one: it
   inherits the banner's text colour, so the underline is what marks it. */
.flash:not(.spot-card) a, .message a { color: inherit; text-decoration: underline; }
.flash.show { display: block; }

/* Inline, non-blocking amber advisory (a soft nudge, not an error) — e.g. a
   settings field whose value pairs poorly with another. Hidden until `.show`.
   Shared here (theme-aware, contrast-checked) so pages don't re-declare it in a
   per-page <style>. */
.cfg-warn { display: none; align-items: flex-start; gap: 0.4rem; margin: 0.1rem 0 0.45rem 0;
  font-size: 0.82rem; color: var(--pill-amber-text); background: var(--pill-amber-bg);
  border-radius: 7px; padding: 7px 10px; max-width: 520px; }
.cfg-warn.show { display: flex; }
.cfg-warn .lucide { width: 15px; height: 15px; flex-shrink: 0; margin-top: 1px; }

/* ── app_config field chrome (partials/config-group.ejs) ─────────────────────
   A setting group is edited on the page that OWNS the feature, not only on the
   page that owns it, so this chrome is shared here rather than in
   a per-page <style>. Pair with /js/config-fields.js for the autosave. */
#cfg-form { display: block; } /* beats the global `form{display:flex}` in style.css */
.cfg-field { padding: 0.85rem 0; border-bottom: 1px dashed var(--border-light); }
.cfg-field:last-child { border-bottom: none; }
.cfg-field > label:not(.cfg-bool) { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.45rem; font-weight: 600; color: var(--text-strong); font-size: 0.95rem; margin-bottom: 0.35rem; }
.cfg-field .cfg-help { color: var(--text-subtle); font-size: 0.83rem; margin: 0.1rem 0 0.45rem 0; }
.cfg-field input[type="text"], .cfg-field input[type="password"], .cfg-field input[type="number"] {
  border: 1px solid var(--border-subtle); border-radius: 7px; padding: 8px 10px; font-size: 0.92rem;
  background: var(--surface); color: var(--text); min-width: 280px; max-width: 100%;
}
/* A NUMBER setting holds a handful of digits — a count of days, a cap, a price —
   so the 280px above (sized for a URL or an API key) is a width it can never use,
   and the admin-page control reset further down stretches it to the full card:
   either way the box reads as an empty text field with one digit alone at the far
   left. Size it to its content instead — four digits plus the browser's spinner.
   The long first selector is deliberate: a config group IS a .styled-card on an
   admin page, so this has to out-specify that reset's `width:100%` — matching its
   specificity is not enough, since the reset comes LATER in this file. */
body.admin-page .styled-card.cfg-group .cfg-field input[type="number"],
.cfg-field input[type="number"] { min-width: 0; width: 120px; }
.cfg-field select.input.sm { min-width: 280px; }
.cfg-field input.f-secret { font-family: var(--font-mono, monospace); }
.cfg-field .cfg-masked { font-family: var(--font-mono, monospace); font-size: 0.8rem; color: var(--pill-blue-text); background: var(--pill-blue-bg); border-radius: 6px; padding: 2px 8px; }
.cfg-field .cfg-unset { font-size: 0.8rem; color: var(--pill-amber-text); background: var(--pill-amber-bg); border-radius: 6px; padding: 2px 8px; }
.cfg-bool { display: inline-flex; align-items: center; gap: 0.6rem; cursor: pointer; }
/* Sliding toggle switch (matches Job Schedules), built on a real checkbox so the
   save handler keeps reading el.checked. */
.cfg-switch { position: relative; display: inline-block; width: 44px; height: 24px; flex-shrink: 0; }
.cfg-switch input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; z-index: 1; }
.cfg-switch .slider { position: absolute; inset: 0; background: var(--border); border-radius: 12px; transition: background 0.2s; pointer-events: none; }
.cfg-switch .slider::after { content: ""; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background: var(--surface); border-radius: 50%; transition: left 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.2); }
.cfg-switch input:checked + .slider { background: var(--green, #48bb78); }
.cfg-switch input:checked + .slider::after { left: 22px; }
.cfg-switch input:focus-visible + .slider { box-shadow: 0 0 0 3px var(--accent-soft); }
/* Per-field autosave status pill. */
.cfg-status { font-size: 0.78rem; font-weight: 600; transition: opacity 0.2s; }
.cfg-status[data-state="saving"] { color: var(--text-muted); }
.cfg-status[data-state="saved"]  { color: var(--success); }
.cfg-status[data-state="error"]  { color: var(--danger); }
.cfg-rowstatus { margin-left: 8px; }
/* Filter: hide non-matching fields and any group/section left empty. */
.cfg-field[hidden] { display: none; }
/* A setting locked by its PARENT switch (dependsOn in src/config/app-config.js):
   drawn off and dimmed, never hidden — you can still read what you are turning
   back on, and the note says why it is off. Toggled live by /js/config-fields.js. */
.cfg-field.is-locked > label:not(.cfg-bool), .cfg-field.is-locked .cfg-help { opacity: 0.6; }
.cfg-field.is-locked .cfg-bool { opacity: 0.6; cursor: not-allowed; }
.cfg-locked-note { display: flex; align-items: center; gap: 0.4rem; margin: 0.5rem 0 0; padding: 0.35rem 0.6rem; border-radius: 8px;
  font-size: 0.82rem; color: var(--pill-amber-text); background: var(--pill-amber-bg); border: 1px solid var(--pill-amber-border); }
.cfg-locked-note[hidden] { display: none; }
.cfg-locked-note svg, .cfg-locked-note i[data-lucide] { width: 14px; height: 14px; flex-shrink: 0; }
.cfg-group.is-empty, .cfg-section.is-empty { display: none; }
/* A switch with `inline` companions (its amount, its channels — Admin → Fraud
   Checks → Order verification): ONE row per rule, wrapping on a narrow screen.
   The companions dim while the switch is off but stay editable, so the amount
   and channels are ready when it goes back on. The checkbox dropdown is the
   shared one (app.css "CHECKBOX DROPDOWN"); its toggle is matched here to the
   number box beside it. */
.cfg-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 1.5rem; }
.cfg-inline-ctl { display: inline-flex; align-items: center; gap: 0.5rem; }
.cfg-inline-cap { font-size: 0.83rem; color: var(--text-subtle); font-weight: 500; white-space: nowrap; }
/* Dim the caption, the box and the toggle — never the wrapper: opacity makes a
   stacking context, which would trap the open panel UNDER the next row. */
.cfg-row:has(.cfg-switch input:not(:checked)) :is(.cfg-inline-cap, .cfg-inline-ctl > input, .inv-dropdown-toggle) { opacity: 0.6; }
.cfg-inline-ctl .inv-dropdown-toggle { height: 38px; }
.cfg-channels .inv-dropdown-panel { min-width: 260px; }
/* Try-On size-field mapping table: uniform full-width dropdowns. */
.cfg-field select.cfg-tryon-cat, .cfg-field select.cfg-tryon-place { min-width: 0; width: 100%; }
.cfg-field .tablewrap td, .cfg-field .tablewrap th { vertical-align: middle; }
.cfg-field .tablewrap td:first-child { width: 26%; }
.cfg-field .tablewrap td:last-child { width: 26%; }
/* A group whose ONE editor is another page (GROUP_OWNER): listed here so the
   settings index stays complete + searchable, but linked rather than duplicated. */
.cfg-owned-list { list-style: none; margin: 0 0 0.9rem; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; }
.cfg-owned-list li { font-size: 0.88rem; color: var(--text-2); display: flex; gap: 0.45rem; align-items: baseline; flex-wrap: wrap; }
.cfg-owned-list li[hidden] { display: none; }

/* AI DEPENDENCY NOTE (partials/ai-switch-note.ejs).
   Some admin pages configure something that does nothing unless an AI switch on
   another page is on ("Enable for Autofill" on a field, "Generate this title
   with AI" on a template). The OFF case uses the canonical .message banner; this
   is only the quiet one-line ON confirmation, so the dependency is visible
   before it surprises someone rather than only after.
   The `i[data-lucide]` twin is required: the <i> is 0x0 until the deferred
   Lucide bundle swaps it for an <svg>, and :where(i[data-lucide]) reserves 24px
   — without the twin this line jumps sideways on load. */
.ai-note { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
           margin: -4px 0 14px; font-size: 0.85rem; color: var(--text-muted); }
.ai-note svg, .ai-note i[data-lucide] { width: 15px; height: 15px; flex-shrink: 0; }
.ai-note strong { color: var(--text-2); font-weight: 600; }


/* =====================================================================
   KPI-strip column-count modifiers — base .kpis is 4-col (above); these add
   5/6/7-col and auto-flow variants so pages express their KPI count with a
   canonical class instead of redefining .kpis inline (was done ~8 ways). All
   collapse to 2 then 1 column on narrow screens, like the base .kpis.
   ===================================================================== */
.kpis-3 { grid-template-columns: repeat(3, 1fr); }
.kpis-5 { grid-template-columns: repeat(5, 1fr); }
.kpis-6 { grid-template-columns: repeat(6, 1fr); }
.kpis-7 { grid-template-columns: repeat(7, 1fr); }
.kpis-auto { grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: 1fr; }
@media(max-width:1180px){ .kpis-3,.kpis-5,.kpis-6,.kpis-7 { grid-template-columns: repeat(2,1fr); } }
@media(max-width:680px){ .kpis-3,.kpis-5,.kpis-6,.kpis-7 { grid-template-columns: repeat(2,minmax(0,1fr)); } .kpis-auto { grid-auto-flow: row; grid-template-columns: repeat(2,minmax(0,1fr)); } }

/* AI Fill: highlight inputs the AI just pre-filled so the operator reviews them
   before saving. Token-driven (accent), so it reads in both themes. The class is
   removed by the normal field styling once the operator edits/saves. */
.ai-filled,
input.ai-filled,
select.ai-filled,
textarea.ai-filled {
  background: var(--accent-soft) !important;
  box-shadow: inset 3px 0 0 0 var(--accent);
}

/* =====================================================================
   CATEGORY CHANGE (product page) — picking another category previews what
   changes (.move-plan). Custom attributes then show the suggested title
   (.ca-title), every value saved before with the field it goes in (.ca-source),
   and the new category's fields empty, with a box under a field a saved value
   points at (.ca-suggest). Token-driven, both themes.
   ===================================================================== */
.ca-move-banner{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px}
.ca-move-banner .ca-move-text{flex:1 1 320px;min-width:0;font-weight:400}
.ca-group-note{margin:0 0 8px;font-size:12.5px}
.ca-suggest{display:flex;flex-wrap:wrap;align-items:center;gap:4px 10px;margin-top:6px;padding:6px 8px 6px 10px;border:1px solid var(--accent-border);border-radius:8px;background:var(--accent-soft);color:var(--text);font-size:13px;line-height:1.35;min-width:0}
.ca-suggest-from{color:var(--text-muted);font-size:12px}
.ca-suggest-val{font-weight:600;overflow-wrap:anywhere;min-width:0}
.ca-suggest > .btn{margin-left:auto}
/* A value that can't go in (not one of the dropdown's options): neutral and
   dashed, so it never reads like the accent box that offers Use. */
.ca-suggest.no-fit{background:var(--surface-2);border-style:dashed;border-color:var(--border-strong)}
/* The title and "Saved for …" cards take the whole row of the 2-column
   #custom-attrs-fields grid — half width squeezed the table into broken words. */
.ca-title,.ca-source{grid-column:1/-1}
.ca-source-table td{vertical-align:middle}
.ca-source-table td.wrap-cell{word-break:normal;overflow-wrap:break-word;min-width:7rem}
.ca-source-table .input.sm{max-width:240px}
.ca-title-row{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 10px;padding:4px 0}
.ca-title-lbl{flex:0 0 190px;color:var(--text-muted);font-size:12.5px}
.ca-title-text{overflow-wrap:anywhere;min-width:0}
.move-plan{display:flex;flex-direction:column;gap:16px}
.move-plan h3{margin:0 0 6px;font-family:var(--font-display);font-size:14px;font-weight:600;letter-spacing:-.02em}
.move-plan p{margin:0 0 6px}
.move-plan-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}
.move-plan-list li{display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px}
@media(max-width:560px){ .ca-title-lbl{flex-basis:100%} }

/* Chooser tiles — a reusable "how do you want to do this?" grid for modals
   (e.g. the purchase-order Add-item chooser: blank vs from-inventory). Mirrors the
   tray Add-item tiles but lives here so any operator page can use it. */
.chooser-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 14px;
  margin-top: 4px;
}
.chooser-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 10px;
  padding: 26px 18px;
  background: var(--surface);
  border: 1.25px solid var(--border);
  border-radius: 12px;
  cursor: pointer;
  transition: border-color .15s, box-shadow .15s, transform .05s;
}
.chooser-tile:hover { border-color: var(--accent); box-shadow: var(--shadow-md); }
.chooser-tile:active { transform: translateY(1px); }
.chooser-tile-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--pill-blue-bg);
  color: var(--pill-blue-text);
}
.chooser-tile-icon svg,.chooser-tile-icon i[data-lucide] { width: 23px; height: 23px; }
.chooser-tile-title { font-family: var(--font-display); font-weight: 600; letter-spacing: -.01em; color: var(--text); }
.chooser-tile-desc { font-size: .82rem; line-height: 1.35; color: var(--text-muted); }


/* ===== Admin editor pages (moved from admin.ejs inline <style>, 2026-07-17) =====
   Shared chrome for the split-out admin editor pages (Categories, Tray Locations,
   Roles, Users, Goals) + the admin hub. Scoped under body.admin-page so it only
   affects admin views. Kept verbatim from the old mega-page so styling is identical. */
        /* ===== Shared styled card (matches the rest of the app) ===== */
        /* Subnav anchor jumps land the target under the fixed 60px topbar
           otherwise — offset them so the section header stays visible. */
        body.admin-page .styled-card[id] { scroll-margin-top: 76px; }
        /* Base .styled-card (bg, accent bar, card-header, header-icon) now comes
           from the shared app.css; the admin-only bits ([id] scroll offset above,
           card-subtitle + form-input styling below) stay. */
        body.admin-page .styled-card .card-subtitle {
            margin: -0.2rem 0 0.9rem 0;
            color: var(--text-muted);
            font-size: 0.9rem;
        }

        /* Manual jobs button row */
        body.admin-page #manual-jobs-actions {
            display: flex;
            justify-content: flex-start;
            gap: 0.6rem;
            flex-wrap: wrap;
            row-gap: 0.6rem;
        }
        body.admin-page #manual-jobs-actions .primary-btn {
            background: linear-gradient(135deg, var(--accent), var(--accent-end, var(--accent-strong, var(--accent)))) !important;
            color: var(--on-brand) !important;
            border: none !important;
            border-radius: 8px !important;
            padding: 0.5rem 0.95rem !important;
            font-size: 0.85rem !important;
            font-weight: 600 !important;
            cursor: pointer;
            box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 20%, transparent);
            transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
            display: inline-flex;
            align-items: center;
            gap: 6px;
        }
        body.admin-page #manual-jobs-actions .primary-btn:not(:disabled):hover {
            transform: translateY(-1px);
            box-shadow: 0 6px 16px color-mix(in srgb, var(--accent) 28%, transparent);
        }
        body.admin-page #manual-jobs-actions .primary-btn:disabled {
            opacity: 0.55;
            cursor: not-allowed;
        }
        body.admin-page .action-feedback {
            display: inline-block;
            margin-top: 0.75rem;
            font-size: 0.9rem;
            font-weight: 600;
        }

        /* Inputs / selects / textareas inside admin cards.
           :not(.input) excludes elements already using the canonical .input/.input.sm
           vocab (app.css) — this legacy block's non-zero vertical padding + larger
           font-size were winning on specificity over .input's padding/font-size while
           leaving .input's fixed height untouched, shrinking the available content
           box below the line height and clipping the bottom of select/input text. */
        body.admin-page .styled-card input[type="text"]:not(.input),
        body.admin-page .styled-card input[type="email"]:not(.input),
        body.admin-page .styled-card input[type="tel"]:not(.input),
        body.admin-page .styled-card input[type="password"]:not(.input),
        body.admin-page .styled-card input[type="number"]:not(.input),
        body.admin-page .styled-card input[type="file"]:not(.input),
        body.admin-page .styled-card select:not(.input),
        body.admin-page .styled-card textarea:not(.input) {
            width: 100%;
            box-sizing: border-box;
            padding: 0.5rem 0.7rem;
            border: 1px solid var(--border);
            border-radius: 8px;
            background: var(--surface);
            font-size: 0.9rem;
            color: var(--text-strong);
            font-family: inherit;
            transition: border-color 0.15s ease, box-shadow 0.15s ease;
        }
        body.admin-page .styled-card input:not(.input):focus,
        body.admin-page .styled-card select:not(.input):focus,
        body.admin-page .styled-card textarea:not(.input):focus {
            outline: none;
            border-color: var(--info);
            box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.18);
        }
        body.admin-page .styled-card .form-field > label {
            font-size: 0.78rem;
            font-weight: 600;
            color: var(--text-subtle);
            text-transform: uppercase;
            letter-spacing: 0.04em;
            margin-bottom: 4px;
            display: block;
        }

        /* Primary / secondary / tertiary buttons */
        body.admin-page .styled-card .btn.primary-btn {
            background: linear-gradient(135deg, var(--accent), var(--accent-end, var(--accent-strong, var(--accent)))) !important;
            color: var(--on-brand) !important;
            border: none !important;
            border-radius: 8px !important;
            padding: 0.5rem 1rem !important;
            font-weight: 600 !important;
            font-size: 0.9rem !important;
            cursor: pointer;
            box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 20%, transparent);
            transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
            display: inline-flex;
            align-items: center;
            gap: 6px;
            text-decoration: none;
        }
        body.admin-page .styled-card .btn.primary-btn:not(:disabled):hover {
            transform: translateY(-1px);
            box-shadow: 0 6px 16px color-mix(in srgb, var(--accent) 28%, transparent);
        }
        body.admin-page .styled-card .btn.secondary-btn {
            background: var(--surface-3) !important;
            color: var(--text-strong) !important;
            border: 1px solid var(--border) !important;
            border-radius: 8px !important;
            padding: 0.5rem 1rem !important;
            font-weight: 600 !important;
            font-size: 0.9rem !important;
            cursor: pointer;
            text-decoration: none;
            transition: transform 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease;
            display: inline-flex;
            align-items: center;
        }
        body.admin-page .styled-card .btn.secondary-btn:not(:disabled):hover {
            background: var(--border) !important;
            transform: translateY(-1px);
            box-shadow: 0 4px 10px rgba(16, 42, 94, 0.12);
        }
        /* Plain cancel buttons (.btn without primary/secondary — and not the
           canonical .danger/.success color variants, which keep their tint) */
        body.admin-page .styled-card .btn:not(.primary-btn):not(.secondary-btn):not(.danger):not(.success) {
            background: var(--surface) !important;
            color: var(--text-subtle) !important;
            border: 1px solid var(--border) !important;
            border-radius: 8px !important;
            padding: 0.4rem 0.9rem !important;
            font-weight: 600 !important;
            font-size: 0.85rem !important;
            cursor: pointer;
            transition: background-color 0.15s ease, transform 0.15s ease;
        }
        body.admin-page .styled-card .btn:not(.primary-btn):not(.secondary-btn):not(.danger):not(.success):hover {
            background: var(--surface-2) !important;
            transform: translateY(-1px);
        }
        /* …but an ICON-only button keeps .btn.icon's zero padding. The side padding
           above is !important, so it beat `.btn.icon{padding:0}` and squeezed a 32px
           button's 16px icon to about a pixel (src/ui/admin-card-icon-button.browser.test.js).
           Same :not() chain + .icon, so it out-ranks that rule. */
        body.admin-page .styled-card .btn.icon:not(.primary-btn):not(.secondary-btn):not(.danger):not(.success) {
            padding: 0 !important;
        }

        /* Delete buttons (category / role / user / goal / group / tray-location etc.)
           — ONE consistent, clearly-visible danger trash-ICON button: red-tinted
           fill + a solid red border + saturated red icon, flipping to solid red
           with a white icon on hover. Token-based, so it reads in both themes. */
        body.admin-page .styled-card .delete-category-btn,
        body.admin-page .styled-card .delete-role-btn,
        body.admin-page .styled-card .delete-user-btn,
        body.admin-page .styled-card .delete-goal-btn,
        body.admin-page .styled-card .delete-group-btn,
        body.admin-page .styled-card .delete-group-goal-btn,
        body.admin-page .styled-card .delete-tray-location-btn,
        body.admin-page .styled-card .btn.delete-group-btn:not(.primary-btn),
        body.admin-page .styled-card .btn.delete-goal-btn:not(.primary-btn),
        body.admin-page .styled-card .btn.delete-group-goal-btn:not(.primary-btn) {
            background: var(--pill-red-bg) !important;
            color: var(--danger) !important;
            border: 1px solid var(--danger) !important;
            border-radius: 8px !important;
            font-weight: 600 !important;
            cursor: pointer;
            transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
        }
        body.admin-page .styled-card .delete-category-btn:hover:not(:disabled),
        body.admin-page .styled-card .delete-role-btn:hover:not(:disabled),
        body.admin-page .styled-card .delete-user-btn:hover:not(:disabled),
        body.admin-page .styled-card .delete-goal-btn:hover:not(:disabled),
        body.admin-page .styled-card .delete-group-btn:hover:not(:disabled),
        body.admin-page .styled-card .delete-group-goal-btn:hover:not(:disabled),
        body.admin-page .styled-card .delete-tray-location-btn:hover:not(:disabled) {
            background: var(--danger-strong) !important;
            border-color: var(--danger-strong) !important;
            color: var(--on-brand) !important;
            transform: translateY(-1px);
            box-shadow: 0 4px 10px color-mix(in srgb, var(--danger) 35%, transparent);
        }
        body.admin-page .styled-card .delete-user-btn:disabled,
        body.admin-page .styled-card .delete-category-btn:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }
        /* Disabled icon buttons in admin table rows (e.g. the reserved/System
           category's edit pencil) must clearly read as inactive in BOTH themes —
           the base .btn:disabled wasn't visibly greyed in dark mode. */
        body.admin-page .admin-data-table .row-actions .btn:disabled {
            opacity: 0.4 !important;
            filter: grayscale(1);
            cursor: not-allowed;
            pointer-events: none;
        }
        body.admin-page .admin-data-table .row-actions .btn:disabled:hover {
            transform: none;
            box-shadow: none;
        }

        /* Compact, uniform action buttons across EVERY admin table (Goal Groups,
           Goals, Group Goals, Category, Tray Locations, Roles, Users). The global
           .btn is a fixed 38px tall — override it so icon + text row buttons all
           share one small, consistent size. */
        body.admin-page #groups-tbody button.btn,
        body.admin-page #goals-tbody button.btn,
        body.admin-page #group-goals-tbody button.btn,
        body.admin-page .admin-data-table .row-actions .btn,
        body.admin-page .admin-data-table .row-actions > button {
            height: 30px !important;
            min-height: 0 !important;
            padding: 0 9px !important;
            font-size: 12px !important;
            line-height: 1 !important;
            border-radius: 8px !important;
            gap: 5px !important;
            display: inline-flex;
            align-items: center;
            justify-content: center;
        }
        /* Icon glyph size inside any admin action button (pencil / trash) */
        body.admin-page #groups-tbody button i,
        body.admin-page #goals-tbody button i,
        body.admin-page #group-goals-tbody button i,
        body.admin-page #groups-tbody button svg,
        body.admin-page #goals-tbody button svg,
        body.admin-page #group-goals-tbody button svg,
        body.admin-page .admin-data-table .row-actions i,
        body.admin-page .admin-data-table .row-actions svg {
            width: 15px;
            height: 15px;
        }

        /* Table wrap — matches other reports */
        body.admin-page .admin-table-wrap {
            border: 1px solid var(--border-light);
            border-radius: 10px;
            overflow: hidden;
            background: var(--surface);
            box-shadow: 0 1px 2px rgba(16, 42, 94, 0.04);
        }
        body.admin-page .admin-table-wrap table { margin: 0 !important; }
        body.admin-page .admin-table-wrap thead th {
            background: var(--th-bg) !important;
            color: var(--th-text) !important;
            font-size: 0.72rem !important;
            text-transform: uppercase;
            letter-spacing: 0.05em;
            font-weight: 700 !important;
            border-bottom: 1px solid color-mix(in srgb, var(--th-bg) 75%, #000) !important;
        }
        body.admin-page .admin-table-wrap tbody tr { transition: background-color 0.15s ease; }
        body.admin-page .admin-table-wrap tbody tr:hover { background: var(--surface-2); }

        /* Data tables (Category / Tray Locations / Roles / Users) — uniform cell
           padding + a right-aligned action column with consistently-sized icon /
           text buttons, matching the Goal Groups / Goals tables above. */
        body.admin-page .admin-table-wrap table.admin-data-table th,
        body.admin-page .admin-table-wrap table.admin-data-table td {
            padding: 9px 12px;
            text-align: left;
            vertical-align: middle;
        }
        body.admin-page .admin-table-wrap table.admin-data-table tbody td {
            border-bottom: 1px solid var(--border-light);
            color: var(--text-strong);
        }
        body.admin-page .admin-table-wrap table.admin-data-table tbody tr:last-child td { border-bottom: none; }
        body.admin-page .admin-table-wrap table.admin-data-table .col-actions {
            text-align: right;
            white-space: nowrap;
            width: 1%;
        }
        body.admin-page .admin-table-wrap table.admin-data-table .row-actions {
            display: inline-flex;
            gap: 6px;
            align-items: center;
            justify-content: flex-end;
        }

        /* The Edit Tray Location modal now uses the shared .x-modal system in
           app.css (/js/x-modal.js) with canonical .field/.input/.btn inside. */

        /* Form rows: inline forms + goal/group forms */
        body.admin-page .styled-card form.inline-form {
            display: flex;
            flex-wrap: wrap;
            gap: 0.6rem;
            align-items: stretch;
        }
        body.admin-page .styled-card form.inline-form input[type="text"],
        body.admin-page .styled-card form.inline-form input[type="email"],
        body.admin-page .styled-card form.inline-form input[type="file"] {
            flex: 1;
            min-width: 180px;
        }

        /* Create user: password requirements */
        body.admin-page #password-requirements {
            display: none;
            grid-column: 1 / -1;
            font-size: 0.82rem;
            color: var(--text-muted);
            background: var(--surface-2);
            border: 1px solid var(--border-light);
            border-radius: 10px;
            padding: 0.6rem 0.85rem;
            margin-top: -0.2rem;
        }
        body.admin-page #password-requirements > div {
            padding: 2px 0;
        }
        body.admin-page #password-requirements > div.valid {
            color: var(--success);
            font-weight: 600;
        }
        body.admin-page #password-requirements > div.valid::before {
            content: "✓ ";
            color: var(--success);
            font-weight: 700;
        }

        /* Form grid for create-user */
        body.admin-page .styled-card .form-grid {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 0.9rem 1rem;
        }
        body.admin-page .styled-card form.card {
            background: transparent !important;
            border: none !important;
            box-shadow: none !important;
            padding: 0 !important;
            margin: 0;
        }
        body.admin-page .styled-card .form-actions {
            margin-top: 1rem;
            border-top: none;
            display: flex;
            justify-content: flex-end;
        }
        body.admin-page .styled-card .form-actions .btn.secondary-btn {
            background: linear-gradient(135deg, #276749, #2f855a) !important;
            color: var(--text-inverse) !important;
            border: none !important;
            box-shadow: 0 4px 12px rgba(47, 103, 73, 0.25);
        }
        body.admin-page .styled-card .form-actions .btn.secondary-btn:hover {
            box-shadow: 0 6px 16px rgba(47, 103, 73, 0.32);
        }

        /* Goal form save buttons — brand as primary CTA */
        body.admin-page #save-group-btn,
        body.admin-page #save-goal-btn,
        body.admin-page #save-group-goal-btn {
            background: linear-gradient(135deg, var(--accent), var(--accent-end, var(--accent-strong, var(--accent)))) !important;
            color: var(--on-brand) !important;
            border: none !important;
            box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 25%, transparent);
        }

        /* Group-goals sub-heading */
        body.admin-page .styled-card h4 {
            color: var(--text-strong);
            font-size: 1rem;
            margin: 1.2rem 0 0.6rem 0;
            padding-top: 0.8rem;
            border-top: 1px solid var(--border-light);
        }

        @media (max-width: 768px) {
            body.admin-page .styled-card { padding: 1rem; border-radius: 12px; }
            body.admin-page #manual-jobs-actions .primary-btn { flex: 1 0 calc(50% - 0.3rem); justify-content: center; }
            body.admin-page .styled-card .form-grid { grid-template-columns: 1fr; }
            body.admin-page .admin-table-wrap { overflow-x: auto; }
        }

        /* ===== Collapsible sections (native <details>/<summary>) ===== */
        body.admin-page details.styled-card { display: block; }
        body.admin-page details.styled-card > summary {
            list-style: none;
            cursor: pointer;
            user-select: none;
        }
        body.admin-page details.styled-card > summary::-webkit-details-marker { display: none; }
        /* The count-pill defaults to margin-left:auto (app.css); inside the summary the
           chevron ::after already owns that auto margin, so cancel it here to keep the
           pill next to the heading text instead of stranded in the middle of the row. */
        body.admin-page details.styled-card > summary .count-pill { margin-left: 0; }
        body.admin-page details.styled-card:not([open]) > summary {
            margin-bottom: 0;
            padding-bottom: 0;
            border-bottom: none;
        }
        body.admin-page details.styled-card:not([open]) {
            padding-bottom: 1.1rem;
        }
        body.admin-page details.styled-card > summary::after {
            content: "";
            margin-left: auto;
            width: 16px;
            height: 16px;
            flex-shrink: 0;
            background-color: var(--accent, #4a5568);
            -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>") center / contain no-repeat;
            mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>") center / contain no-repeat;
            transition: transform 0.18s ease;
        }
        body.admin-page details.styled-card:not([open]) > summary::after {
            transform: rotate(-90deg);
        }
        body.admin-page details.styled-card > summary:hover h2 {
            color: var(--accent);
        }
        body.admin-page details.styled-card > summary:focus-visible {
            outline: 2px solid var(--accent, var(--info));
            outline-offset: 2px;
            border-radius: 6px;
        }

        /* Section nav toolbar */
        body.admin-page #admin-section-toolbar {
            display: flex;
            justify-content: flex-end;
            gap: 0.5rem;
            margin-bottom: 1rem;
        }
        body.admin-page #admin-section-toolbar button {
            background: var(--surface);
            border: 1px solid var(--border);
            color: var(--text);
            border-radius: 8px;
            padding: 0.4rem 0.85rem;
            font-size: 0.8rem;
            font-weight: 500;
            cursor: pointer;
            display: inline-flex;
            align-items: center;
            gap: 6px;
            transition: background 0.15s ease, border-color 0.15s ease;
        }
        body.admin-page #admin-section-toolbar button:hover {
            background: var(--surface-2);
            border-color: var(--border-subtle);
        }
        body.admin-page #admin-section-toolbar button i {
            width: 14px;
            height: 14px;
        }

        /* Card column = grouped, ordered to match the subnav. CSS `order` keeps the
           DOM order intact, so the collapse-state JS, anchor jumps and localStorage
           keep working — only the visual order changes. */
        body.admin-page .admin-sections { display: flex; flex-direction: column; }
        body.admin-page .admin-group-head {
            font-family: var(--font-display, inherit);
            font-size: 0.72rem;
            font-weight: 700;
            text-transform: uppercase;
            letter-spacing: 0.08em;
            color: var(--text-muted);
            margin: 1.6rem 0 0.9rem;
            padding-bottom: 0.45rem;
            border-bottom: 1px solid var(--border-light);
            display: flex;
            align-items: center;
            gap: 8px;
        }
        body.admin-page .admin-group-head:first-child { margin-top: 0; }
        body.admin-page .admin-group-head i { width: 15px; height: 15px; color: var(--accent); }
/* ===== end admin editor pages ===== */

/* Admin sub-rail content column: allow wide tables/charts to shrink + scroll inside the .split grid. */
body.admin-page .admin-content{min-width:0}

/* Inline links inside rendered detail text (e.g. the alerts page's linkified
   order paths). The global a{color:inherit;text-decoration:none} reset makes
   such links invisible — this utility restores a visible, theme-aware link. */
.linkified a{color:var(--accent);text-decoration:underline;text-underline-offset:2px}
.linkified a:hover{opacity:.85}

/* Listing Specifics → package (shipping) dimensions. Break the six package
   fields (length/width/height/unit-of-length/weight/unit-of-weight) out of the
   panel's 4-col form grid onto their own dedicated row, all six side by side.
   grid-column:1/-1 makes the wrapper span the full panel row when it sits inside
   a CSS-grid panel (tray/review/fixing); when the parent isn't a grid it simply
   falls back to a full-width block, still a new row. align-items:end keeps the
   inputs bottom-aligned when a label wraps to two lines. */
.package-dims-row{
  grid-column:1 / -1;
  display:grid;
  grid-template-columns:repeat(6, minmax(0, 1fr));
  gap:.7rem 1.1rem;
  align-items:end;
}
@media (max-width:760px){
  .package-dims-row{ grid-template-columns:repeat(3, minmax(0, 1fr)); }
}
@media (max-width:460px){
  .package-dims-row{ grid-template-columns:repeat(2, minmax(0, 1fr)); }
}

/* ===== Entity timeline ======================================================
   ONE chronological feed of everything that happened with a record (built for
   the CRM customer 360; reuse it for any entity that grows a history). Shared
   chrome — never re-implement this per page. Kind colours reuse the pill
   palette, so a timeline dot and that record's pill read as the same thing.

   <ol class="timeline">
     <li class="tl-item">
       <span class="tl-dot k-order"><i data-lucide="shopping-bag"></i></span>
       <div class="tl-main">
         <div class="tl-head"><span class="tl-title">…</span><span class="tl-when">…</span></div>
         <div class="tl-detail">…</div><div class="tl-meta">…</div>
       </div>
     </li>
   </ol>                                                                     */
.timeline{list-style:none;margin:0;padding:2px 0 0;position:relative}
/* The rail sits under the dots (they paint over it at z-index 1). */
.timeline::before{content:"";position:absolute;left:12px;top:8px;bottom:8px;width:2px;background:var(--border-light)}
.tl-item{position:relative;display:grid;grid-template-columns:26px minmax(0,1fr);gap:12px;padding-bottom:16px}
.tl-item:last-child{padding-bottom:0}
.tl-dot{position:relative;z-index:1;width:26px;height:26px;border-radius:50%;display:flex;
  align-items:center;justify-content:center;background:var(--gray-bg);color:var(--gray);
  border:2px solid var(--surface)}
.tl-dot svg,.tl-dot i[data-lucide]{width:14px;height:14px}
.tl-main{min-width:0}
.tl-head{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
.tl-title{font-size:13.5px;font-weight:600;color:var(--text)}
.tl-title a{color:var(--text)}
.tl-title a:hover{color:var(--accent)}
.tl-when{font-family:var(--font-mono);font-size:11px;color:var(--text-muted);white-space:nowrap}
/* pre-wrap so a pasted note keeps its line breaks; anywhere so a long id or URL
   can't widen the card (see the .split minmax(0,1fr) note above). */
.tl-detail{margin-top:3px;font-size:12.5px;color:var(--text-subtle);white-space:pre-wrap;overflow-wrap:anywhere}
.tl-meta{margin-top:3px;font-size:11.5px;color:var(--text-muted)}
.tl-dot.k-note{background:var(--amber-bg);color:var(--amber)}
.tl-dot.k-order{background:var(--green-bg);color:var(--green)}
.tl-dot.k-return{background:var(--red-bg);color:var(--red)}
.tl-dot.k-consignment{background:var(--violet-bg);color:var(--violet)}
.tl-dot.k-repair{background:var(--blue-bg);color:var(--blue)}
.tl-dot.k-lead{background:var(--blue-bg);color:var(--blue)}
.tl-dot.k-message{background:var(--gray-bg);color:var(--gray)}
.tl-dot.k-search_request{background:var(--amber-bg);color:var(--amber)}
.tl-dot.k-consent{background:var(--violet-bg);color:var(--violet)}
.tl-dot.k-marketing{background:var(--blue-bg);color:var(--blue)}
.tl-dot.k-social{background:var(--violet-bg);color:var(--violet)}
.tl-dot.k-address{background:var(--gray-bg);color:var(--gray)}
.tl-dot.k-occasion{background:var(--violet-bg);color:var(--violet)}
.tl-dot.k-customer{background:var(--accent-soft);color:var(--accent)}
.tl-dot.k-circle{background:var(--blue-bg);color:var(--blue)}
/* The scroll box for a feed that sits ABOVE a compose field in a modal (the
   notes modals). Its height is FIXED, not content-derived, and that is the whole
   point: the feed arrives from a fetch, so a box sized by its contents opens at
   the height of the loading skeleton and then jumps to the height of the notes —
   the dialog visibly resizing under the cursor. A constant box is the same size
   from the first paint, whatever lands in it (skeleton, notes, or the empty
   state — which must live INSIDE it for the same reason). It also keeps the
   compose field reachable: a long history scrolls here instead of pushing the
   field it belongs to below the fold. */
.notes-feed{height:min(340px,40vh);overflow-y:auto;margin-bottom:1rem;padding-right:4px}
/* MODAL FORM FIELD — the bold question-style label and the full-width control
   under it, in the tray editor's modals (Assign Task, Skip, Add Item…) and in
   THE Notes window (partials/notes-modal), which every page with notes shares —
   a tray, a purchase, a trade-in, a consignment and the reports that open them.
   Lived in edit-panels.css until the Notes window left the tray pages. */
.consignment-question{display:block;margin:.25rem 0 .5rem;font-size:1rem;font-weight:600;color:var(--text)}
.consignment-input{width:100%;padding:.7rem .85rem;font-family:inherit;font-size:1rem;border:1px solid var(--border-subtle);border-radius:8px;box-sizing:border-box;transition:border-color .15s,box-shadow .15s}
.consignment-input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-focus-ring)}

/* ===== Authored notes on a record ==========================================
   Free text a colleague wrote, so the byline and the edited marker are part of
   the component — an unattributed note is worth much less than an attributed
   one. Pinned notes carry an accent edge.                                   */
.note-list{display:flex;flex-direction:column;gap:10px}
.note{background:var(--surface-2);border:1px solid var(--border-light);
  border-radius:var(--radius-sm);padding:10px 12px}
.note.pinned{border-color:var(--accent);box-shadow:inset 3px 0 0 var(--accent)}
.note-body{font-size:13px;color:var(--text);white-space:pre-wrap;overflow-wrap:anywhere}
.note-meta{display:flex;align-items:center;gap:8px;margin-top:7px;font-size:11.5px;color:var(--text-muted)}
.note-meta .spacer{flex:1}
.note-edit{width:100%;min-height:70px;resize:vertical}

/* ===== A logged value inside a change/audit log table =====================
   One recorded value (an old/new field value, an error reason) in a log row.
   Capped so a single enormous entry scrolls in its own box instead of
   stretching the row past the rest of the table. Shared by the tray Change Log
   modal and the inventory Listing-error History modal.                      */
.log-value{max-height:100px;overflow-y:auto;white-space:pre-wrap;word-break:break-all}

/* ===== Choice list (pick one of N records) ================================
   A radio list where each option is a card: a title line plus supporting
   detail. Used by the consignment intake's "which of this customer's
   addresses?" dialog; reach for it whenever the operator must pick one record
   out of a handful rather than type it again.                               */
.choice{display:flex;flex-direction:column;gap:8px}
.choice-opt{display:flex;gap:10px;align-items:flex-start;padding:11px 13px;cursor:pointer;
  border:1px solid var(--border);border-radius:10px;background:var(--surface);transition:.12s}
.choice-opt:hover{border-color:var(--border-strong);background:var(--surface-2)}
.choice-opt:has(input:checked){border-color:var(--accent);background:var(--accent-soft)}
.choice-opt input[type=radio]{margin:2px 0 0;accent-color:var(--accent);flex:0 0 auto}
.choice-opt .co-body{min-width:0;flex:1}
.choice-opt .co-title{font-size:13px;font-weight:600;color:var(--text);
  display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.choice-opt .co-sub{font-size:12px;color:var(--muted);margin-top:2px;line-height:1.5}

/* ===== CRM customer picker (shared typeahead) =============================
   The "link this record to a CRM customer" control: a search input whose
   results drop down beneath it, and a chip showing the linked customer.
   Used by the consignment intake form (/consignments/new); the older
   per-page copies (search requests, consignment list modal) migrate onto
   these classes as they're touched.                                        */
.cust-picker{position:relative}
.cust-results{position:absolute;z-index:40;left:0;right:0;margin-top:4px;
  border:1px solid var(--border);border-radius:8px;background:var(--surface);
  box-shadow:0 8px 24px rgba(16,42,94,.12);display:none;max-height:240px;overflow-y:auto}
.cust-results.open{display:block}
.cust-opt{padding:8px 11px;cursor:pointer;border-bottom:1px solid var(--border);
  display:flex;flex-direction:column;gap:2px}
.cust-opt:last-child{border-bottom:0}
.cust-opt:hover,.cust-opt.active{background:var(--surface-2)}
.cust-opt .nm{color:var(--text);font-size:13px;font-weight:500}
.cust-opt .sub,.cust-empty{color:var(--muted);font-size:12px}
.cust-empty{padding:9px 11px}
.cust-selected{display:flex;align-items:center;gap:10px;border:1px solid var(--border);
  border-radius:9px;background:var(--surface-2);padding:8px 11px}
.cust-selected[hidden]{display:none}
.cust-selected .nm{font-weight:600;font-size:13px;color:var(--text)}
.cust-selected .sub{color:var(--muted);font-size:12px}
.cust-selected i[data-lucide]{color:var(--green,var(--accent));flex:0 0 auto}
/* The linked customer opens their CRM record (a new tab). Only an <a> WITH an
   href is a link — the form leaves it off for a viewer without CRM access, and
   the chip then reads as plain text. */
.cust-selected .cust-open{display:flex;align-items:center;gap:10px;min-width:0;border-radius:6px}
.cust-selected .cust-open .meta{display:flex;flex-direction:column;gap:1px;min-width:0}
.cust-selected .cust-open[href]:hover .nm{color:var(--link);text-decoration:underline}
.cust-selected .cust-open:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* ===== Audit / history list (canonical) ==================================
   The "who changed what, when" list shown in a report's History modal —
   consignments + trade-ins. `.audit-scope` names the item (or the record
   itself) an entry belongs to. `tr.row-clickable` is a table row that opens
   the record on click. */
.audit-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:8px}
.audit-row{border:1px solid var(--border);border-radius:8px;padding:.6rem .8rem;background:var(--surface);font-size:.85rem}
.audit-row .audit-when{color:var(--muted);font-size:.78rem}
.audit-row .audit-who{font-weight:600;color:var(--text)}
.audit-row .audit-field{font-family:var(--font-mono);color:var(--accent);font-size:.82rem}
.audit-row .audit-old{color:var(--red);text-decoration:line-through;margin-right:6px}
.audit-row .audit-new{color:var(--green);font-weight:600}
.audit-row .audit-scope{font-family:var(--font-mono);font-size:.75rem;background:var(--surface-2);border:1px solid var(--border);border-radius:5px;padding:1px 5px;margin-right:6px;color:var(--muted)}
.table tbody tr.row-clickable{cursor:pointer}
.table tbody tr.row-clickable:hover{background:var(--surface-2)}
/* A clickable row reached by Tab (tabindex="0", opened with Enter) shows where the focus is. */
.table tbody tr.row-clickable:focus-visible{outline:2px solid var(--accent);outline-offset:-2px;background:var(--surface-2)}
/* Two-line customer cell: name, then the e-mail in muted small type. */
.customer-cell .name{font-weight:600;color:var(--text);display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.customer-cell .contact{display:block;color:var(--muted);font-size:.78rem;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The ellipsis above does NOTHING without a cap: a display:block child just takes
   the cell's auto width, so the column grew to the longest e-mail on the page.
   The cap is viewport-relative on purpose — a wide monitor gets more characters
   before the … appears, a laptop keeps the column narrow — because a fixed px cap
   would clip the same name identically on a 27" screen with room to spare.
   The trailing `- 3ch` is a character-count trim, not a pixel one: this column was
   tuned by how many characters fit, so it shrinks by three glyphs at whatever font
   size the line is drawn at (the name 13px, the e-mail .78rem). Adjust THAT number
   to take more or fewer characters off; leave the clamp for how it tracks the screen. */
.customer-cell .name,
.customer-cell .contact{max-width:calc(clamp(130px,12vw,210px) - 3ch)}
/* A short fact under any cell's value, on its own line — the same muted small
   type as the customer cell's e-mail (the Purchases list: "Item" / "Mail-in"). */
.cell-sub{display:block;color:var(--muted);font-size:.78rem;margin-top:2px;white-space:nowrap}

/* ===== Image tiles + upload row (canonical) ===============================
   The square-thumbnail grid with a floating remove button and an Upload row
   beneath it — the product page's image UI. Promoted here from inventory.css
   the moment a second page needed it (consignment intake: ID photo + item
   photos). Drag-reorder + video-play affordances stay in inventory.css —
   they're product-page-specific. `.sm` caps tiles at 132px for single-photo
   widgets inside a form column. */
.inv-image-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:0.75rem}
.inv-image-grid.sm{grid-template-columns:repeat(auto-fill,132px)}
.inv-image-grid[hidden]{display:none}
.inv-image-tile{position:relative;aspect-ratio:1;border:1.25px solid var(--border);
  border-radius:10px;overflow:hidden;
  background:linear-gradient(135deg,var(--surface-2),var(--surface-3));
  box-shadow:0 1px 3px rgba(16,42,94,0.05)}
.inv-image-tile img{width:100%;height:100%;object-fit:cover;display:block;cursor:zoom-in}
.inv-image-tile .inv-image-placeholder{position:absolute;inset:0;
  display:flex;align-items:center;justify-content:center;
  color:var(--text-faint);font-size:0.8rem;text-align:center;padding:0.5rem;word-break:break-all}
.inv-image-tile .inv-image-remove{position:absolute;top:6px;right:6px;width:26px;height:26px;
  background:var(--overlay);color:var(--text-inverse);border:none;border-radius:999px;cursor:pointer;
  display:flex;align-items:center;justify-content:center;
  padding:0;font-size:0;line-height:0;transition:background 0.15s ease}
.inv-image-tile .inv-image-remove:hover{background:var(--danger)}
.inv-image-tile .inv-image-remove svg,.inv-image-tile .inv-image-remove i[data-lucide]{width:14px;height:14px}
.inv-image-add{display:flex;gap:0.5rem;margin-top:0.3rem;align-items:center}
/* The hidden ATTRIBUTE must always win on these components. Their author
   display rules (grid/flex above) silently defeated it — the UA's
   [hidden]{display:none} loses to any author display — so el.hidden=true left
   the consignment form's remove button (and the upload row) fully visible. */
.inv-image-grid[hidden],.inv-image-add[hidden],.inv-image-tile .inv-image-remove[hidden]{display:none}
.inv-image-add input{flex:1}

/* ===== Auto-growing single-line field (/js/autogrow.js) ===================
   A text input whose value is wider than its box is upgraded in place to a
   textarea that WRAPS instead of clipping, so a long value is fully readable
   without the operator dragging through the field. It must read as the same
   control it replaced: no resize grip, no scrollbar, no min-height of its own
   — the height is measured from the content and written inline by the script,
   so this only has to get out of its way. Last in the file so it wins over
   `.field textarea` / `textarea.input` (same specificity) on any page.      */
/* The attribute is doubled deliberately: page sheets load AFTER app.css, so a
   plain `textarea[data-autogrown]` would lose to any page's generic
   `.card textarea{resize:vertical}` and the field would grow a resize grip it
   must not have. */
textarea[data-autogrown][data-autogrown]{
  resize:none;overflow:hidden;min-height:0;
  white-space:pre-wrap;overflow-wrap:break-word}

/* CoPilot chat: a customer message the model DRAFTED and the operator must approve.
   The model cannot send; the button is the only send path (pending-sends.js). */
.sw-send-card { margin-top: 10px; border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: 8px; background: var(--surface-2); padding: 10px 12px; }
.sw-send-card.sent { border-left-color: var(--success); opacity: .85; }
.sw-send-head { font-weight: 600; font-size: 13px; display: flex; align-items: center; gap: 6px; }
.sw-send-meta { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
.sw-send-warn { font-size: 12px; color: var(--danger); margin-top: 6px; }
.sw-send-body { white-space: pre-wrap; font-family: var(--font-body); font-size: 13px; margin: 8px 0; padding: 8px 10px; background: var(--surface); border: 1px solid var(--border-light); border-radius: 6px; max-height: 220px; overflow: auto; }

/* "Since you were last here" — the dashboard's one backward-looking card
   (public/js/away-card.js). Lives here, not inline in the builder, because the
   card is built from a JS template string and every colour/size it needs has to
   come from the tokens like any other component.

   The box is RESERVED at a fixed min-height, sized to the card each role usually
   gets (measured against real windows, 2026-09-12): the head plus two rows, and a
   see-all role adds the "Across the shop" block. views/index.ejs draws the
   skeleton in that same shape, so the fetch landing doesn't move the Overview
   grid below. Only an unusually busy window (or a phone, where rows stack and
   wrap) grows it. */
.away-body { min-height:82px; }            /* head, two rows */
.away-body.with-shop { min-height:122px; } /* head, "Across the shop", two rows */
/* Every line holds at least one text line (1.5em) — true of real text already —
   so a skeleton bar standing in for a line takes that line's height. */
.away-head { display:flex; align-items:baseline; gap:10px; margin:0 0 10px; min-height:1.5em; }
.away-head > .skel-bar { align-self:center; }
/* A quiet one-liner is centred in the reserved box, not stranded at its top. */
.away-quiet { display:flex; align-items:center; gap:8px; color:var(--muted); min-height:inherit; }
.away-quiet svg,.away-quiet i[data-lucide] { width:16px; height:16px; flex:0 0 auto; }
/* The rows flow into COLUMNS at most 2 tall, never one long stack: a busy window
   can carry a sale plus six trays plus six cases, and thirteen stacked rows would
   push the whole Overview grid off the fold. The column count is ceil(n/2) capped
   at 4, written by away-card.js as `data-cols` — a COUNT, not chrome, so the card
   still gets every colour and size from here. One or two rows stay one column. */
.away-list { list-style:none; margin:0; padding:0; display:grid; grid-template-columns:1fr;
  gap:7px 26px; align-items:start; }
.away-list[data-cols="2"]{ grid-template-columns:repeat(2,minmax(0,1fr)) }
.away-list[data-cols="3"]{ grid-template-columns:repeat(3,minmax(0,1fr)) }
.away-list[data-cols="4"]{ grid-template-columns:repeat(4,minmax(0,1fr)) }
.away-list li { display:flex; align-items:center; gap:8px; flex-wrap:wrap; font-size:14px; min-width:0; min-height:1.5em; }
.away-list li > span { min-width:0; }
.away-list li svg,.away-list li i[data-lucide] { width:15px; height:15px; flex:0 0 auto; color:var(--muted); }
.away-list li.away-sold svg { color:var(--green); }
/* "Across the shop" — the see-all block appended under the viewer's own rows.
   A quiet labelled divider, not a second card: it is the same card continuing,
   so it gets a rule and a caption rather than its own surface. Colour is carried
   ONLY by the icon (the row text stays --text), so a five-line block doesn't read
   as five competing alerts. */
.away-shop-head { margin:13px 0 9px; padding-top:12px; border-top:1px solid var(--border-light);
  font-size:11px; font-weight:600; letter-spacing:.06em; text-transform:uppercase; color:var(--muted);
  min-height:1.5em; }
.away-list li.away-up   svg { color:var(--accent); }
.away-list li.away-warn svg { color:var(--warning); }
.away-list li.away-bad  svg { color:var(--danger); }

/* "What's new" — the dashboard's release-notes window (.x-modal #whats-new, filled by
   /js/release-notes.js from docs/release-notes.md). One section per release, newest
   first: the date (.t-cap), the release's name, then its notes. The notes are
   server-rendered markdown, so the list chrome is set here rather than on each <li>. */
.whats-new-release + .whats-new-release { margin-top:18px; padding-top:16px; border-top:1px solid var(--border-light); }
.whats-new-title { margin:2px 0 10px; font-family:var(--font-display); font-size:15px; font-weight:600;
  letter-spacing:-.02em; color:var(--text-strong); }
.whats-new-release > .t-cap + .whats-new-notes { margin-top:8px; }
.whats-new-notes ul { margin:0; padding-left:18px; }
.whats-new-notes li { margin:0 0 10px; line-height:1.5; color:var(--text-2); }
.whats-new-notes li:last-child { margin-bottom:0; }
.whats-new-notes li::marker { color:var(--muted); }
.whats-new-notes strong { color:var(--text-strong); font-weight:600; }
.whats-new-notes p { margin:0 0 8px; }
.whats-new-notes a { color:var(--accent); }

/* ===================== CALENDAR =====================
   My Work → Calendar (month / week / list), the calendar modals
   (partials/calendar-modals.ejs) and the CRM customer page's Appointments card.
   Driven by /js/calendar.js. Entry colours reuse the .pill tone tokens, so a
   chip reads exactly like a pill of the same tone in both themes. */

/* Small shared utilities the calendar needed and nothing provided yet. */
.field[hidden],.chip[hidden],.field-grid[hidden]{display:none}
/* A link inside an empty state (e.g. "add them under Admin → Calendar") must read as one. */
.empty a{color:var(--accent);text-decoration:underline}
.field-grid>.field.wide{grid-column:1/-1}
.card-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}
tr.is-muted td{opacity:.55}

.cal-toolbar{gap:10px}
.cal-title{font-family:var(--font-display);font-size:17px;font-weight:600;letter-spacing:-.02em;margin:0;min-width:180px;color:var(--text)}
.cal-card{overflow:hidden}
.cal-overdue{margin-bottom:14px}
.cal-overdue[hidden]{display:none}
.cal-tz-note,.cal-empty-note{font-size:12px;margin:10px 2px 0}
.cal-empty-note{padding:0 14px 14px}

/* Tones: text + fill from the pill tokens. */
.cal-chip,.cal-block,.cal-row-icon{color:var(--gray);background:var(--gray-bg)}
.cal-chip.blue,.cal-block.blue,.cal-row-icon.blue{color:var(--blue);background:var(--blue-bg)}
.cal-chip.amber,.cal-block.amber,.cal-row-icon.amber{color:var(--amber);background:var(--amber-bg)}
.cal-chip.red,.cal-block.red,.cal-row-icon.red{color:var(--red);background:var(--red-bg)}
.cal-chip.violet,.cal-block.violet,.cal-row-icon.violet{color:var(--violet);background:var(--violet-bg)}
.cal-chip.green,.cal-block.green,.cal-row-icon.green{color:var(--green);background:var(--green-bg)}
.cal-chip.is-done,.cal-block.is-done{opacity:.55;text-decoration:line-through}
.cal-chip.is-overdue,.cal-block.is-overdue{box-shadow:inset 3px 0 0 var(--red)}

/* Month */
.cal-month{display:grid;grid-template-columns:repeat(7,minmax(0,1fr))}
.cal-dow{padding:8px 10px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);border-bottom:1px solid var(--border);background:var(--surface-2)}
.cal-cell{min-height:118px;padding:6px;border-right:1px solid var(--border-light);border-bottom:1px solid var(--border-light);display:flex;flex-direction:column;gap:3px;min-width:0}
.cal-cell:nth-child(7n+7){border-right:0}
.cal-cell.is-other{background:var(--surface-2)}
.cal-cell.is-other .cal-date{color:var(--muted)}
.cal-date{align-self:flex-start;min-width:26px;height:26px;padding:0 6px;border-radius:13px;border:0;background:transparent;color:var(--text);font-family:var(--font-display);font-size:13px;font-weight:600;cursor:pointer}
.cal-date:hover{background:var(--surface-3)}
.cal-cell.is-today .cal-date{background:var(--accent);color:var(--on-brand)}
.cal-chip{display:flex;align-items:center;gap:5px;width:100%;min-width:0;height:22px;padding:0 6px;border:0;border-radius:6px;font-size:12px;font-weight:500;text-align:left;cursor:pointer}
.cal-chip svg,.cal-chip i[data-lucide]{width:12px;height:12px;flex:0 0 auto}
.cal-chip:hover{filter:brightness(.96)}
.cal-chip-time{font-family:var(--font-mono);font-size:11px;flex:0 0 auto}
.cal-chip-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
.cal-more{align-self:flex-start;border:0;background:transparent;color:var(--muted);font-size:11.5px;font-weight:600;padding:1px 6px;cursor:pointer;border-radius:6px}
.cal-more:hover{color:var(--text);background:var(--surface-3)}
.cal-skel{height:18px;border-radius:6px}

/* Week */
.cal-week{--cal-hour:44px;display:flex;flex-direction:column;min-width:0;overflow-x:auto}
.cal-week-row{display:grid;grid-template-columns:52px repeat(7,minmax(96px,1fr))}
.cal-gutter{font-size:11px;color:var(--muted);padding:6px 6px 0 0;text-align:right}
.cal-week-head{border-bottom:1px solid var(--border);background:var(--surface-2)}
.cal-week-day{display:flex;align-items:baseline;justify-content:center;gap:6px;padding:8px 4px;border:0;background:transparent;color:var(--text-2);font-size:12px;cursor:pointer}
.cal-week-day b{font-family:var(--font-display);font-size:16px;color:var(--text)}
.cal-week-day:hover{background:var(--surface-3)}
.cal-week-day.is-today b{color:var(--accent)}
.cal-week-allday{border-bottom:1px solid var(--border)}
.cal-week-allday .cal-gutter{padding-top:8px}
.cal-allday-cell{display:flex;flex-direction:column;gap:3px;padding:4px;min-height:34px;border-left:1px solid var(--border-light);min-width:0}
.cal-week-body{position:relative}
.cal-hours{padding:0}
.cal-hour{height:var(--cal-hour);padding-right:6px;transform:translateY(-6px)}
.cal-hour:first-child{transform:none}
.cal-col{position:relative;height:calc(var(--cal-hour) * var(--cal-hours));border-left:1px solid var(--border-light);background-image:linear-gradient(to bottom,var(--border-light) 1px,transparent 1px);background-size:100% var(--cal-hour)}
.cal-col.is-today{background-color:var(--accent-soft)}
.cal-block{position:absolute;display:flex;flex-direction:column;align-items:flex-start;gap:1px;padding:3px 6px;border:0;border-radius:6px;overflow:hidden;text-align:left;cursor:pointer;font-size:11.5px;border-left:1px solid var(--surface)}
.cal-block:hover{filter:brightness(.96);z-index:2}
.cal-block-time{font-family:var(--font-mono);font-size:10.5px;white-space:nowrap}
.cal-block-title{color:var(--text);font-weight:600;line-height:1.25;overflow:hidden}
.cal-block.is-short{flex-direction:row;align-items:center;gap:6px;padding-top:0;padding-bottom:0}
.cal-block.is-short .cal-block-title{white-space:nowrap;text-overflow:ellipsis;min-width:0}
.cal-settings-switch{margin:4px 0 14px}

/* List */
.cal-agenda{display:flex;flex-direction:column}
.cal-agenda-day+.cal-agenda-day{border-top:1px solid var(--border)}
.cal-agenda-date{display:flex;align-items:center;gap:8px;padding:12px 16px 4px;font-family:var(--font-display);font-size:13.5px;font-weight:600;color:var(--text)}
.cal-row{display:grid;grid-template-columns:150px 30px minmax(0,1fr) auto;align-items:center;gap:12px;width:100%;padding:8px 16px;border:0;background:transparent;text-align:left;cursor:pointer;color:var(--text)}
.cal-row:hover{background:var(--surface-2)}
.cal-row-time{font-family:var(--font-mono);font-size:12px;color:var(--text-2);white-space:nowrap}
.cal-row-icon{width:30px;height:30px;border-radius:8px;display:grid;place-items:center}
.cal-row-icon svg,.cal-row-icon i[data-lucide]{width:15px;height:15px}
.cal-row-main{display:flex;flex-direction:column;min-width:0}
.cal-row-title{font-weight:600;font-size:13.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cal-row-sub{font-size:12px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* Modals + the CRM card */
.cal-facts{display:grid;grid-template-columns:120px minmax(0,1fr);gap:9px 16px;margin:0 0 12px;font-size:13.5px}
.cal-facts dt{color:var(--muted);font-weight:500}
.cal-facts dd{margin:0;color:var(--text);overflow-wrap:anywhere}
.cal-inline-icon{width:14px;height:14px;vertical-align:-2px}
.cal-feed-howto{margin:10px 0 0;padding-left:18px;font-size:13px;color:var(--text-2);display:flex;flex-direction:column;gap:4px}
.cal-allday-check{align-self:end;min-height:36px}
tr.cal-link-row{cursor:pointer}
tr.cal-link-row:hover td{background:var(--surface-2)}

/* A second count on one menu entry (My Work: appointments today), told apart from
   the first by its icon and colour. Right-aligned on its own, 4px after the first
   when both show. */
.navlink .badge.nav-count-alt{display:inline-flex;align-items:center;gap:3px;background:var(--blue-bg);color:var(--blue)}
.navlink .badge.nav-count-alt svg,.navlink .badge.nav-count-alt i[data-lucide]{width:11px;height:11px}
.navlink .badge.nav-count-alt b{font-weight:600}
.navlink .badge:not([hidden]) + .badge{margin-left:4px}

/* ===================== CHECKBOX DROPDOWN =====================
   The multi-select: <details class="inv-dropdown"> > <summary class="inv-dropdown-toggle">
   + <div class="inv-dropdown-panel"> of label.inv-dropdown-item > checkbox +
   span.inv-dropdown-item-name. The toggle reads a STABLE field name + an
   .inv-dd-count badge. Built for the inventory filter bars (inventory.js drives
   those); any other page adds data-dd-chip="<name>" on the <details> and loads
   /js/checkbox-dropdown.js. Kept LAST in this sheet: it used to live in
   inventory.css (loaded after app.css), so it must still win equal-specificity
   ties with the rules above. */
.toolbar details.inv-dropdown { position: relative; }
/* Canonical-toolbar footprint: auto width (the label is a fixed field name, not a
   growing selection), 36px tall, matching .input / .tb-input / .chip beside it. */
.toolbar .inv-dropdown-toggle {
  width: auto; min-width: 0; max-width: 230px;
  height: 36px; padding: 0 12px;
  border: 1px solid var(--border); border-radius: 9px;
  font-size: 13px; font-weight: 500; color: var(--text);
  gap: 8px;
}
.toolbar .inv-dropdown-toggle:hover { border-color: var(--border-strong); }
.toolbar .inv-dropdown[open] .inv-dropdown-toggle {
  border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}
/* Count of ticked options, inside the toggle — this is what tells you the filter
   is on, now that the toggle shows a stable field name instead of the selection.
   Render it `hidden` at zero and only UPDATE it (never insert it after paint). */
.inv-dd-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; padding: 0 5px; flex: 0 0 auto;
  border-radius: 999px; font-size: 11px; font-weight: 700;
  background: var(--accent-soft); color: var(--accent);
}
.inv-dd-count[hidden] { display: none; }
details.inv-dropdown { position: relative; }
.inv-dropdown-toggle {
  list-style: none;
  cursor: pointer;
  padding: 0.45rem 0.6rem;
  border: 1.25px solid var(--border);
  border-radius: 8px;
  font-size: 0.85rem;
  /* Fixed footprint: a restored single-selection can carry a long category/
     warehouse name, and a min-width-only toggle would grow to fit it once the
     selection is applied (after the DB load), reflowing the whole filter
     toolbar. Pin the width and ellipsis the label so the card never shifts. */
  width: 180px;
  min-width: 180px;
  max-width: 180px;
  overflow: hidden;
  background: var(--surface);
  color: var(--text-strong);
  font-weight: 500;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.inv-dropdown-toggle::-webkit-details-marker { display: none; }
/* The label is wrapped in .inv-dd-label so a long name truncates instead of
   widening the toggle. */
.inv-dropdown-toggle .inv-dd-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Same chevron the native <select> uses (`select.input`), so the custom
   checkbox dropdowns match a native dropdown exactly. */
.inv-dropdown-toggle::after {
  content: '';
  flex: none;
  width: 14px;
  height: 14px;
  background-image: var(--select-chevron);
  background-repeat: no-repeat;
  background-position: center;
}
.inv-dropdown[open] .inv-dropdown-toggle {
  border-color: var(--info);
  box-shadow: 0 0 0 3px var(--accent-focus-ring);
}
.inv-dropdown-panel {
  position: absolute;
  z-index: 30;
  top: calc(100% + 4px);
  left: 0;
  min-width: 220px;
  max-height: 280px;
  overflow-y: auto;
  background: var(--surface);
  border: 1.25px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(16, 42, 94, 0.12);
  padding: 0.4rem;
}
/* Set by /js/checkbox-dropdown.js when the panel would spill off a screen edge:
   .dd-end hangs it off the toggle's RIGHT edge, .dd-start off its left. Specific
   enough to beat mobile.css's phone-width `right:0`. */
details.inv-dropdown.dd-end > .inv-dropdown-panel { left: auto; right: 0; }
details.inv-dropdown.dd-start > .inv-dropdown-panel { left: 0; right: auto; }
/* Scoped under .inv-dropdown-panel so these win over the generic
   `.inv-toolbar label` rule (which forces column layout + muted small font). */
.inv-dropdown-panel .inv-dropdown-item {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.5rem;
  border-radius: 6px;
  font-size: 0.85rem;
  color: var(--text-strong);
  font-weight: 400;
  cursor: pointer;
}
.inv-dropdown-panel .inv-dropdown-item:hover { background: var(--surface-3); }
.inv-dropdown-item-name {
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.inv-dropdown-panel input[type="checkbox"] {
  flex: 0 0 auto;
  min-width: auto;
  width: auto;
  padding: 0;
  margin: 0;
  /* The generic `.inv-toolbar input` rule sets appearance:none (for the text
     filters/selects); restore the native checkbox + give it the accent tick. */
  appearance: auto;
  -webkit-appearance: auto;
  accent-color: var(--accent);
}
.inv-dropdown-empty {
  padding: 0.5rem;
  color: var(--text-muted);
  font-size: 0.8rem;
}
/* Loading placeholder for a dropdown's options — the canonical .skel-text
   shimmer sized to the .inv-dropdown-item rhythm, so the panel opens at the
   height its options will fill. */
.inv-dropdown-panel > .skel-text { padding: 0.4rem 0.5rem; gap: 14px; }
.inv-dropdown-panel > .skel-text > .skel-bar { height: 10px; }
/* Small caption row at the top of a checkbox dropdown panel (e.g. the bulk-ops
   Category / Overall-condition pickers). */
.inv-dropdown-hint {
  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-muted);
  padding: 0.15rem 0.5rem 0.3rem;
  margin-bottom: 0.1rem;
  border-bottom: 1px solid var(--border-light);
  white-space: nowrap;
}
/* Inside an .x-modal the body scrolls (overflow:auto) and would clip an absolute
   panel — render it in-flow so it expands the form instead, full-width toggle. */
.x-modal__body details.inv-dropdown { position: relative; }
.x-modal__body .inv-dropdown-toggle { width: 100%; min-width: 0; max-width: none; }
.x-modal__body .inv-dropdown-panel { position: static; margin-top: 0.35rem; min-width: 0; box-shadow: none; }
/* A .styled-card clips its content (overflow:hidden); let an open dropdown's
   panel overhang the card instead of being cut off. */
.styled-card:has(details.inv-dropdown[open]) { overflow: visible; }
body.admin-page .styled-card form.inline-form details.inv-dropdown { align-self: center; }

/* ======================= ACTION MENU (.act-menu) =======================
   THE "more actions" dropdown: a button that opens a short list of actions, so a
   crowded bar keeps only its main controls in view. A <details>, so it opens and
   closes with no script; /js/act-menu.js (app-wide, from partials/icons) adds
   close-on-outside-click, Escape, close-after-a-pick and the side that fits.
   Same panel chrome as the avatar's account menu (.user-menu-dropdown).
     <details class="act-menu">
       <summary class="btn ghost sm" data-tip="Item — …"><i data-lucide="package"></i> Item <i data-lucide="chevron-down" class="act-menu-caret"></i></summary>
       <div class="act-menu-pop">
         <div class="act-menu-head"><strong>Title</strong>Subtitle</div>
         <a class="act-menu-item" href="…"><i data-lucide="store"></i> Open on eBay</a>
         <div class="act-menu-sep"></div>
         <button type="button" class="act-menu-item">…</button>
       </div>
     </details>
   An icon-only trigger (`btn ghost sm icon` + `ellipsis`) carries data-tip +
   aria-label like every icon button. Worked example: the CRM inbox thread head. */
.act-menu { position: relative; display: inline-flex; }
.act-menu > summary { list-style: none; cursor: pointer; user-select: none; }
.act-menu > summary::-webkit-details-marker { display: none; }
.act-menu > summary::marker { content: ""; }
.act-menu[open] > summary { background: var(--surface-3); color: var(--text); }
.act-menu-caret, .btn .act-menu-caret, .act-menu i.act-menu-caret[data-lucide] { width: 14px; height: 14px; margin-left: -2px; opacity: .7; transition: transform .12s ease; }
.act-menu[open] .act-menu-caret { transform: rotate(180deg); }
.act-menu-pop {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 1000;
  min-width: 220px; max-width: min(320px, calc(100vw - 32px)); box-sizing: border-box;
  background: var(--surface); border: 1.25px solid var(--border); border-radius: 8px;
  box-shadow: var(--shadow-lg); padding: 6px;
}
.act-menu.act-menu-start .act-menu-pop { right: auto; left: 0; }
.act-menu-head { padding: 6px 10px 8px; margin-bottom: 4px; border-bottom: 1px solid var(--border-light); font-size: 12px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.act-menu-head strong { display: block; color: var(--text); font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.act-menu-item {
  display: flex; align-items: center; gap: 10px; width: 100%; box-sizing: border-box;
  padding: 8px 10px; border: 0; border-radius: 6px; background: none; color: var(--text);
  font: inherit; font-size: 13px; text-align: left; text-decoration: none; cursor: pointer; white-space: nowrap;
}
.act-menu-item:hover, .act-menu-item:focus-visible { background: var(--surface-2); color: var(--text); text-decoration: none; outline: none; }
.act-menu-item svg, .act-menu-item i[data-lucide] { width: 16px; height: 16px; flex: 0 0 auto; color: var(--text-muted); }
.act-menu-sub { margin-left: auto; padding-left: 12px; color: var(--text-muted); font-size: 12px; }
.act-menu-sep { height: 1px; margin: 4px 6px; background: var(--border-light); }

/* =====================================================================
   REPORT HUB (/reports) — the card grid behind the section tabs.
   ---------------------------------------------------------------------
   These used to live in a page-level <style> in views/reports.ejs. They are
   here so the hub inherits every token change like the rest of the app.

   ONE section is visible at a time (its tab); `.is-searching` flips the hub
   into a cross-section result list — every section shown at once, its heading
   back, and the section name surfaced on each card (.rc-sec), because a flat
   list of matches otherwise says nothing about where a report lives. The
   sections that don't match hide themselves; the FAVORITES section is always
   out of a search, since its cards are copies and would match twice.
   ===================================================================== */
.report-section { margin-top: 0; }
.reports-hub.is-searching .report-section { margin-top: 26px; }
.reports-hub.is-searching .report-section:first-of-type { margin-top: 0; }
/* The tab already names the section — the heading is search-mode only. */
.report-section-head {
  font-family: var(--font-display); font-size: 12px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted); margin: 0 0 12px; display: none;
}
.reports-hub.is-searching .report-section-head { display: block; }
.reports-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); grid-auto-rows: 1fr; gap: 16px; }

.report-card {
  position: relative; height: 100%;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow); transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.report-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); border-color: var(--accent); }
/* The whole card is the link. The star is a sibling BUTTON on top of it (a
   button nested in an <a> is invalid and eats the card's own click), so the
   link keeps a padding-right wide enough to clear it. */
.report-card .rc-link {
  display: flex; gap: 14px; align-items: flex-start; height: 100%;
  padding: 18px 44px 18px 18px; text-decoration: none; color: inherit;
}
.report-card .rc-chip {
  flex-shrink: 0; width: 40px; height: 40px; border-radius: 11px; display: grid; place-items: center;
  background: var(--accent-soft); color: var(--accent);
}
.report-card .rc-chip svg,.report-card .rc-chip i[data-lucide] { width: 20px; height: 20px; }
.report-card .rc-body { display: block; min-width: 0; }
.report-card .rc-title {
  display: block; font-family: var(--font-display); font-size: 14.5px; font-weight: 600;
  letter-spacing: -.01em; margin-bottom: 5px; color: var(--text);
}
.report-card .rc-desc {
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
  font-size: 12.5px; color: var(--muted); line-height: 1.45;
}
/* Which section this card came from — only meaningful where the grid is NOT
   one section (Favorites, and a search across all of them). */
.report-card .rc-sec {
  display: none; margin-top: 8px; font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-faint);
}
.reports-hub.is-searching .rc-sec,
.report-section[data-report-section="favorites"] .rc-sec { display: block; }

/* Star = keep this report on the Favorites tab. Off is an outline in the muted
   text colour; on is the attention signal filled, so a starred card reads at a
   glance across the grid. */
.rc-star {
  position: absolute; top: 10px; right: 10px; width: 30px; height: 30px; padding: 0;
  display: grid; place-items: center; border: 0; border-radius: 8px;
  background: transparent; color: var(--text-faint); cursor: pointer; transition: .15s;
}
.rc-star svg,.rc-star i[data-lucide] { width: 16px; height: 16px; }
.rc-star:hover { background: var(--surface-2); color: var(--text-2); }
.rc-star:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.rc-star.is-on { color: var(--signal); }
.rc-star.is-on svg { fill: currentColor; }
.rc-star.is-on:hover { color: var(--signal); }
/* `hidden` must win over any later display rule — a tab's section and a card
   filtered out by the search are hidden with the attribute. */
.report-section[hidden],.report-card[hidden] { display: none !important; }

/* ═══════════════════════════════════════════════════════════════════════════
   ITEM CHANGE LOG  (/items/:sku/history — views/item-history.ejs)

   The page shows the ITEM as it stood at a moment in time, with the fields a
   save touched marked inside it, over a TIME TRACK of the item's whole life.
   Colour carries the whole message here, so it comes from the tokens that are
   contrast-checked in both themes (--red/--green/--violet and the *-surface
   families). Never hard-code one: a diff nobody can read in dark mode is a diff
   nobody trusts.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── the item at this moment ─────────────────────────────────────────────── */
.hist-item-card { display: flex; align-items: center; gap: 12px; min-width: 0; }
.hist-photo {
    width: 62px; height: 62px; flex: 0 0 62px; object-fit: cover;
    border-radius: 10px; border: 1px solid var(--border); background: var(--surface-3);
}
.hist-photo-none { display: flex; align-items: center; justify-content: center; color: var(--text-faint); }
.hist-photo-none svg, .hist-photo-none i[data-lucide] { width: 22px; height: 22px; }
.hist-photo-skel { width: 62px; height: 62px; flex: 0 0 62px; border-radius: 10px; }
.hist-item-text { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.hist-item-title {
    font-family: var(--font-display); font-size: 16px; font-weight: 600;
    letter-spacing: -.01em; line-height: 1.3; color: var(--text-strong); overflow-wrap: anywhere;
}
.hist-item-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; font-size: 12px; }

/* ── the timeline: the track, and where you are standing ─────────────────
   The marks are the navigation, so they get the width; the cluster on the right
   says which moment you are on and offers the one thing you can do about it.
   They sit side by side because they are one thought: "this point — put it back". */
/* THE ALIGNMENT TRAP: the track reserves 26px above itself for the flag that
   labels a destructive save, so a row aligned to flex-START drops the line 26px
   below the cluster beside it and every line on the card reads ragged. Both
   columns are centred against each other instead, and the reserve is constant
   whether or not a flag is showing, so nothing shifts as you move along. */
.hist-timeline { display: flex; align-items: center; gap: 20px; margin-top: 18px; min-height: 78px; }
.hist-track-wrap { position: relative; flex: 1 1 auto; min-width: 0; padding-top: 26px; }
/* THE WIDTH TRAP: this cluster's text changes on every step ("Aug 12, 3:25 PM ·
   Joslyn Kessler" → "Aug 9, 9:05 AM · Bob"). Sized to its content, it grew and
   shrank with each name and time, and the track beside it — which takes whatever
   is left — jumped wider and narrower as you pressed the arrows, so the mark under
   your eye slid sideways. The cluster is a FIXED width; its text ellipsizes on one
   line (the full text is in the element's title), so neither the track's width
   nor the card's height can move while you step. */
.hist-at { flex: 0 0 440px; width: 440px; display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 8px 10px; }
.hist-at-nav { display: flex; align-items: center; gap: 4px; }
.hist-at-when { display: flex; flex-direction: column; min-width: 0; line-height: 1.35; }
.hist-at-when > * { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The action spans both columns under the stepper and the label it acts on. */
.hist-at > .btn.primary { grid-column: 1 / -1; justify-self: start; }
.hist-at-when strong { font-family: var(--font-display); font-size: 14px; font-weight: 600; color: var(--text-strong); }
.hist-at-when .t-mut { font-size: 11px; }

/* The whole strip is live: press anywhere and it snaps to the nearest mark,
   drag across marks to compare a span. The ticks stay real buttons underneath
   so the track is keyboard-reachable and each mark keeps its own tooltip. */
.hist-track { position: relative; height: 22px; cursor: pointer; touch-action: none; }
.hist-track::before {
    content: ""; position: absolute; left: 0; right: 0; top: 9px;
    height: 4px; border-radius: 2px; background: var(--surface-3);
}
.hist-track-fill { position: absolute; top: 9px; height: 4px; border-radius: 2px; background: var(--accent); }
.hist-tick {
    position: absolute; top: 5px; width: 3px; height: 12px; padding: 0;
    transform: translateX(-50%); border: 0; border-radius: 2px;
    background: var(--text-faint); cursor: pointer;
}
.hist-tick:hover { background: var(--text-muted); }
.hist-tick.is-loss { top: 3px; height: 16px; width: 4px; background: var(--red); }
.hist-tick.is-restore { background: var(--violet); }
/* The mark you are standing on — and the far end of a span, which looks the same
   because both ends are equally "where you are". */
.hist-tick.is-on {
    top: 2px; width: 18px; height: 18px; border-radius: 999px;
    background: var(--surface); border: 3px solid var(--accent); box-sizing: border-box;
}
.hist-tick:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The loudest thing on the page, deliberately: a save that emptied a pile of
   fields is what someone comes to this page to find. Clicking it goes there. */
.hist-flag {
    position: absolute; top: 0; transform: translateX(-50%); white-space: nowrap;
    padding: 2px 8px; border-radius: 6px; font-size: 11px; font-weight: 600; cursor: pointer;
    background: var(--danger-surface); color: var(--danger-surface-text);
    border: 1px solid var(--danger-surface-border);
}
.hist-track-ends { display: flex; justify-content: space-between; gap: 10px; font-size: 11px; margin-top: 2px; }
.hist-hint { font-style: italic; }
/* While Compare is armed the hint stops being a hint and becomes the instruction. */
.hist-hint.is-asking { font-style: normal; font-weight: 600; color: var(--accent); }
#hist-compare.is-on { background: var(--surface-3); color: var(--text); }

/* Photos in the record read as photos — a comma-separated list of URLs tells an
   operator nothing about what the item looked like. */
.hist-thumbs { display: inline-flex; align-items: center; gap: 4px; vertical-align: middle; }
.hist-thumb {
    width: 34px; height: 34px; object-fit: cover; border-radius: 6px;
    border: 1px solid var(--border); background: var(--surface-3);
}

/* ── the record ──────────────────────────────────────────────────────────── */
.hist-record .card-head { padding: 0 0 10px; }
.hist-cols { columns: 2; column-gap: 28px; }
.hist-panel { break-inside: avoid; margin: 0 0 14px; }
.hist-panel h3 {
    font-family: var(--font-display); font-size: 11px; font-weight: 600;
    letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted); margin: 0 0 4px;
}
/* A row is a field. An untouched one is grey and silent; a changed one carries a
   mark in the margin — the record reads as the item, with edits marked on it. */
.hist-row {
    display: flex; align-items: baseline; gap: 12px;
    padding: 5px 0 5px 14px; border-left: 3px solid transparent;
}
.hist-row.hist-changed, .hist-row.hist-removed { border-left-color: var(--red); }
.hist-row.hist-added { border-left-color: var(--green); }
.hist-label { flex: 0 0 132px; font-size: 12px; color: var(--text-muted); }
/* An untouched field recedes: its label and value both drop a step, so the marked
   rows carry all the signal in the "whole item" view. */
.hist-row:not(.hist-changed):not(.hist-added):not(.hist-removed) .hist-label { color: var(--text-faint); }
/* The value takes the width it needs, not all of it, so the row's action sits
   beside the text it belongs to. With `flex: 1 1 auto` the value ate the whole
   column and pushed the action out to the far right edge, where it read as
   belonging to nothing. A long value still fills the column and wraps. */
.hist-value { flex: 0 1 auto; min-width: 0; font-size: 13px; line-height: 1.45; color: var(--text); overflow-wrap: anywhere; }
.hist-row:not(.hist-changed):not(.hist-added):not(.hist-removed) .hist-value { color: var(--text-subtle); }
.hist-old { color: var(--red); text-decoration-thickness: 1px; }
.hist-new { text-decoration: none; color: var(--text); }
.hist-row.hist-added .hist-new { color: var(--green); }
.hist-empty { color: var(--text-faint); font-style: italic; }
/* Who set this value, and when. Inline after the value, never on a line of its
   own: a second line doubles the height of every row, which is what the app's
   table rule forbids for exactly this reason. */
.hist-by { color: var(--text-faint); font-size: 11px; white-space: nowrap; }
.hist-by::before { content: "· "; }
.hist-act { flex: 0 0 auto; margin-left: 4px; }
/* THE TWIN RULE, IN ITS OTHER SHAPE. The usual form sizes an icon INSIDE a box
   (`.foo svg,.foo i[data-lucide]`). Here the class is on the icon ITSELF, and
   Lucide REPLACES that <i> with an <svg> carrying the same class — so a
   descendant selector matches neither state and the icon paints at Lucide's
   default 24px. Sizing the class directly covers the placeholder and the swapped
   svg in one rule; do not "correct" this into the descendant form. */
.hist-why { width: 15px; height: 15px; flex: 0 0 auto; color: var(--text-muted); }
/* ALWAYS VISIBLE, never hover-only. A control you can only find by sweeping the
   pointer over the page is a control most people never find — the whole reason
   the restore button was reported "missing" in the first place. It stays quiet
   instead of hidden: ghost chrome, muted until you touch it. */
.hist-act .hist-one { color: var(--text-muted); }
.hist-row:hover .hist-act .hist-one, .hist-act .hist-one:hover, .hist-act .hist-one:focus-visible { color: var(--accent); }
/* Values the item holds that its log never recorded (entered before the log
   tracked those fields). They belong to no save, so they sit apart from the
   record above, under a rule, with the reason stated once. */
.hist-unlogged { margin-top: 10px; padding-top: 14px; border-top: 1px solid var(--border); }

@media (max-width: 900px) {
    .hist-cols { columns: 1; }
    .hist-label { flex-basis: 110px; }
    /* Stacked, the track keeps the full width and the cluster sits under it. */
    .hist-timeline { flex-direction: column; gap: 12px; }
    .hist-at { flex-basis: auto; width: 100%; }
    .hist-hint { display: none; }
}


/* A background scan's step banner, parked in the shared top-right .toast-region
   (never in the page flow). It appears mid-session and can run for minutes, so
   inline it would shove whatever sits under it down the page the moment the run
   starts — the "never INSERT into laid-out chrome after paint" rule. In the
   corner nothing moves. Pure status with nothing to click, so it inherits the
   region's pointer-events:none and can never eat a click on the table beneath.
   Hidden with [hidden], which needs the explicit rule because the author
   `display` below otherwise beats the UA sheet. */
.scan-banner{
  display:block;background:var(--elev);border:1px solid var(--border);border-radius:11px;
  padding:13px 16px;box-shadow:var(--shadow-lg);font-size:13px;color:var(--text);
  animation:slidein .25s ease;
}
.scan-banner[hidden]{display:none}
.scan-banner .scan-head{font-weight:600;margin-bottom:2px}
.scan-banner .scan-head svg,.scan-banner .scan-head i[data-lucide]{width:15px;height:15px}
.scan-banner .scan-step-label{font-size:12.5px}

/* ── BACKGROUND-SCAN PROGRESS ─────────────────────────────────────────────────
   A long job that runs on the SERVER and outlives the page: the inventory drift
   scan and the fraud checks' "Check past sales". Promoted here from inventory.css
   the moment a second page needed it.

   ONE ROW PER STEP, never one summed bar. The steps' units are not comparable —
   one eBay page costs tens of seconds, one local row a millisecond — so a
   combined percentage sits near zero for most of the run and then jumps. Per
   step it reads honestly, and it tells an operator who navigated away and came
   back HOW FAR it got.

   Mount every step from the first paint, including the ones not started, so the
   banner reserves its full height and nothing below it jumps as the run moves
   on — the same "reserve the box" rule as the loading skeletons. */
.inv-sync-progress-bar {
  height: 8px; background: var(--surface-3); border-radius: 999px; overflow: hidden;
  margin: 0.4rem 0;
}
.inv-sync-progress-fill {
  height: 100%;
  background: var(--accent-bar);
  border-radius: 999px;
  transition: width 0.4s ease;
}
.scan-head { display: flex; align-items: center; flex-wrap: wrap; gap: 2px 10px; }
.scan-meta { font-weight: 400; opacity: .85; }
.scan-steps { margin-top: 10px; display: flex; flex-direction: column; gap: 9px; }
.scan-step { display: grid; grid-template-columns: 1fr auto; gap: 1px 12px; align-items: baseline; }
.scan-step.is-pending { opacity: .5; }
.scan-step-count { font-family: var(--font-mono); font-size: 12px; white-space: nowrap; }
.scan-step .inv-sync-progress-bar { grid-column: 1 / -1; height: 6px; margin: 3px 0 0; }

/* ── UNSAVED-CHANGES DOT ──────────────────────────────────────────────────────
   `.btn.has-unsaved` — a Save button while the page holds edits that have not
   reached the server (the tray page sets/clears it). The page used to look the
   same saved or not; the dot is the only tell. Theme-aware: --amber. */
.btn.has-unsaved::after{content:'';display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--amber);margin-left:6px;vertical-align:middle}

/* ── INVALID CONTROL + ITS MESSAGE ────────────────────────────────────────────
   THE look of a form control holding a value it does not accept: the control
   gets aria-invalid="true" (red outline + ring) and ONE short line directly
   under it, `.field-msg`. Set by /js/numeric-only.js for "Numbers only" fields
   (tray, review, fixing, product page); reuse it for any per-field check rather
   than hand-rolling a red border. The ring is a box-shadow on purpose: a page
   sheet that pins its inputs' border colour (the product page does) still
   shows the state. The line appears only while the value is wrong. */
input[aria-invalid="true"],textarea[aria-invalid="true"],select[aria-invalid="true"]{border-color:var(--danger);box-shadow:0 0 0 2px var(--danger-surface-border)}
.field-msg{display:block;margin-top:4px;font-size:12px;line-height:1.35;font-weight:500;color:var(--danger);white-space:normal}
/* The line makes its field one line taller. The tray/review .form-grid stretches
   every field in a row to the tallest, and its text inputs grow to fill their
   field — so the NEIGHBOURS' boxes would swell with it. While a grid shows a
   message, its fields keep their own height instead. */
.form-grid:has(> .form-field > .field-msg)>.form-field{align-self:start}

/* ======================= CHANNEL READINESS (src/listing-readiness/) =======================
   The "Channel readiness" card on the product page and the review page
   (/js/listing-readiness.js): one row per channel — a status .pill and the list of
   what's missing, each item a link to the field that fixes it. */
.rdy-issues{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}
.rdy-issues li{line-height:1.45}
.rdy-card{margin:16px 0}
.rdy-card .card-head .rdy-head-end{display:inline-flex;align-items:center;gap:8px}
/* A channel's type tag (EBAY, SHOPIFY, …) beside its name — the product page's
   Listings table and the Channel readiness card (product + review page). */
.inv-listings-channel-type{font-size:.68rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--pill-purple-text);background:var(--pill-purple-bg);border:1px solid var(--pill-purple-border);border-radius:999px;padding:.08rem .5rem;align-self:flex-start}

/* ===== PRINT DOCUMENT — every page that prints labels or a list =====
   Label slips (purchase / trade-in), the scanner picklist, the shipping-labels
   sheet, the packing slip and the stocktake report all print through this ONE
   look (CLAUDE.md "Every printed label or list is ONE print document"):

     <body class="print-doc">                       light-pinned (<html data-theme="light">)
       <div class="pdoc-bar no-print">              screen only, top-right: Close · Print
       <div class="pdoc">                           the sheet: white paper on screen
         <div class="pdoc-head"><div><h1>…</h1><div class="pdoc-sub">…</div></div>
                                <div class="pdoc-meta">…</div></div>   (meta optional)
         .pdoc-block / table.pdoc-table / .pdoc-cap / .pdoc-note(.alert) / .pdoc-empty

   On screen the sheet sits on the page background like paper in a preview; on
   paper it is the whole page, with the zero @page margin (no URL footer — see
   "A printed document never shows a URL") and its own 14mm × 15mm margin. */
body.print-doc{margin:0;background:var(--bg);color:var(--text);font-family:var(--font-body);font-size:14px;line-height:1.45}
.pdoc-bar{max-width:860px;margin:0 auto;padding:16px 16px 0;display:flex;justify-content:flex-end;gap:8px;align-items:center}
.pdoc-bar .pdoc-bar-note{margin-right:auto;color:var(--text-muted);font-size:12.5px}
/* The bar inside a page that is not a print document by itself (the stocktake
   report card in the kiosk): no centring, it sits at the top of its card. */
.pdoc-bar.inline{max-width:none;padding:0;margin:0 0 14px}
.pdoc{max-width:860px;margin:12px auto 48px;background:var(--surface);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow-sm);padding:36px 44px;box-sizing:border-box}
.pdoc-head{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;border-bottom:2px solid var(--text);padding-bottom:10px;margin-bottom:16px}
.pdoc-head h1{font-family:var(--font-display);font-weight:600;letter-spacing:-.02em;font-size:22px;line-height:1.2;margin:0;color:var(--text-strong)}
.pdoc-head > div:first-child{flex:1 1 0;min-width:0}
.pdoc-sub{font-family:var(--font-mono);font-size:12px;color:var(--text-muted);margin-top:4px}
.pdoc-meta{text-align:right;font-size:13px;flex:0 0 auto}
/* A row of counts on paper (the stocktake report): compact cells, not screen KPI cards. */
.pdoc-stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:1px;background:var(--border);border:1px solid var(--border);border-radius:8px;overflow:hidden;margin:0 0 14px}
.pdoc-stats > div{padding:8px 12px;background:var(--surface)}
.pdoc-stats .lbl{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted)}
.pdoc-stats .val{font-family:var(--font-display);font-weight:600;font-size:20px;line-height:1.2;color:var(--text-strong)}
.pdoc-cap{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted);margin:0 0 4px}
.pdoc-block{border-bottom:1.5px solid var(--text);padding:10px 0 12px;break-inside:avoid;page-break-inside:avoid}
.pdoc-block:last-child{border-bottom:0}
.pdoc-block-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:6px}
.pdoc-block-title{font-family:var(--font-display);font-weight:600;font-size:15px;color:var(--text-strong)}
table.pdoc-table{width:100%;border-collapse:collapse;font-size:12.5px;margin:4px 0 0}
/* Paper, not a screen table: no coloured header bar (it is all ink), no sticky
   header, no row hover — the global thead/tbody rules are undone here. */
table.pdoc-table thead th{text-align:left;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);background:none;border-bottom:1px solid var(--border-strong);padding:6px 8px 6px 0;white-space:nowrap;position:static}
table.pdoc-table tbody tr{cursor:default}
table.pdoc-table tbody tr:hover{background:none}
table.pdoc-table tbody tr.row-mismatch:hover{background:var(--red-bg)}
table.pdoc-table td{padding:7px 8px 7px 0;border-bottom:1px solid var(--border);vertical-align:top;white-space:normal;overflow-wrap:anywhere}
table.pdoc-table .num,table.pdoc-table .qty{text-align:right;font-variant-numeric:tabular-nums}
table.pdoc-table .mono{font-family:var(--font-mono)}
.pdoc-note{border:1px solid var(--border);border-radius:8px;background:var(--surface-2);padding:10px 12px;margin:10px 0;font-size:13px;break-inside:avoid}
.pdoc-note.alert{border-color:var(--red);background:var(--red-bg);color:var(--red)}
.pdoc-note ul{margin:4px 0 0;padding-left:18px}
.pdoc-empty{color:var(--text-muted);padding:28px 0;text-align:center}
.pdoc-foot{font-size:13px;color:var(--text-muted);border-top:1px solid var(--border);padding-top:12px;margin-top:16px}
@media (max-width:768px){
  .pdoc-stats{grid-template-columns:repeat(2,minmax(0,1fr))}
  .pdoc{margin:8px 0 32px;border-radius:0;border-left:0;border-right:0;padding:20px 16px}
  .pdoc-head{flex-wrap:wrap}
  .pdoc-bar{padding:12px 16px 0}
}
@media print{
  @page{margin:0}
  body.print-doc{background:var(--surface)}
  .print-doc .no-print{display:none !important}
  .pdoc{max-width:none;margin:0;border:0;border-radius:0;box-shadow:none;padding:14mm 15mm;-webkit-box-decoration-break:clone;box-decoration-break:clone}
  .print-doc *{-webkit-print-color-adjust:exact;print-color-adjust:exact}
}
