/* ============================================================
   Factory ERP design system (from scratch)
   Dense, calm, industrial. Light and dark from one token set.
   ============================================================ */

:root {
  /* Neutrals: warm stone */
  --bg:            #f4f4f2;
  --bg-raised:     #ffffff;
  --bg-sunken:     #ecece9;
  --surface:       #ffffff;
  --border:        #dcdcd7;
  --border-strong: #c6c6bf;
  --text:          #24241f;
  --text-soft:     #5b5b53;
  --text-faint:    #8b8b81;

  /* Brand: deep teal */
  --accent:        #0f766e;
  --accent-strong: #0b5d57;
  --accent-soft:   #e2f1ef;
  --accent-text:   #ffffff;

  /* Status */
  --ok:        #15803d;  --ok-soft:     #e3f2e7;
  --warn:      #b45309;  --warn-soft:   #fdf0dd;
  --danger:    #b91c1c;  --danger-soft: #fdeaea;
  --info:      #1d4ed8;  --info-soft:   #e7edfd;

  --shadow-sm: 0 1px 2px rgba(30, 30, 20, .06);
  --shadow-md: 0 4px 16px rgba(30, 30, 20, .10), 0 1px 3px rgba(30,30,20,.07);
  --shadow-lg: 0 18px 50px rgba(30, 30, 20, .22);

  --r-sm: 6px; --r-md: 10px; --r-lg: 14px;
  --sidebar-w: 228px;
  --topbar-h: 52px;

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
}

/* The design system is px-sized and tuned for ~1400-1700px viewports (the
   factory office laptops). On big monitors the same layout renders tiny with
   dead margins, and users compensate with browser zoom — so do it for them:
   step the root zoom so the EFFECTIVE width always lands back around 1600px.
   Media queries evaluate against the unzoomed viewport, so this can't loop.
   (Firefox < 126 ignores zoom and simply stays unscaled — harmless.)
   Unlike browser zoom, CSS zoom does NOT shrink the viewport, so vh/vw
   lengths render zoom× too large; every viewport-unit rule below divides by
   --zoom to land back on the real viewport. Keep the two in lockstep. */
:root { --zoom: 1; }
@media (min-width: 2000px) { html { zoom: 1.25; } :root { --zoom: 1.25; } }
@media (min-width: 2400px) { html { zoom: 1.5; }  :root { --zoom: 1.5; } }
@media (min-width: 2800px) { html { zoom: 1.75; } :root { --zoom: 1.75; } }
@media (min-width: 3200px) { html { zoom: 2; }    :root { --zoom: 2; } }

[data-theme="dark"] {
  --bg:            #16181a;
  --bg-raised:     #1e2124;
  --bg-sunken:     #101214;
  --surface:       #1e2124;
  --border:        #30343a;
  --border-strong: #40454d;
  --text:          #e8e8e4;
  --text-soft:     #a3a49c;
  --text-faint:    #71726b;

  --accent:        #2bb3a3;
  --accent-strong: #43cdbd;
  --accent-soft:   #14332f;
  --accent-text:   #06211e;

  --ok:      #4ade80;  --ok-soft:     #143221;
  --warn:    #fbbf24;  --warn-soft:   #362a10;
  --danger:  #f87171;  --danger-soft: #3b1a1a;
  --info:    #93b4f8;  --info-soft:   #1a2440;

  --shadow-sm: 0 1px 2px rgba(0,0,0,.4);
  --shadow-md: 0 4px 16px rgba(0,0,0,.5);
  --shadow-lg: 0 18px 50px rgba(0,0,0,.6);
}

/* ---------- accent presets (picked in the Account dialog) ---------- */

[data-accent="ocean"] {
  --accent: #1d4ed8; --accent-strong: #1e40af; --accent-soft: #e4ebfc; --accent-text: #ffffff;
}
[data-theme="dark"][data-accent="ocean"] {
  --accent: #7aa5f8; --accent-strong: #a3c0fa; --accent-soft: #16233f; --accent-text: #0a1226;
}
[data-accent="violet"] {
  --accent: #6d28d9; --accent-strong: #5b21b6; --accent-soft: #efe8fc; --accent-text: #ffffff;
}
[data-theme="dark"][data-accent="violet"] {
  --accent: #b195f5; --accent-strong: #c7b2f8; --accent-soft: #251740; --accent-text: #150a2e;
}
[data-accent="magenta"] {
  --accent: #be185d; --accent-strong: #9d174d; --accent-soft: #fbe7f0; --accent-text: #ffffff;
}
[data-theme="dark"][data-accent="magenta"] {
  --accent: #f27ab0; --accent-strong: #f5a3c6; --accent-soft: #3b1226; --accent-text: #2a0a18;
}
[data-accent="ember"] {
  --accent: #c2410c; --accent-strong: #9a3412; --accent-soft: #fcece2; --accent-text: #ffffff;
}
[data-theme="dark"][data-accent="ember"] {
  --accent: #f59e6c; --accent-strong: #f8bd97; --accent-soft: #3a1d0d; --accent-text: #2a1204;
}
[data-accent="forest"] {
  --accent: #15803d; --accent-strong: #166534; --accent-soft: #e4f2e8; --accent-text: #ffffff;
}
[data-theme="dark"][data-accent="forest"] {
  --accent: #5fce8a; --accent-strong: #8adcaa; --accent-soft: #12301d; --accent-text: #06210f;
}

.accent-dots { display: flex; gap: 10px; }
.accent-dot {
  width: 28px; height: 28px; border-radius: 50%;
  border: 2px solid var(--border-strong);
  cursor: pointer; padding: 0;
}
.accent-dot.sel { border-color: var(--text); box-shadow: 0 0 0 2px var(--bg-raised), 0 0 0 4px var(--text); }
.accent-dot[data-accent="teal"]    { background: #0f766e; }
.accent-dot[data-accent="ocean"]   { background: #1d4ed8; }
.accent-dot[data-accent="violet"]  { background: #6d28d9; }
.accent-dot[data-accent="magenta"] { background: #be185d; }
.accent-dot[data-accent="ember"]   { background: #c2410c; }
.accent-dot[data-accent="forest"]  { background: #15803d; }

/* ---------- base ---------- */

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  font-family: var(--font);
  font-size: 13.5px;
  line-height: 1.45;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}
h1, h2, h3 { margin: 0; font-weight: 650; letter-spacing: -.01em; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
::selection { background: color-mix(in srgb, var(--accent) 25%, transparent); }

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

/* ---------- app shell ---------- */

.app-root { height: 100%; }
/* minmax(0, 1fr) pins the single row to the shell's height: an implicit
   auto row sizes to CONTENT, so a tall page (a long chat, a big grid)
   would grow the row straight past the fixed shell and hand the overflow
   to the document instead of .content's own scrollbar. */
.shell { display: grid; grid-template-columns: var(--sidebar-w) 1fr; grid-template-rows: minmax(0, 1fr); height: calc(100vh / var(--zoom)); }

.sidebar {
  background: var(--bg-raised);
  border-right: 1px solid var(--border);
  display: flex; flex-direction: column;
  overflow-y: auto;
}
.brand {
  display: flex; align-items: center; gap: 10px;
  padding: 14px 16px 12px;
  border-bottom: 1px solid var(--border);
}
.brand img, .brand svg { width: 30px; height: 30px; border-radius: 8px; }
/* The name is a full company name — keep it a touch smaller and let it wrap
   snugly (with the TEST chip it can need two lines). */
.brand .brand-name { font-weight: 700; font-size: 13.5px; letter-spacing: -.01em; line-height: 1.25; }

.nav { padding: 10px 8px 20px; flex: 1; }
.nav-group { margin-top: 14px; }
.nav-group:first-child { margin-top: 4px; }
.nav-group-title {
  font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
  color: var(--text-faint);
  padding: 0 10px 5px;
}
.nav-item {
  display: flex; align-items: center; gap: 9px;
  width: 100%; text-align: left;
  padding: 6.5px 10px;
  border: 0; border-radius: var(--r-sm);
  background: transparent; color: var(--text-soft);
  font: inherit; font-size: 13px; font-weight: 500;
  cursor: pointer;
}
.nav-item svg { width: 16px; height: 16px; flex: none; opacity: .75; }
.nav-item:hover { background: var(--bg-sunken); color: var(--text); }
.nav-item.active {
  background: var(--accent-soft); color: var(--accent);
  font-weight: 600;
}
.nav-item.active svg { opacity: 1; }

.main { display: flex; flex-direction: column; min-width: 0; }
.topbar {
  height: var(--topbar-h); flex: none;
  display: flex; align-items: center; gap: 12px;
  padding: 0 18px;
  background: var(--bg-raised);
  border-bottom: 1px solid var(--border);
}
.topbar .page-title { font-size: 15px; font-weight: 650; }
.topbar .spacer { flex: 1; }
.userchip {
  display: flex; align-items: center; gap: 8px;
  white-space: nowrap;
  padding: 4px 10px 4px 4px; border-radius: 99px;
  border: 1px solid var(--border);
  background: var(--surface);
  cursor: pointer; font: inherit; font-size: 12.5px; color: var(--text-soft);
}
.userchip .avatar {
  width: 24px; height: 24px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--accent); color: var(--accent-text);
  font-size: 11px; font-weight: 700;
}
.rolepill {
  font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  padding: 2px 7px; border-radius: 99px;
  background: var(--accent-soft); color: var(--accent);
}

/* HOW DATES READ, said once for the whole app, beside the language and theme
   switches (user 2026-08-02). It came out of the date COLUMN HEADERS, where
   the same app-wide fact was reprinted per column and spent width the label
   itself needed; forms keep their own hint, since that is where a typed date
   is genuinely ambiguous.

   IT MUST NOT READ AS A THIRD SWITCH. It stands between two buttons, so it
   carries none of their affordances: no border, no surface, no hover, no
   pointer cursor, and no icon — a glyph beside it made it look pressable
   (user 2026-08-02). Tabular numerals keep DD/MM/YYYY and MM/DD/YYYY the same
   width, so the controls beside it do not shift when the language changes.
   It sits to the RIGHT of the language button, which is what decides it. */
.datefmt {
  display: inline-flex; align-items: center;
  white-space: nowrap; cursor: default;
  font-size: 11.5px; font-variant-numeric: tabular-nums;
  letter-spacing: .04em;
  color: var(--text-faint, var(--text-soft));
  opacity: .85;
}
@media (max-width: 720px) { .datefmt { display: none; } }

.hamburger { display: none; }

/* min-height: 0 makes .content the page's real scroller: without it the
   flex minimum (auto = content size) lets tall pages GROW the column and
   push the overflow out to the document — the topbar scrolls away, and the
   chat page's height:100% chain silently breaks. */
.content { flex: 1; overflow: auto; padding: 18px; min-height: 0; }
.content-inner { max-width: 1500px; margin: 0 auto; display: flex; flex-direction: column; gap: 14px; }

/* ---------- buttons ---------- */

.btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface); color: var(--text);
  font: inherit; font-size: 12.5px; font-weight: 600;
  cursor: pointer; white-space: nowrap;
  box-shadow: var(--shadow-sm);
}
.btn svg { width: 14px; height: 14px; }
.btn:hover { background: var(--bg-sunken); }
.btn:active { transform: translateY(.5px); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
.btn.primary:hover { background: var(--accent-strong); border-color: var(--accent-strong); }
.btn.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, var(--border-strong)); }
.btn.danger:hover { background: var(--danger-soft); }
.btn.ghost { border-color: transparent; background: transparent; box-shadow: none; color: var(--text-soft); }
.btn.ghost:hover { background: var(--bg-sunken); color: var(--text); }
.btn.small { padding: 3.5px 8px; font-size: 12px; }

.icon-btn {
  display: inline-grid; place-items: center;
  width: 28px; height: 28px; border-radius: var(--r-sm);
  border: 0; background: transparent; color: var(--text-soft); cursor: pointer;
}
.icon-btn:hover { background: var(--bg-sunken); color: var(--text); }
/* Two icon buttons in one grid cell (a document's PDF and dot-matrix prints). */
.cell-actions { display: inline-flex; gap: 2px; }
/* display above would defeat the UA's [hidden] rule — restate it, so an
   icon button that hides itself (the column-width reset, which only
   appears once there are widths to reset) actually goes away. */
.icon-btn[hidden] { display: none; }
.icon-btn:disabled { opacity: 0.35; pointer-events: none; }
.icon-btn svg { width: 15px; height: 15px; }

/* ---------- forms ---------- */

.field { display: flex; flex-direction: column; gap: 4px; }
.field > label { font-size: 11.5px; font-weight: 650; color: var(--text-soft); }
.field .req { color: var(--danger); }
.input, select.input, textarea.input {
  font: inherit; font-size: 13px;
  color: var(--text);
  background: var(--bg-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: 6px 9px;
  width: 100%;
}
/* The Numbering screen's IN-PLACE count edit: the input sits in the very
   slot the figure held, text-sized with the spinners off, and the value
   line reserves its height in BOTH states — a full-size field there made
   the whole card JUMP the moment editing started (user 2026-08-21). Width
   rides the digit count from JS; .lower marks a value below the stored one
   (repeat warning, colour only so nothing reflows). */
.input.numedit { padding: 4px 9px; -moz-appearance: textfield; }
.input.numedit::-webkit-outer-spin-button,
.input.numedit::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.input.numedit.lower { border-color: var(--warn); color: var(--warn); }
/* THE CARET IS OURS, NOT THE BROWSER'S (user 2026-08-14, who asked twice).
   Chrome pins its own select caret to the border box and MEASURABLY ignores
   padding-right — 9px, 20px, 28px and 36px all draw it in the same place, so
   there is no way to give it air while it is the browser's. Drawing it
   ourselves is the only way to move it, and it comes out as the app's own
   glyph anyway: Lucide chevron-down at the house stroke of 1.8, the same one
   `icon()` builds everywhere else.

   It is a background image and therefore cannot say `currentColor` (a data
   URI is its own document), so the two themes carry their own stroke — the
   one place in this sheet where a colour is written twice instead of once as
   a token. --text-soft, in both cases. */
select.input {
  -webkit-appearance: none; appearance: none;
  padding-right: 28px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235b5b53' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 13px 13px;
}
[data-theme="dark"] select.input {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a3a49c' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}
.input:focus { outline: 2px solid color-mix(in srgb, var(--accent) 60%, transparent); outline-offset: 0; border-color: var(--accent); }
.input[readonly] { background: var(--bg-sunken); color: var(--text-soft); }
.input.num { text-align: right; font-variant-numeric: tabular-nums; }
textarea.input { resize: vertical; min-height: 56px; }
.checkbox-line { display: flex; align-items: center; gap: 8px; font-size: 13px; padding: 4px 0; }
.checkbox-line input { width: 15px; height: 15px; accent-color: var(--accent); }

.combo { position: relative; }
.combo > .input { padding-right: 30px; }
/* An id-backed picker's field is readonly — typing must not eat the chosen
   value — but it is a live control, not a dimmed fact: keep the normal field
   look and a pointer cursor. Searching happens in the panel's own box. */
.combo > .input[readonly] { background: var(--bg-raised); color: var(--text); cursor: pointer; }
.combo-clear {
  position: absolute; top: 50%; right: 5px; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; padding: 0; border: 0; border-radius: var(--r-sm);
  background: none; color: var(--text-faint); cursor: pointer;
}
.combo-clear:hover { color: var(--text); background: var(--bg-sunken); }
.combo-clear svg { width: 13px; height: 13px; }
.combo > .input:placeholder-shown + .combo-clear { display: none; }
.combo-list {
  position: absolute; z-index: 60; left: 0; right: 0; top: calc(100% + 3px);
  background: var(--bg-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-md);
  padding: 4px;
}
.combo-search { margin-bottom: 4px; }
.combo-items { max-height: 240px; overflow: auto; }
.combo-empty { padding: 6px 8px; font-size: 12.5px; color: var(--text-faint); }
.combo-item { padding: 6px 8px; border-radius: var(--r-sm); cursor: pointer; font-size: 12.5px; }
.combo-item:hover, .combo-item.hi { background: var(--accent-soft); color: var(--accent); }
.combo-item .sub { font-size: 11px; color: var(--text-faint); }
.combo-item.hi .sub { color: inherit; opacity: .75; }

/* ---------- cards, tiles ---------- */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}
.card .card-head {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  font-weight: 650; font-size: 13.5px;
}
.card .card-body { padding: 14px 16px; }

.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
/* Every tile is an <a> to the rows it counted (screens.js dashboardScreen):
   the WHOLE card is the link, so the number, its label and the space around
   them are one target. Hence the resets — an anchor arrives underlined and in
   the link colour, and a tile is neither. */
.tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 13px 15px;
  box-shadow: var(--shadow-sm);
  display: flex; flex-direction: column;
  min-height: 88px;
  color: inherit; text-decoration: none;
  transition: border-color .12s, box-shadow .12s;
}
/* The hover cue is the card lifting, never an underline: the underline would
   run along the LABEL and read as a link inside the tile — the same "two
   things to click" look the panels below were cleaned of. The `text-decoration:
   none` above cannot do it alone, since `a:hover` (line 145) outranks a plain
   class. */
.tile:hover { border-color: var(--accent); box-shadow: var(--shadow-md); text-decoration: none; }
.tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tile .tile-label { font-size: 11.5px; font-weight: 650; color: var(--text-soft); }
/* The label stays at the left edge and the NUMBER is centred (user
   2026-08-04). text-align, not a flex rule: the value is a full-width block,
   so it centres inside the tile rather than inside its own text. */
.tile .tile-value {
  font-size: 24px; font-weight: 700; letter-spacing: -.02em;
  margin-top: auto; font-variant-numeric: tabular-nums; text-align: center;
}
.tile.alert .tile-value { color: var(--danger); }
.tile.good .tile-value { color: var(--ok); }

/* ---------- badges ---------- */

.badge {
  display: inline-block;
  font-size: 11px; font-weight: 700;
  padding: 2px 8px; border-radius: 99px;
  background: var(--bg-sunken); color: var(--text-soft);
}
.badge.ok     { background: var(--ok-soft); color: var(--ok); }
.badge.warn   { background: var(--warn-soft); color: var(--warn); }
.badge.danger { background: var(--danger-soft); color: var(--danger); }
.badge.info   { background: var(--info-soft); color: var(--info); }
.badge.accent { background: var(--accent-soft); color: var(--accent); }

/* ---------- data grid ---------- */

.grid { display: flex; flex-direction: column; min-height: 0; }
.grid-toolbar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
}
.grid-toolbar .sep { width: 1px; height: 20px; background: var(--border); margin: 0 2px; }
.grid-toolbar .spacer { flex: 1; }
.searchbox { position: relative; }
.searchbox input {
  font: inherit; font-size: 12.5px;
  padding: 5.5px 9px 5.5px 28px;
  border: 1px solid var(--border-strong);
  border-radius: 99px;
  background: var(--bg);
  color: var(--text);
  width: 210px;
}
.searchbox svg {
  position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
  width: 13px; height: 13px; color: var(--text-faint); pointer-events: none;
}

.table-scroll { overflow: auto; }
/* Bare LIST pages put the grid straight into .content-inner: there the grid
   is the scroller (viewport cap, so the toolbar and pager stay put).
   Everywhere else — stacked document pages (WO tabs, SO/PO line cards,
   dashboard) — the grid grows with its rows and the PAGE is the one
   vertical scroller, never a second nested one (2026-07-28 report: double
   scroll on the WO page's BOM tab). */
.content-inner > .grid .table-scroll { max-height: calc(100vh / var(--zoom) - 285px); }
.grid.fill .table-scroll { max-height: none; flex: 1; }

/* Detail pages whose line grid FITS the viewport instead of growing the page
   (material/supplier — a price list can run hundreds of rows): the flex
   chain hands the leftover height to the grid card, and the grid's own
   .fill mode (table-scroll flex:1, sticky headers) scrolls the rows
   internally, so .content never grows a page scrollbar. */
.content-inner.page-fill { height: 100%; }
/* The header cards keep their natural height — only the grid gives way
   (and never below a usable strip; a too-short window falls back to the
   normal page scroll). */
.content-inner.page-fill > :not(.card-fill) { flex-shrink: 0; }
.content-inner.page-fill > .card-fill {
  flex: 1; min-height: 160px;
  display: flex; flex-direction: column;
}
.card-fill > .grid { flex: 1; min-height: 0; }
/* A grid dropped straight into a card (master/detail line lists: the
   card-head is the title, the grid is the body) is a SECTION of that card,
   not a card of its own — its rounded 1px border painted a second arc just
   inside the host card's clipped corners, and its top edge doubled the
   card-head's hairline. Grids that sit bare on a page keep their chrome. */
.card > .card.grid { border: 0; border-radius: 0; box-shadow: none; }
/* Tabbed fill (the WO page): the tab strip keeps its height, the active
   grid takes the rest — the same internal row scroll as every other fill
   page. */
.card-fill > .tab-body { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.tab-body > .grid { flex: 1; min-height: 0; }
table.dt {
  border-collapse: separate; border-spacing: 0; width: 100%; font-size: 12.5px;
  /* How wide one column may grow on its own — see the filler rules below.
     grid.js reads this same property for the double-click fit, so the number
     is written down once. */
  --col-max: 340px;
}
table.dt th {
  position: sticky; top: 0; z-index: 5;
  text-align: left;
  background: var(--bg-sunken);
  color: var(--text-soft);
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  padding: 7px 10px;
  border-bottom: 1px solid var(--border-strong);
  white-space: nowrap;
  user-select: none;
}
table.dt th.sortable { cursor: pointer; }
table.dt th.sortable:hover { color: var(--text); }
table.dt th .arrow { font-size: 9px; margin-left: 3px; }
table.dt td {
  padding: 6px 10px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
  max-width: var(--col-max); overflow: hidden; text-overflow: ellipsis;
}
table.dt td.num, table.dt th.num { text-align: right; font-variant-numeric: tabular-nums; }
table.dt td.neg { color: var(--danger); font-weight: 650; }
table.dt tbody tr { cursor: default; }
table.dt tbody tr:hover td { background: color-mix(in srgb, var(--accent) 5%, transparent); }
table.dt tbody tr.selected td { background: var(--accent-soft); }
table.dt tbody tr.clickable { cursor: pointer; }
/* A row that IS its link. Every cell holds an anchor whose clickable box is
   stretched over that cell, so the cells together cover the row: a click
   anywhere opens the record, and it is a real link everywhere, so middle-click
   and copy-address work over the whole row too. The alternative, an anchor on
   the number plus a click handler on the row, is two separate ways into one
   page: the row would open on a plain click while only the number could be
   opened in a new tab. Hovering anywhere underlines the number, so the row
   says which record it is about. Only the number's anchor is a link for the
   keyboard and for screen readers — screens.js linkedRow takes the copies out
   of both, so the row still reads as ONE link.

   THE CELL IS WHAT THE STRETCH IS MEASURED AGAINST, NEVER THE ROW, and that
   is the whole reason for one anchor per cell rather than one per row. A
   table ROW is not a positioning container in every browser: WebKit ignores
   position:relative on a <tr>, so a single anchor stretched with inset:0
   measured itself against the PAGE instead, and the last such pseudo-element
   covered the entire dashboard — every tap anywhere on it opened one
   warehouse document. It only showed on phones because the desktop here is
   Chrome, which does position a row (measured 2026-08-26 in iOS Safari 26: a
   goods issue on staging, a goods receipt on production, those being the last
   paper each one holds). A <td> is a positioning container in every engine,
   and its own overflow:hidden above then clips the stretch to exactly the
   cell. border-collapse is `separate` above, which is the case where browsers
   position table parts reliably. */
table.dt tbody tr.rowlink { cursor: pointer; }
table.dt tbody tr.rowlink > td { position: relative; }
table.dt tbody tr.rowlink a.stretch::after { content: ''; position: absolute; inset: 0; }
/* THE ROW IS THE LINK, SO THE ROW IS THE ONLY THING THAT LOOKS LIKE ONE. The
   anchor takes the row's own colour and never underlines: painting it in the
   accent put a second link-looking thing inside a row that already opens from
   anywhere, and that is what a person sees no matter what the markup says
   (user 2026-08-03, twice: "I just don't want double link" / "now both the row
   and the work order no, or doc are links"). The pointer cursor and the hover
   tint already say the row opens. What the anchor is still FOR is the part
   with no appearance: open-in-new-tab, copy-address and the keyboard. */
table.dt tbody tr.rowlink a.stretch,
table.dt tbody tr.rowlink a.stretch:hover { color: inherit; text-decoration: none; }
table.dt tbody tr:last-child td { border-bottom: 0; }
table.dt .checkcell { width: 30px; text-align: center; }
table.dt .checkcell input { accent-color: var(--accent); }

.grid-foot {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 12px;
  border-top: 1px solid var(--border);
  font-size: 12px; color: var(--text-soft);
}
.grid-foot .spacer { flex: 1; }
.pager { display: flex; align-items: center; gap: 4px; }
.pager button {
  border: 1px solid var(--border-strong); background: var(--surface);
  color: var(--text-soft);
  border-radius: var(--r-sm); padding: 3px 8px; font: inherit; font-size: 12px; cursor: pointer;
}
.pager button:disabled { opacity: .4; cursor: default; }
.pager .page-now {
  font-weight: 700; color: var(--text); padding: 0 2px;
  display: inline-flex; align-items: center; gap: 5px;
}
/* Our own caret here too, for the reason given at select.input — smaller, to
   match a control that is itself smaller. */
.grid-foot select.page-size {
  border: 1px solid var(--border-strong); background: var(--surface); color: var(--text-soft);
  border-radius: var(--r-sm); padding: 3px 20px 3px 6px; font: inherit; font-size: 12px; cursor: pointer;
  -webkit-appearance: none; appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235b5b53' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 5px center; background-size: 11px 11px;
}
[data-theme="dark"] .grid-foot select.page-size {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a3a49c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}
/* The current-page box inside the pager: it doubles as jump-to-page, so it
   is a real input, sized for a page count and centred like the number the
   pager used to print. The spinners would only crowd a 3-digit box. */
.grid-foot input.jump {
  width: 46px; text-align: center; -moz-appearance: textfield; appearance: textfield;
  border: 1px solid var(--border-strong); background: var(--surface); color: var(--text);
  border-radius: var(--r-sm); padding: 3px 4px; font: inherit; font-size: 12px;
}
.grid-foot input.jump::-webkit-outer-spin-button,
.grid-foot input.jump::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
/* row highlighted after a Duplicate lands its copy (grid.flashRow) */
@keyframes rowflash { from { background: var(--accent-soft); } to { background: transparent; } }
table.dt tbody tr.flash td { animation: rowflash 1.6s ease-out; }
/* refresh in progress: dim the rows, spin the reload icon */
.grid.busy table.dt { opacity: .45; transition: opacity .15s; }
.grid.busy .grid-refresh svg,
.grid .grid-refresh.spin svg { animation: spin .7s linear infinite; }

.empty {
  padding: 40px 20px; text-align: center; color: var(--text-faint); font-size: 13px;
}

/* ---------- drawer / modal / toast ---------- */

.scrim {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(15, 15, 10, .40);
  backdrop-filter: blur(1.5px);
}
.drawer {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 95;
  /* 540: the widest edit-drawer footer (History/Delete/Duplicate/Cancel/Save)
     measures 501px incl. paddings — anything tighter clips or wraps it. */
  width: min(540px, calc(96vw / var(--zoom)));
  background: var(--bg-raised);
  border-left: 1px solid var(--border);
  box-shadow: var(--shadow-lg);
  display: flex; flex-direction: column;
  animation: slidein .18s ease-out;
}
@keyframes slidein { from { transform: translateX(30px); opacity: .4; } to { transform: none; opacity: 1; } }
.drawer-head {
  display: flex; align-items: center; gap: 10px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--border);
  font-weight: 650; font-size: 14px;
}
.drawer-head .spacer { flex: 1; }
.drawer-body { flex: 1; overflow-y: auto; padding: 16px 18px; display: flex; flex-direction: column; gap: 12px; }
.drawer-body .row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.drawer-foot {
  /* wrap, don't clip: with flex-end, an overflowing row spills off the LEFT
     edge (it cut off the History button at 460px — the five buttons measure
     ~487px in English) */
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end;
  padding: 12px 18px;
  border-top: 1px solid var(--border);
  background: var(--bg);
  border-bottom-left-radius: 0;
}

.modal-wrap {
  position: fixed; inset: 0; z-index: 95;
  display: grid; place-items: center; padding: 20px;
  outline: none; /* focused programmatically so Esc addresses the dialog */
}
.modal {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  width: 100%; max-width: 480px;
  max-height: calc(86vh / var(--zoom)); display: flex; flex-direction: column;
  animation: pop .16s ease-out;
}
.modal.wide { max-width: 860px; }
.modal.danger .modal-head { color: var(--danger); }
.modal-danger-title { display: inline-flex; align-items: center; gap: 7px; }
.modal-danger-title svg { width: 17px; height: 17px; flex: none; }
@keyframes pop { from { transform: scale(.97); opacity: .5; } to { transform: none; opacity: 1; } }
.modal-head { padding: 14px 18px; border-bottom: 1px solid var(--border); font-weight: 650; display: flex; align-items: center; }
.modal-head .spacer { flex: 1; }
.modal-body { padding: 16px 18px; overflow-y: auto; }
.modal-foot { padding: 12px 18px; border-top: 1px solid var(--border); display: flex; gap: 8px; justify-content: flex-end; }

/* A ROOMY dialog is one holding DROPDOWNS, and it does not scroll its own
   body: the WRAPPER around it scrolls instead. A dropdown panel is positioned
   against the field it hangs off, so every scrolling box between the two
   clips it — and .modal-body cut the search builder's work order list off at
   the top and again at the footer (user 2026-08-14). The wrapper is the whole
   viewport, so it clips nothing anyone can see, and the panel still travels
   with its field when the dialog is scrolled.

   CENTRED BY AUTO MARGINS, not by place-items: a box that centres with
   align-items puts the top of an over-tall child out of reach of its own
   scrollbar, and this is the one dialog that can grow past the screen (twenty
   conditions). Auto margins absorb the free space first, so alignment never
   gets to do that. The footer stays put through it all. */
.modal-wrap.roomy { display: flex; overflow: auto; }
.modal-wrap.roomy > .modal { margin: auto; max-height: none; }
.modal-wrap.roomy .modal-body { overflow: visible; }
.modal-wrap.roomy .modal-foot {
  position: sticky; bottom: 0; background: var(--bg-raised);
  border-radius: 0 0 var(--r-lg) var(--r-lg);
}

.toasts {
  position: fixed; top: 14px; right: 14px; z-index: 120;
  display: flex; flex-direction: column; gap: 8px;
}
.toast {
  background: var(--bg-raised);
  color: var(--text);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-md);
  padding: 10px 14px;
  font-size: 12.5px; font-weight: 550;
  max-width: 360px;
  animation: pop .16s ease-out;
}
.toast.error { border-left-color: var(--danger); }
.toast.ok { border-left-color: var(--ok); }

/* ---------- login ---------- */

.login-wrap {
  min-height: calc(100vh / var(--zoom));
  display: grid; place-items: center;
  padding: 24px;
  background:
    radial-gradient(700px 380px at 50% -8%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 70%),
    var(--bg);
}
.login-card {
  width: 100%; max-width: 380px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 18px;
  box-shadow: var(--shadow-md);
  padding: 34px 30px 28px;
  text-align: center;
  display: flex; flex-direction: column; gap: 14px;
}
.login-card .logo { width: 52px; height: 52px; margin: 0 auto; border-radius: 13px; }
.login-card h1 { font-size: 19px; }
.login-card p { margin: 0; color: var(--text-soft); font-size: 13px; }
.login-card .btn { justify-content: center; padding: 9px 12px; font-size: 13.5px; }
.login-div { display: flex; align-items: center; gap: 10px; color: var(--text-faint); font-size: 11.5px; }
.login-div::before, .login-div::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.login-note { font-size: 11.5px !important; color: var(--text-faint) !important; }
.login-code { display: flex; flex-direction: column; gap: 8px; text-align: left; }
/* display:flex above would defeat the UA's [hidden] rule — restate it. */
.login-code[hidden] { display: none; }
.login-code label { font-size: 12px; color: var(--text-soft); }
.login-code input { text-transform: uppercase; text-align: center; letter-spacing: .14em; font-family: var(--mono); }
.login-code input::placeholder { letter-spacing: normal; }

/* ---------- detail pages ---------- */

.page-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.page-head h2 { font-size: 17px; }
.page-head .crumb { color: var(--text-faint); font-size: 12.5px; }
.page-head .crumb-link {
  display: inline-flex; align-items: center; gap: 2px;
  border: 0; background: none; padding: 0; cursor: pointer;
  font: inherit; font-size: 12.5px; color: var(--text-faint);
}
.page-head .crumb-link:hover { color: var(--accent); text-decoration: underline; }
.page-head .crumb-link svg { width: 13px; height: 13px; }
.page-head .spacer { flex: 1; }

.kv-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 10px 22px;
}
.kv { display: flex; flex-direction: column; gap: 1px; }
.kv .k { font-size: 11px; font-weight: 700; color: var(--text-faint); text-transform: uppercase; letter-spacing: .04em; }
.kv .v { font-size: 13.5px; font-weight: 550; }
.kv .v.num { font-variant-numeric: tabular-nums; }

.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); padding: 0 8px; }
.tab {
  border: 0; background: transparent; font: inherit;
  font-size: 12.5px; font-weight: 600; color: var(--text-soft);
  padding: 9px 12px; cursor: pointer;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.tab:hover { color: var(--text); }
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }

/* Product photo gallery: thumbnail tiles with an on-hover delete, click for
   full size (in a wide modal). */
.photo-grid { display: flex; flex-wrap: wrap; gap: 10px; }
.photo-thumb {
  position: relative; width: 128px; height: 128px;
  border: 1px solid var(--border); border-radius: var(--r-md);
  overflow: hidden; cursor: zoom-in;
  background: var(--bg-sunken);
}
.photo-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photo-thumb .photo-del {
  position: absolute; top: 5px; right: 5px;
  width: 24px; height: 24px;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(20, 20, 15, .55); color: #fff;
  opacity: 0; transition: opacity .12s;
}
.photo-thumb:hover .photo-del, .photo-thumb .photo-del:focus-visible { opacity: 1; }
@media (hover: none) { .photo-thumb .photo-del { opacity: 1; } }
.photo-thumb .photo-del:hover { background: var(--danger); }
.photo-thumb .photo-del svg { width: 13px; height: 13px; }
.photo-full { display: block; max-width: 100%; max-height: 72vh; margin: 0 auto; border-radius: var(--r-sm); }

.approvals { display: flex; gap: 14px; flex-wrap: wrap; }
.approval {
  display: flex; align-items: center; gap: 8px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 8px 13px; font-size: 12.5px; font-weight: 600;
  background: var(--surface); cursor: pointer;
}
.approval input { width: 15px; height: 15px; accent-color: var(--ok); }
.approval.on { border-color: color-mix(in srgb, var(--ok) 45%, var(--border)); background: var(--ok-soft); color: var(--ok); }

/* ---------- misc ---------- */

.loading { display: grid; place-items: center; padding: 50px; color: var(--text-faint); }
.spinner {
  width: 22px; height: 22px; border-radius: 50%;
  border: 2.5px solid var(--border);
  border-top-color: var(--accent);
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.muted { color: var(--text-faint); }
.mono { font-family: var(--mono); font-size: 12px; }
.hist-row { border-bottom: 1px solid var(--border); padding: 8px 0; font-size: 12.5px; }
.hist-row .mono { white-space: pre-wrap; word-break: break-all; color: var(--text-soft); }

@media (max-width: 900px) {
  .shell { grid-template-columns: 1fr; }
  .sidebar { position: fixed; z-index: 80; width: var(--sidebar-w); height: 100vh; transform: translateX(-100%); transition: transform .18s; }
  .sidebar.open { transform: none; box-shadow: var(--shadow-lg); }
  .hamburger { display: inline-grid; }
  .content { padding: 12px; }
  .topbar { padding: 0 10px; gap: 8px; }
  .two-col { grid-template-columns: 1fr; }
}

@media (max-width: 560px) {
  .userchip .userchip-name { display: none; }
}

/* ---------- history diff viewer ---------- */

.hist-block {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  margin-bottom: 10px;
  overflow: hidden;
}
.hist-head {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 6px 10px;
  background: var(--bg-sunken);
  font-size: 12px;
}
.hist-note { padding: 8px 12px; font-size: 12.5px; color: var(--text-soft); }
table.hist-diff { width: 100%; border-collapse: collapse; font-size: 12px; }
.hist-diff th {
  text-align: left; padding: 5px 12px 3px;
  font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-faint);
}
/* Uniform row rhythm (verified by measurement): a fixed min-height keeps rows
   with EMPTY values exactly as tall as filled ones (an empty cell has no line
   box, which otherwise shrinks its row), and middle alignment centers the two
   different font sizes without baseline coupling. Wrapped values still grow. */
.hist-diff td { padding: 4px 12px; border-top: 1px solid var(--border); vertical-align: middle; line-height: 18px; height: 27px; word-break: break-word; }
/* NOT class "field": the generic form rule `.field { display:flex }` would
   turn the cell into a flex box, breaking table-cell border collapsing (the
   row lines then jog at the column boundary). */
.hist-diff td.hist-field { font-family: var(--mono); font-size: 11px; color: var(--text-soft); white-space: nowrap; }
.hist-diff td.before { color: var(--danger); }
.hist-diff td.after { color: var(--ok); }

.hist-diff tr.changed td { background: color-mix(in srgb, var(--accent) 10%, transparent); }
.hist-diff tr.changed td.hist-field { color: var(--accent); font-weight: 700; }

/* ---------- test-environment watermark ---------- */

.test-watermark {
  position: fixed; bottom: 10px; left: 50%; transform: translateX(-50%);
  z-index: 200; pointer-events: none;
  padding: 4px 14px; border-radius: 99px;
  background: color-mix(in srgb, var(--warn) 12%, var(--bg-raised));
  border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
  color: var(--warn);
  font-size: 11.5px; font-weight: 700; letter-spacing: .05em;
  box-shadow: var(--shadow-md);
  opacity: .92;
}
.test-chip {
  display: inline-block; vertical-align: 2px;
  margin-left: 7px; padding: 1px 6px; border-radius: 99px;
  background: var(--warn-soft); color: var(--warn);
  font-size: 9.5px; font-weight: 800; letter-spacing: .08em;
}

/* The sign-in warning and the acknowledgement dialog. Both wear the WARNING
   colour rather than the accent: this is the one thing on the page that must
   not read as decoration. */
.test-banner {
  text-align: left;
  padding: 10px 12px; border-radius: var(--r-md);
  background: color-mix(in srgb, var(--warn) 12%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
  color: var(--text);
  font-size: 12px; line-height: 1.45;
}
.test-banner-title {
  display: flex; align-items: center; gap: 6px;
  margin-bottom: 3px;
  color: var(--warn); font-weight: 800; letter-spacing: .06em;
}
.test-banner-title svg { width: 15px; height: 15px; flex: none; }
.modal.ack { max-width: 460px; border-color: color-mix(in srgb, var(--warn) 45%, var(--border)); }
.modal.ack .modal-head { color: var(--warn); }
.test-ack-title { display: inline-flex; align-items: center; gap: 7px; }
.test-ack-title svg { width: 17px; height: 17px; flex: none; }
.test-ack p { margin: 0 0 9px; font-size: 13px; line-height: 1.5; }
.test-ack p:last-child { margin-bottom: 0; }
.test-ack-host {
  font-family: var(--mono); font-size: 12px !important;
  color: var(--text-faint); word-break: break-all;
}

/* ---------- app-language date field ---------- */

.datefield { position: relative; }
.datefield .input { padding-right: 34px; }
.datefield .cal-btn {
  position: absolute; right: 3px; top: 50%; transform: translateY(-50%);
  width: 26px; height: 26px;
}
.datefield .date-hidden {
  position: absolute; right: 8px; bottom: 0;
  width: 1px; height: 1px; opacity: 0; pointer-events: none; border: 0; padding: 0;
}
.input.invalid { border-color: var(--danger); outline-color: var(--danger); }

/* ---------- passkey manager (account dialog) ---------- */

.pk-list { display: flex; flex-direction: column; gap: 6px; }
.pk-row {
  display: flex; align-items: center; gap: 9px;
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
}
.pk-row > svg { width: 16px; height: 16px; color: var(--text-soft); flex: none; }
.pk-row .pk-name { font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ---------- column widths ---------- */

/* A COLUMN IS AS WIDE AS WHAT IS IN IT, up to --col-max — never stretched to
   use up the leftover width of a wide screen. A three-letter Status column
   used to come out as wide as a remark, and a long text column was handed
   623px straight past its own 340px cap (measured), because the auto table
   layout shares out the excess between every column and that sharing beats
   max-width.
   The table still spans the card (a header strip stopping short of the card's
   edge reads as a broken card), so the excess has to go somewhere: it goes to
   one empty trailing column, which is the only way to tell the layout WHICH
   column should take it. The filler collapses to nothing the moment the real
   columns want the room, and .table-scroll takes over with a sideways
   scroll. Built by grid.js (and the dashboard's two lists). */
table.dt th.filler, table.dt td.filler { width: 100%; max-width: none; padding: 0; }
/* Remembered widths (grid.js): only a fixed layout makes a th's width an
   order rather than a preference. The filler asks for `auto` there, which in a
   fixed table is the same job — whatever is left over, or nothing. */
table.dt.frozen { table-layout: fixed; }
table.dt.frozen th.filler { width: auto; }

/* ---------- resizable columns ---------- */

table.dt th { overflow: hidden; text-overflow: ellipsis; }
table.dt th .col-resizer {
  position: absolute; top: 0; right: -4px;
  width: 9px; height: 100%;
  cursor: col-resize; z-index: 7;
}
table.dt th .col-resizer::after {
  content: ''; position: absolute; right: 4px; top: 18%;
  height: 64%; width: 2px; border-radius: 2px;
  background: var(--border-strong); /* always faintly visible so the drag spot is discoverable */
}
table.dt th .col-resizer:hover::after,
table.dt th .col-resizer.active::after { background: var(--accent); }

/* ---------- workflow guide (#/guide) ---------- */
.guide-intro { margin: -4px 0 14px; color: var(--text-soft); font-size: 13.5px; max-width: 70ch; }

/* The pipeline strip: stops joined by labeled arrows; scrolls sideways on
   narrow screens rather than wrapping (the sequence IS the content). */
.guide-flow-card { padding: 8px 10px; margin-bottom: 14px; }
.flow { display: flex; align-items: stretch; gap: 2px; overflow-x: auto; padding: 6px 2px; }
.flow-node {
  flex: 1 0 108px; display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 11px 8px; border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--bg-raised); cursor: pointer; text-align: center; font: inherit; color: inherit;
  transition: border-color .15s, box-shadow .15s;
}
.flow-node:hover { border-color: var(--accent); box-shadow: var(--shadow-sm); }
.flow-node .fn-icon { color: var(--accent); display: inline-flex; }
.flow-node .fn-icon svg { width: 22px; height: 22px; }
.flow-node .fn-title { font-weight: 600; font-size: 13px; }
.flow-node .fn-sub { font-size: 11.5px; color: var(--text-soft); }
.flow-arrow {
  flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0; min-width: 62px; max-width: 84px; padding: 0 3px; color: var(--text-faint); text-align: center;
}
.flow-arrow span { font-size: 11px; font-weight: 600; color: var(--accent-strong); }
.flow-arrow svg { width: 15px; height: 15px; }
[data-theme="dark"] .flow-arrow span { color: var(--accent); }

/* Step cards */
.guide-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(310px, 1fr)); gap: 14px; margin-bottom: 14px; }
.guide-step .card-head { display: flex; align-items: center; gap: 8px; }
.step-no {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%; flex: 0 0 22px;
  background: var(--accent); color: var(--accent-text); font-size: 12px; font-weight: 700;
}
.guide-points { margin: 0; padding: 10px 16px 12px 32px; display: grid; gap: 6px; }
.guide-points li { font-size: 13px; color: var(--text); line-height: 1.45; }
.guide-points li::marker { color: var(--text-faint); }

/* Glossary */
.guide-terms { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 8px 20px; }
.guide-term { display: flex; gap: 8px; font-size: 13px; align-items: baseline; }
.guide-term b { flex: 0 0 auto; min-width: 86px; }
.guide-term span { color: var(--text-soft); }

.guide .two-col { margin-bottom: 14px; }
@media (max-width: 900px) { .guide .two-col { grid-template-columns: 1fr; } }

/* ============ AI assistant (#/assistant) ============ */

/* The chat fills the content area exactly — page never scrolls, only the
   thread does. Height comes from the .content flex chain (not a 100vh
   calc, which drifts from the real chrome and ignores --zoom). */
.chat-page { height: 100%; }
.chat-card { display: flex; flex-direction: column; }
.chat-page .chat-card { flex: 1; min-height: 0; }
.chat-top { display: flex; gap: 8px; align-items: center; padding: 10px 12px; border-bottom: 1px solid var(--border); }
.chat-conv-select { flex: 1; min-width: 0; }
.chat-thread { flex: 1; min-height: 0; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 10px; }
.chat-msg { display: flex; flex-direction: column; max-width: 78%; }
.chat-msg.user { align-self: flex-end; align-items: flex-end; }
.chat-msg.assistant { align-self: flex-start; align-items: flex-start; }
.chat-bubble {
  padding: 9px 12px; border-radius: var(--r-md); font-size: 13.5px; line-height: 1.5;
  overflow-wrap: anywhere; box-shadow: var(--shadow-sm); min-width: 0; max-width: 100%;
}
/* The user's own text is plain and keeps its typed line breaks; assistant
   answers are Markdown-rendered (md.js) and manage their own layout. */
.chat-msg.user .chat-bubble, .chat-bubble.refusal { white-space: pre-wrap; }
.chat-msg.user .chat-bubble { background: var(--accent); color: var(--accent-text); border-bottom-right-radius: 4px; }
/* The global ::selection is a translucent wash of the ACCENT — invisible on
   this bubble, whose background is the accent itself. Invert the pair
   instead: the accent-text/accent-strong tokens are a designed contrast
   pair in every accent preset and both themes, so selected text reads
   clearly everywhere. */
.chat-msg.user .chat-bubble::selection,
.chat-msg.user .chat-bubble ::selection {
  background: var(--accent-text);
  color: var(--accent-strong);
}
.chat-msg.assistant .chat-bubble {
  background: var(--bg-sunken); color: var(--text);
  border: 1px solid var(--border); border-bottom-left-radius: 4px;
}
.chat-bubble.refusal { background: var(--danger-soft); color: var(--danger); border-color: var(--danger); }
/* "…cut off" notice, inside the bubble under a truncated answer. */
.chat-cutoff {
  display: flex; align-items: center; gap: 5px; margin-top: 8px;
  padding-top: 6px; border-top: 1px dashed var(--border-strong);
  font-size: 11.5px; color: var(--warn);
}
.chat-cutoff svg { width: 13px; height: 13px; flex: 0 0 auto; }

/* Markdown content inside an answer bubble. */
.chat-md > :last-child { margin-bottom: 0; }
.chat-md p { margin: 0 0 8px; }
.chat-md .md-h { font-weight: 700; margin: 10px 0 6px; }
.chat-md > .md-h:first-child { margin-top: 0; }
.chat-md .md-h1 { font-size: 15px; }
.chat-md .md-h2 { font-size: 14px; }
.chat-md .md-h3 { font-size: 13.5px; }
.chat-md ul, .chat-md ol { margin: 0 0 8px; padding-left: 20px; display: grid; gap: 3px; }
.chat-md .table-scroll { margin: 0 0 8px; max-width: 100%; }
.chat-md .dt { font-size: 12.5px; }
.chat-md .dt td, .chat-md .dt th { white-space: nowrap; }
.chat-md code {
  font-family: var(--mono); font-size: 12px;
  background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 0 4px;
}
.chat-md pre {
  margin: 0 0 8px; padding: 8px 10px; overflow-x: auto;
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--r-sm);
}
.chat-md pre code { background: none; border: 0; padding: 0; }
.chat-md hr { border: 0; border-top: 1px solid var(--border); margin: 8px 0; }
.chat-trace { display: flex; gap: 5px; align-items: center; margin-top: 4px; font-size: 11px; color: var(--text-faint); }
.chat-trace svg { width: 12px; height: 12px; flex: 0 0 auto; }
/* stretch (not flex-end): the textarea and the actions column share one
   height, so the box's top edge lines up with the attach button and its
   bottom with Send. */
.chat-compose { display: flex; gap: 8px; align-items: stretch; padding: 10px 12px; border-top: 1px solid var(--border); }
.chat-compose textarea { flex: 1; resize: none; }
/* Actions in a column on the right — attach above Send, equal width AND
   equal height: the same arrangement as the azcode ticket reply composer.
   The column stretch widens the buttons past their natural size, so the
   content (a lone paperclip) must center itself. */
.chat-compose-actions { display: flex; flex-direction: column; gap: 8px; }
.chat-compose-actions .btn { flex: 1; justify-content: center; }
.chat-remaining { padding: 0 12px 8px; font-size: 11px; text-align: right; }

/* Typing indicator: three blinking dots while the answer is on its way. */
.chat-typing { display: flex; gap: 4px; padding: 3px 2px; }
.chat-typing span {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--text-faint); animation: chat-blink 1.2s infinite;
}
.chat-typing span:nth-child(2) { animation-delay: .2s; }
.chat-typing span:nth-child(3) { animation-delay: .4s; }
@keyframes chat-blink { 0%, 80%, 100% { opacity: .25; } 40% { opacity: 1; } }

/* Attachments: picked-file chips in the composer (draggable to reorder)
   and sent attachments under thread bubbles. */
.chat-card.drop-target { outline: 2px dashed var(--accent); outline-offset: -6px; }
.chat-files {
  display: flex; flex-wrap: wrap; gap: 6px;
  padding: 8px 12px 0; border-top: 1px solid var(--border);
}
.chat-files[hidden] { display: none; }
.chat-chip, .chat-att-chip {
  display: inline-flex; align-items: center; gap: 6px;
  max-width: 240px; padding: 4px 8px; font-size: 12px; color: var(--text);
  background: var(--bg-sunken); border: 1px solid var(--border);
  border-radius: var(--r-sm); cursor: pointer;
}
.chat-chip { cursor: grab; }
.chat-chip.dragging { opacity: .4; }
.chat-chip svg, .chat-att-chip svg { width: 14px; height: 14px; flex: 0 0 auto; color: var(--text-faint); }
.chip-thumb { width: 22px; height: 22px; object-fit: cover; border-radius: 4px; flex: 0 0 auto; }
.chat-att-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-att-size { color: var(--text-faint); flex: 0 0 auto; }
.chip-remove {
  border: 0; background: none; color: var(--text-faint); cursor: pointer;
  font-size: 15px; line-height: 1; padding: 0 2px; flex: 0 0 auto;
}
.chip-remove:hover { color: var(--danger); }
.chat-atts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
.chat-msg.user .chat-atts { justify-content: flex-end; }
.chat-att-thumb {
  border: 1px solid var(--border); border-radius: var(--r-sm); padding: 0;
  background: var(--bg-sunken); cursor: zoom-in; overflow: hidden; max-width: 220px;
}
.chat-att-thumb img { display: block; max-width: 220px; max-height: 160px; object-fit: cover; }

/* ---------- search builder (search.js) ---------- */

/* The toolbar's Build Search button wears its condition count, and goes accent
   while any are running: that badge is the only thing on screen saying the
   grid is showing part of its table rather than all of it. */
.grid-search.on {
  border-color: var(--accent); color: var(--accent); background: var(--accent-soft);
}

/* A chip naming what a list arrived narrowed by — a dashboard tile's filter,
   which the Search dialog cannot express and which nothing else on the toolbar
   would admit to. It wears the same accent as the button above for the same
   reason: both say "this table is showing part of itself". There is no chip
   when nothing is filtered, which is the whole point of it replacing a row of
   dropdowns that were on screen for ever. */
.filter-chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 4px 3px 10px;
  border: 1px solid var(--accent); border-radius: 99px;
  background: var(--accent-soft); color: var(--accent);
  font-size: 12.5px; font-weight: 600; white-space: nowrap;
}
.filter-chip .icon-btn { width: 18px; height: 18px; color: inherit; }
.filter-chip .icon-btn:hover { background: color-mix(in srgb, var(--accent) 18%, transparent); }
.filter-chip svg { width: 12px; height: 12px; }

.sb { display: flex; flex-direction: column; gap: 10px; }
.sb-head { display: flex; align-items: center; gap: 8px; }
.sb-lead { font-size: 12px; font-weight: 650; color: var(--text-soft); white-space: nowrap; }
.sb-pick { flex: 1; min-width: 0; }
.sb-add { flex: none; padding: 6px 10px; }

.sb-rows { display: flex; flex-direction: column; gap: 6px; }

/* ONE GRID FOR EVERY ROW, not a flex row per condition: the field names and
   the operators have to line up down the dialog, or three conditions read as
   three unrelated controls. The name column is capped rather than sized to
   content so one long label cannot push every value box off to the right. */
.sb-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr) minmax(0, 1.6fr) auto;
  align-items: center; gap: 8px;
}
.sb-name {
  font-size: 12.5px; font-weight: 650; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sb-name::after { content: ':'; }
/* The value cell holds a plain input, a date box or a whole combo, and the
   combo's dropdown is positioned against it — so it needs to be the
   positioning context and must NOT clip. */
.sb-val { position: relative; min-width: 0; }
/* Date and combo values arrive wrapped in the form kit's .field, which is
   built to sit under a label. Here the row's own name column is the label.
   (A fixed list is a bare select and needs nothing.) */
.sb-val > .field { gap: 0; }
.sb-del { flex: none; }
.sb-del svg { width: 13px; height: 13px; }

.sb-empty {
  padding: 14px 4px; text-align: center;
  font-size: 12.5px; color: var(--text-faint);
}

/* A dialog with a dozen conditions scrolls, but its dropdowns must not be
   clipped by that scroll — so the panel stays visible and the MODAL's own
   body does the scrolling (see .modal max-height). */
@media (max-width: 700px) {
  /* Too narrow for four columns: the name takes its own line above the
     controls, which keeps the value box wide enough to type a date into. */
  .sb-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) auto; }
  .sb-name { grid-column: 1 / -1; }
}
